mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
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:
@ -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",
|
||||
},
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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%;
|
||||
|
@ -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",
|
||||
|
@ -80,6 +80,7 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
data-chromatic="ignore"
|
||||
title="Expiration"
|
||||
description={
|
||||
form.values.lifetime
|
||||
|
@ -12,12 +12,6 @@ const meta: Meta<typeof CreateTokenPage> = {
|
||||
},
|
||||
],
|
||||
},
|
||||
decorators: [
|
||||
(Story) => {
|
||||
Date.now = () => new Date("01/01/2014").getTime();
|
||||
return <Story />;
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
@ -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({
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
};
|
||||
|
@ -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: {
|
||||
|
Reference in New Issue
Block a user