refactor: match StatusIndicator component with the new designs (#16458)

Reference:
https://www.figma.com/design/WfqIgsTFXN2BscBSSyXWF8/Coder-kit?node-id=489-4278&m=dev
This commit is contained in:
Bruno Quaresma
2025-02-07 15:11:56 -03:00
committed by GitHub
parent 15d5563423
commit 8a3a79f527
10 changed files with 184 additions and 83 deletions

View File

@ -1,10 +1,17 @@
import type { Meta, StoryObj } from "@storybook/react";
import { StatusIndicator } from "./StatusIndicator";
import { StatusIndicator, StatusIndicatorDot } from "./StatusIndicator";
const meta: Meta<typeof StatusIndicator> = {
title: "components/StatusIndicator",
component: StatusIndicator,
args: {},
args: {
children: (
<>
<StatusIndicatorDot />
Status
</>
),
},
};
export default meta;
@ -12,52 +19,37 @@ type Story = StoryObj<typeof StatusIndicator>;
export const Success: Story = {
args: {
color: "success",
variant: "success",
},
};
export const SuccessOutline: Story = {
export const Failed: Story = {
args: {
color: "success",
variant: "outlined",
},
};
export const Warning: Story = {
args: {
color: "warning",
},
};
export const WarningOutline: Story = {
args: {
color: "warning",
variant: "outlined",
},
};
export const Danger: Story = {
args: {
color: "danger",
},
};
export const DangerOutline: Story = {
args: {
color: "danger",
variant: "outlined",
variant: "failed",
},
};
export const Inactive: Story = {
args: {
color: "inactive",
variant: "inactive",
},
};
export const InactiveOutline: Story = {
export const Warning: Story = {
args: {
color: "inactive",
variant: "outlined",
variant: "warning",
},
};
export const Pending: Story = {
args: {
variant: "pending",
},
};
export const Small: Story = {
args: {
variant: "success",
size: "sm",
},
};

View File

@ -1,33 +1,97 @@
import { useTheme } from "@emotion/react";
import type { FC } from "react";
import type { ThemeRole } from "theme/roles";
import { type VariantProps, cva } from "class-variance-authority";
import { type FC, createContext, useContext } from "react";
import { cn } from "utils/cn";
interface StatusIndicatorProps {
color: ThemeRole;
variant?: "solid" | "outlined";
}
const statusIndicatorVariants = cva(
"font-medium inline-flex items-center gap-2",
{
variants: {
variant: {
success: "text-content-success",
failed: "text-content-destructive",
inactive: "text-highlight-grey",
warning: "text-content-warning",
pending: "text-highlight-sky",
},
size: {
sm: "text-xs",
md: "text-sm",
},
},
defaultVariants: {
variant: "success",
size: "md",
},
},
);
type StatusIndicatorContextValue = VariantProps<typeof statusIndicatorVariants>;
const StatusIndicatorContext = createContext<StatusIndicatorContextValue>({});
export interface StatusIndicatorProps
extends React.HTMLAttributes<HTMLDivElement>,
StatusIndicatorContextValue {}
export const StatusIndicator: FC<StatusIndicatorProps> = ({
color,
variant = "solid",
size,
variant,
className,
...props
}) => {
const theme = useTheme();
return (
<StatusIndicatorContext.Provider value={{ size, variant }}>
<div
className={cn(statusIndicatorVariants({ variant, size }), className)}
{...props}
/>
</StatusIndicatorContext.Provider>
);
};
const dotVariants = cva("rounded-full inline-block border-4 border-solid", {
variants: {
variant: {
success: "bg-content-success border-surface-green",
failed: "bg-content-destructive border-surface-destructive",
inactive: "bg-highlight-grey border-surface-grey",
warning: "bg-content-warning border-surface-orange",
pending: "bg-highlight-sky border-surface-sky",
},
size: {
sm: "size-3 border-4",
md: "size-4 border-4",
},
},
defaultVariants: {
variant: "success",
size: "md",
},
});
export interface StatusIndicatorDotProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof dotVariants> {}
export const StatusIndicatorDot: FC<StatusIndicatorDotProps> = ({
className,
// We allow the size and variant to be overridden directly by the component.
// This allows StatusIndicatorDot to be used alone.
size,
variant,
...props
}) => {
const { size: ctxSize, variant: ctxVariant } = useContext(
StatusIndicatorContext,
);
return (
<div
css={[
{
height: 8,
width: 8,
borderRadius: 4,
},
variant === "solid" && {
backgroundColor: theme.roles[color].fill.solid,
},
variant === "outlined" && {
border: `1px solid ${theme.roles[color].outline}`,
},
]}
className={cn(
dotVariants({ variant: variant ?? ctxVariant, size: size ?? ctxSize }),
className,
)}
{...props}
/>
);
};

View File

@ -15,8 +15,8 @@
--content-invert: 0 0% 98%;
--content-disabled: 240 5% 65%;
--content-success: 142 72% 29%;
--content-danger: 0 84% 60%;
--content-warning: 27 96% 61%;
--content-destructive: 0 84% 60%;
--surface-primary: 0 0% 98%;
--surface-secondary: 240 5% 96%;
--surface-tertiary: 240 6% 90%;
@ -24,6 +24,10 @@
--surface-invert-primary: 240 4% 16%;
--surface-invert-secondary: 240 5% 26%;
--surface-destructive: 0 93% 94%;
--surface-green: 141 79% 85%;
--surface-grey: 240 5% 96%;
--surface-orange: 34 100% 92%;
--surface-sky: 201 94% 86%;
--border-default: 240 6% 90%;
--border-success: 142 76% 36%;
--border-destructive: 0 84% 60%;
@ -31,6 +35,8 @@
--radius: 0.5rem;
--highlight-purple: 262 83% 58%;
--highlight-green: 143 64% 24%;
--highlight-grey: 240 5% 65%;
--highlight-sky: 201 90% 27%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
@ -45,8 +51,8 @@
--content-invert: 240 10% 4%;
--content-disabled: 240 5% 26%;
--content-success: 142 76% 36%;
--content-danger: 0 91% 71%;
--content-warning: 27 96% 61%;
--content-warning: 31 97% 72%;
--content-destructive: 0 91% 71%;
--surface-primary: 240 10% 4%;
--surface-secondary: 240 6% 10%;
--surface-tertiary: 240 4% 16%;
@ -54,12 +60,18 @@
--surface-invert-primary: 240 6% 90%;
--surface-invert-secondary: 240 5% 65%;
--surface-destructive: 0 75% 15%;
--surface-green: 145 80% 10%;
--surface-grey: 240 6% 10%;
--surface-orange: 13 81% 15%;
--surface-sky: 204 80% 16%;
--border-default: 240 4% 16%;
--border-success: 142 76% 36%;
--border-destructive: 0 91% 71%;
--overlay-default: 240 10% 4% / 80%;
--highlight-purple: 252 95% 85%;
--highlight-green: 141 79% 85%;
--highlight-grey: 240 4% 46%;
--highlight-sky: 198 93% 60%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;

View File

@ -16,7 +16,7 @@ import {
} from "components/HelpTooltip/HelpTooltip";
import { Pill } from "components/Pill/Pill";
import { Stack } from "components/Stack/Stack";
import { StatusIndicator } from "components/StatusIndicator/StatusIndicator";
import { StatusIndicatorDot } from "components/StatusIndicator/StatusIndicator";
import {
Popover,
PopoverContent,
@ -127,7 +127,7 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
}}
>
<div css={{ display: "flex", alignItems: "center", gap: 16 }}>
<StatusIndicator color={hasWarning ? "warning" : "success"} />
<StatusIndicatorDot variant={hasWarning ? "warning" : "success"} />
<div
css={{
display: "flex",

View File

@ -80,6 +80,7 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
</FormFields>
</FormSection>
<FormSection
data-chromatic="ignore"
title="Expiration"
description={
form.values.lifetime

View File

@ -12,12 +12,6 @@ const meta: Meta<typeof CreateTokenPage> = {
},
],
},
decorators: [
(Story) => {
Date.now = () => new Date("01/01/2014").getTime();
return <Story />;
},
],
};
export default meta;

View File

@ -7,7 +7,10 @@ import {
type UseFilterMenuOptions,
useFilterMenu,
} from "components/Filter/menu";
import { StatusIndicator } from "components/StatusIndicator/StatusIndicator";
import {
StatusIndicator,
StatusIndicatorDot,
} from "components/StatusIndicator/StatusIndicator";
import type { FC } from "react";
import { docs } from "utils/docs";
@ -24,17 +27,17 @@ export const useStatusFilterMenu = ({
{
value: "active",
label: "Active",
startIcon: <StatusIndicator color="success" />,
startIcon: <StatusIndicatorDot variant="success" />,
},
{
value: "dormant",
label: "Dormant",
startIcon: <StatusIndicator color="warning" />,
startIcon: <StatusIndicatorDot variant="warning" />,
},
{
value: "suspended",
label: "Suspended",
startIcon: <StatusIndicator color="inactive" />,
startIcon: <StatusIndicatorDot variant="inactive" />,
},
];
return useFilterMenu({

View File

@ -1,6 +1,6 @@
import { useTheme } from "@emotion/react";
import { Stack } from "components/Stack/Stack";
import { StatusIndicator } from "components/StatusIndicator/StatusIndicator";
import { StatusIndicatorDot } from "components/StatusIndicator/StatusIndicator";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { useTime } from "hooks/useTime";
@ -18,19 +18,19 @@ export const LastUsed: FC<LastUsedProps> = ({ lastUsedAt }) => {
const t = dayjs(lastUsedAt);
const now = dayjs();
let message = t.fromNow();
let circle = <StatusIndicator color="info" variant="outlined" />;
let circle = <StatusIndicatorDot variant="inactive" />;
if (t.isAfter(now.subtract(1, "hour"))) {
circle = <StatusIndicator color="success" />;
circle = <StatusIndicatorDot variant="success" />;
// Since the agent reports on a 10m interval,
// the last_used_at can be inaccurate when recent.
message = "Now";
} else if (t.isAfter(now.subtract(3, "day"))) {
circle = <StatusIndicator color="info" />;
circle = <StatusIndicatorDot variant="pending" />;
} else if (t.isAfter(now.subtract(1, "month"))) {
circle = <StatusIndicator color="warning" />;
circle = <StatusIndicatorDot variant="warning" />;
} else if (t.isAfter(now.subtract(100, "year"))) {
circle = <StatusIndicator color="error" />;
circle = <StatusIndicatorDot variant="failed" />;
} else {
message = "Never";
}

View File

@ -10,7 +10,11 @@ import {
type UseFilterMenuOptions,
useFilterMenu,
} from "components/Filter/menu";
import { StatusIndicator } from "components/StatusIndicator/StatusIndicator";
import {
StatusIndicator,
StatusIndicatorDot,
type StatusIndicatorDotProps,
} from "components/StatusIndicator/StatusIndicator";
import type { FC } from "react";
import { getDisplayWorkspaceStatus } from "utils/workspace";
@ -109,7 +113,9 @@ export const useStatusFilterMenu = ({
return {
label: display.text,
value: status,
startIcon: <StatusIndicator color={display.type ?? "warning"} />,
startIcon: (
<StatusIndicatorDot variant={getStatusIndicatorVariant(status)} />
),
};
});
return useFilterMenu({
@ -141,3 +147,26 @@ export const StatusMenu: FC<StatusMenuProps> = ({ width, menu }) => {
/>
);
};
export const getStatusIndicatorVariant = (
status: WorkspaceStatus,
): StatusIndicatorDotProps["variant"] => {
switch (status) {
case "running":
return "success";
case "starting":
case "pending":
return "pending";
case undefined:
case "canceling":
case "canceled":
case "stopping":
case "stopped":
return "inactive";
case "deleting":
case "deleted":
return "warning";
case "failed":
return "failed";
}
};

View File

@ -31,8 +31,8 @@ module.exports = {
disabled: "hsl(var(--content-disabled))",
invert: "hsl(var(--content-invert))",
success: "hsl(var(--content-success))",
danger: "hsl(var(--content-danger))",
link: "hsl(var(--content-link))",
destructive: "hsl(var(--content-destructive))",
warning: "hsl(var(--content-warning))",
},
surface: {
@ -45,6 +45,10 @@ module.exports = {
secondary: "hsl(var(--surface-invert-secondary))",
},
destructive: "hsl(var(--surface-destructive))",
green: "hsl(var(--surface-green))",
grey: "hsl(var(--surface-grey))",
orange: "hsl(var(--surface-orange))",
sky: "hsl(var(--surface-sky))",
},
border: {
DEFAULT: "hsl(var(--border-default))",
@ -56,6 +60,8 @@ module.exports = {
highlight: {
purple: "hsl(var(--highlight-purple))",
green: "hsl(var(--highlight-green))",
grey: "hsl(var(--highlight-grey))",
sky: "hsl(var(--highlight-sky))",
},
},
keyframes: {