mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add early access badges for dynamic parameters (#18114)
Workspace creation page <img width="1438" alt="Screenshot 2025-05-30 at 13 38 22" src="https://github.com/user-attachments/assets/bac94f3a-b695-4662-9a89-7777d03d8f74" /> Workspace parameter settings <img width="1432" alt="Screenshot 2025-05-30 at 13 37 19" src="https://github.com/user-attachments/assets/f1f803a6-b99a-416c-a085-38bafc2ef4e4" /> <img width="1429" alt="Screenshot 2025-05-30 at 13 43 27" src="https://github.com/user-attachments/assets/cb1d37a4-8b79-4858-846e-3b1deb0a63cf" />
This commit is contained in:
@@ -12,27 +12,30 @@ const meta: Meta<typeof FeatureStageBadge> = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FeatureStageBadge>;
|
||||
|
||||
export const MediumBeta: Story = {
|
||||
args: {
|
||||
size: "md",
|
||||
},
|
||||
};
|
||||
|
||||
export const SmallBeta: Story = {
|
||||
args: {
|
||||
size: "sm",
|
||||
contentType: "beta",
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeBeta: Story = {
|
||||
args: {
|
||||
size: "lg",
|
||||
},
|
||||
};
|
||||
|
||||
export const MediumExperimental: Story = {
|
||||
export const MediumBeta: Story = {
|
||||
args: {
|
||||
size: "md",
|
||||
contentType: "experimental",
|
||||
contentType: "beta",
|
||||
},
|
||||
};
|
||||
|
||||
export const SmallEarlyAccess: Story = {
|
||||
args: {
|
||||
size: "sm",
|
||||
contentType: "early_access",
|
||||
},
|
||||
};
|
||||
|
||||
export const MediumEarlyAccess: Story = {
|
||||
args: {
|
||||
size: "md",
|
||||
contentType: "early_access",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import Link from "@mui/material/Link";
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { HelpTooltipContent } from "components/HelpTooltip/HelpTooltip";
|
||||
import { Popover, PopoverTrigger } from "components/deprecated/Popover/Popover";
|
||||
import { Link } from "components/Link/Link";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import type { FC, HTMLAttributes, ReactNode } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
import { docs } from "utils/docs";
|
||||
|
||||
/**
|
||||
@@ -11,132 +14,73 @@ import { docs } from "utils/docs";
|
||||
* ensure that we can't accidentally make typos when writing the badge text.
|
||||
*/
|
||||
export const featureStageBadgeTypes = {
|
||||
early_access: "early access",
|
||||
beta: "beta",
|
||||
experimental: "experimental",
|
||||
} as const satisfies Record<string, ReactNode>;
|
||||
|
||||
type FeatureStageBadgeProps = Readonly<
|
||||
Omit<HTMLAttributes<HTMLSpanElement>, "children"> & {
|
||||
contentType: keyof typeof featureStageBadgeTypes;
|
||||
labelText?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
showTooltip?: boolean;
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
>;
|
||||
|
||||
const badgeColorClasses = {
|
||||
early_access: "bg-surface-orange text-content-warning",
|
||||
beta: "bg-surface-sky text-highlight-sky",
|
||||
} as const;
|
||||
|
||||
const badgeSizeClasses = {
|
||||
sm: "text-xs font-medium px-2 py-1",
|
||||
md: "text-base px-2 py-1",
|
||||
} as const;
|
||||
|
||||
export const FeatureStageBadge: FC<FeatureStageBadgeProps> = ({
|
||||
contentType,
|
||||
labelText = "",
|
||||
size = "md",
|
||||
showTooltip = true, // This is a temporary until the deprecated popover is removed
|
||||
className,
|
||||
...delegatedProps
|
||||
}) => {
|
||||
const colorClasses = badgeColorClasses[contentType];
|
||||
const sizeClasses = badgeSizeClasses[size];
|
||||
|
||||
return (
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
{({ isOpen }) => (
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
css={[
|
||||
styles.badge,
|
||||
size === "sm" && styles.badgeSmallText,
|
||||
size === "lg" && styles.badgeLargeText,
|
||||
isOpen && styles.badgeHover,
|
||||
]}
|
||||
className={cn(
|
||||
"block max-w-fit cursor-default flex-shrink-0 leading-none whitespace-nowrap border rounded-md transition-colors duration-200 ease-in-out bg-transparent border-solid border-transparent",
|
||||
sizeClasses,
|
||||
colorClasses,
|
||||
className,
|
||||
)}
|
||||
{...delegatedProps}
|
||||
>
|
||||
<span style={visuallyHidden}> (This is a</span>
|
||||
<span className="sr-only"> (This is a</span>
|
||||
<span className="first-letter:uppercase">
|
||||
{labelText && `${labelText} `}
|
||||
{featureStageBadgeTypes[contentType]}
|
||||
</span>
|
||||
<span style={visuallyHidden}> feature)</span>
|
||||
<span className="sr-only"> feature)</span>
|
||||
</span>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
|
||||
{showTooltip && (
|
||||
<HelpTooltipContent
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
>
|
||||
<p css={styles.tooltipDescription}>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent align="start" className="max-w-xs text-sm">
|
||||
<p className="m-0">
|
||||
This feature has not yet reached general availability (GA).
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href={docs("/install/releases/feature-stages")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
css={styles.tooltipLink}
|
||||
className="font-semibold"
|
||||
>
|
||||
Learn about feature stages
|
||||
<span style={visuallyHidden}> (link opens in new tab)</span>
|
||||
<span className="sr-only"> (link opens in new tab)</span>
|
||||
</Link>
|
||||
</HelpTooltipContent>
|
||||
)}
|
||||
</Popover>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
badge: (theme) => ({
|
||||
// Base type is based on a span so that the element can be placed inside
|
||||
// more types of HTML elements without creating invalid markdown, but we
|
||||
// still want the default display behavior to be div-like
|
||||
display: "block",
|
||||
maxWidth: "fit-content",
|
||||
|
||||
// Base style assumes that medium badges will be the default
|
||||
fontSize: "0.75rem",
|
||||
|
||||
cursor: "default",
|
||||
flexShrink: 0,
|
||||
padding: "4px 8px",
|
||||
lineHeight: 1,
|
||||
whiteSpace: "nowrap",
|
||||
border: `1px solid ${theme.branding.featureStage.border}`,
|
||||
color: theme.branding.featureStage.text,
|
||||
backgroundColor: theme.branding.featureStage.background,
|
||||
borderRadius: "6px",
|
||||
transition:
|
||||
"color 0.2s ease-in-out, border-color 0.2s ease-in-out, background-color 0.2s ease-in-out",
|
||||
}),
|
||||
|
||||
badgeHover: (theme) => ({
|
||||
color: theme.branding.featureStage.hover.text,
|
||||
borderColor: theme.branding.featureStage.hover.border,
|
||||
backgroundColor: theme.branding.featureStage.hover.background,
|
||||
}),
|
||||
|
||||
badgeLargeText: {
|
||||
fontSize: "1rem",
|
||||
},
|
||||
|
||||
badgeSmallText: {
|
||||
// Have to beef up font weight so that the letters still maintain the
|
||||
// same relative thickness as all our other main UI text
|
||||
fontWeight: 500,
|
||||
fontSize: "0.625rem",
|
||||
},
|
||||
|
||||
tooltipTitle: (theme) => ({
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
fontFamily: "inherit",
|
||||
fontSize: 18,
|
||||
margin: 0,
|
||||
lineHeight: 1,
|
||||
paddingBottom: "8px",
|
||||
}),
|
||||
|
||||
tooltipDescription: {
|
||||
margin: 0,
|
||||
lineHeight: 1.4,
|
||||
paddingBottom: "8px",
|
||||
},
|
||||
|
||||
tooltipLink: {
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
},
|
||||
} as const satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { FriendlyDiagnostic, PreviewParameter } from "api/typesGenerated";
|
||||
import { Alert } from "components/Alert/Alert";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { Badge } from "components/Badge/Badge";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
|
||||
import { Input } from "components/Input/Input";
|
||||
import { Label } from "components/Label/Label";
|
||||
import { Link } from "components/Link/Link";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -353,21 +353,39 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 max-w-screen-md mx-auto">
|
||||
<header className="flex flex-col items-start gap-3 mt-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar
|
||||
variant="icon"
|
||||
size="md"
|
||||
src={template.icon}
|
||||
fallback={template.name}
|
||||
/>
|
||||
<p className="text-base font-medium m-0">
|
||||
{template.display_name.length > 0
|
||||
? template.display_name
|
||||
: template.name}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 justify-between w-full">
|
||||
<span className="flex items-center gap-2">
|
||||
<Avatar
|
||||
variant="icon"
|
||||
size="md"
|
||||
src={template.icon}
|
||||
fallback={template.name}
|
||||
/>
|
||||
<p className="text-base font-medium m-0">
|
||||
{template.display_name.length > 0
|
||||
? template.display_name
|
||||
: template.name}
|
||||
</p>
|
||||
{template.deprecated && (
|
||||
<Badge variant="warning" size="sm">
|
||||
Deprecated
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
{experimentalFormContext && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={experimentalFormContext.toggleOptedOut}
|
||||
>
|
||||
<Undo2 />
|
||||
Classic workspace creation
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
<h1 className="text-3xl font-semibold m-0">New workspace</h1>
|
||||
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -389,19 +407,11 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
|
||||
{template.deprecated && <Pill type="warning">Deprecated</Pill>}
|
||||
|
||||
{experimentalFormContext && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={experimentalFormContext.toggleOptedOut}
|
||||
>
|
||||
<Undo2 />
|
||||
Use the classic workspace creation flow
|
||||
</Button>
|
||||
)}
|
||||
<FeatureStageBadge
|
||||
contentType={"early_access"}
|
||||
size="sm"
|
||||
labelText="Dynamic parameters"
|
||||
/>
|
||||
</header>
|
||||
|
||||
<form
|
||||
@@ -555,7 +565,7 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Label className="text-sm">Preset</Label>
|
||||
<FeatureStageBadge contentType={"beta"} size="md" />
|
||||
<FeatureStageBadge contentType={"beta"} size="sm" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="max-w-lg">
|
||||
|
||||
@@ -53,7 +53,7 @@ export const Section: FC<SectionProps> = ({
|
||||
{featureStage && (
|
||||
<FeatureStageBadge
|
||||
contentType={featureStage}
|
||||
size="lg"
|
||||
size="md"
|
||||
css={{ marginBottom: "5px" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
+10
-10
@@ -117,18 +117,18 @@ export const WorkspaceParametersPageView: FC<
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<header className="flex flex-col items-start gap-2">
|
||||
<span className="flex flex-row justify-between items-center gap-2">
|
||||
<span className="flex flex-row justify-between w-full items-center gap-2">
|
||||
<h1 className="text-3xl m-0">Workspace parameters</h1>
|
||||
{experimentalFormContext && (
|
||||
<ShadcnButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={experimentalFormContext.toggleOptedOut}
|
||||
>
|
||||
Try out the new workspace parameters ✨
|
||||
</ShadcnButton>
|
||||
)}
|
||||
</span>
|
||||
{experimentalFormContext && (
|
||||
<ShadcnButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={experimentalFormContext.toggleOptedOut}
|
||||
>
|
||||
Try out the new workspace parameters ✨
|
||||
</ShadcnButton>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{submitError && !isApiValidationError(submitError) ? (
|
||||
|
||||
+40
-32
@@ -9,6 +9,7 @@ import type {
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
|
||||
import { Link } from "components/Link/Link";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import {
|
||||
@@ -203,39 +204,46 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
||||
</Helmet>
|
||||
|
||||
<header className="flex flex-col items-start gap-2">
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
<h1 className="text-3xl m-0">Workspace parameters</h1>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<CircleHelp className="size-icon-xs text-content-secondary" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs text-sm">
|
||||
Dynamic Parameters enhances Coder's existing parameter system
|
||||
with real-time validation, conditional parameter behavior, and
|
||||
richer input types.
|
||||
<br />
|
||||
<Link
|
||||
href={docs(
|
||||
"/admin/templates/extending-templates/parameters#enable-dynamic-parameters-early-access",
|
||||
)}
|
||||
>
|
||||
View docs
|
||||
</Link>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<span className="flex flex-row items-center gap-2 justify-between w-full">
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
<h1 className="text-3xl m-0">Workspace parameters</h1>
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<CircleHelp className="size-icon-xs text-content-secondary" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs text-sm">
|
||||
Dynamic Parameters enhances Coder's existing parameter system
|
||||
with real-time validation, conditional parameter behavior, and
|
||||
richer input types.
|
||||
<br />
|
||||
<Link
|
||||
href={docs(
|
||||
"/admin/templates/extending-templates/parameters#enable-dynamic-parameters-early-access",
|
||||
)}
|
||||
>
|
||||
View docs
|
||||
</Link>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
{experimentalFormContext && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={experimentalFormContext.toggleOptedOut}
|
||||
>
|
||||
<Undo2 />
|
||||
Classic workspace parameters
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
{experimentalFormContext && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={experimentalFormContext.toggleOptedOut}
|
||||
>
|
||||
<Undo2 />
|
||||
Use the classic workspace parameters
|
||||
</Button>
|
||||
)}
|
||||
<FeatureStageBadge
|
||||
contentType={"early_access"}
|
||||
size="sm"
|
||||
labelText="Dynamic parameters"
|
||||
/>
|
||||
</header>
|
||||
|
||||
{Boolean(error) && <ErrorAlert error={error} />}
|
||||
|
||||
Reference in New Issue
Block a user