From 8a3a79f527169a18dda945f467ea2fb379ae1e46 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 7 Feb 2025 15:11:56 -0300 Subject: [PATCH] refactor: match StatusIndicator component with the new designs (#16458) Reference: https://www.figma.com/design/WfqIgsTFXN2BscBSSyXWF8/Coder-kit?node-id=489-4278&m=dev --- .../StatusIndicator.stories.tsx | 64 +++++----- .../StatusIndicator/StatusIndicator.tsx | 110 ++++++++++++++---- site/src/index.css | 18 ++- .../modules/provisioners/ProvisionerGroup.tsx | 4 +- .../pages/CreateTokenPage/CreateTokenForm.tsx | 1 + .../CreateTokenPage.stories.tsx | 6 - site/src/pages/UsersPage/UsersFilter.tsx | 11 +- site/src/pages/WorkspacesPage/LastUsed.tsx | 12 +- .../src/pages/WorkspacesPage/filter/menus.tsx | 33 +++++- site/tailwind.config.js | 8 +- 10 files changed, 184 insertions(+), 83 deletions(-) diff --git a/site/src/components/StatusIndicator/StatusIndicator.stories.tsx b/site/src/components/StatusIndicator/StatusIndicator.stories.tsx index f3da964dbd..f291089916 100644 --- a/site/src/components/StatusIndicator/StatusIndicator.stories.tsx +++ b/site/src/components/StatusIndicator/StatusIndicator.stories.tsx @@ -1,10 +1,17 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { StatusIndicator } from "./StatusIndicator"; +import { StatusIndicator, StatusIndicatorDot } from "./StatusIndicator"; const meta: Meta = { title: "components/StatusIndicator", component: StatusIndicator, - args: {}, + args: { + children: ( + <> + + Status + + ), + }, }; export default meta; @@ -12,52 +19,37 @@ type Story = StoryObj; 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", }, }; diff --git a/site/src/components/StatusIndicator/StatusIndicator.tsx b/site/src/components/StatusIndicator/StatusIndicator.tsx index f69efe0d2d..3ea9ca0dba 100644 --- a/site/src/components/StatusIndicator/StatusIndicator.tsx +++ b/site/src/components/StatusIndicator/StatusIndicator.tsx @@ -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; + +const StatusIndicatorContext = createContext({}); + +export interface StatusIndicatorProps + extends React.HTMLAttributes, + StatusIndicatorContextValue {} export const StatusIndicator: FC = ({ - color, - variant = "solid", + size, + variant, + className, + ...props }) => { - const theme = useTheme(); + return ( + +
+ + ); +}; + +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, + VariantProps {} + +export const StatusIndicatorDot: FC = ({ + 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 (
); }; diff --git a/site/src/index.css b/site/src/index.css index 29b4240939..a5806bdc98 100644 --- a/site/src/index.css +++ b/site/src/index.css @@ -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%; diff --git a/site/src/modules/provisioners/ProvisionerGroup.tsx b/site/src/modules/provisioners/ProvisionerGroup.tsx index 7bc652a531..017c8f9a2b 100644 --- a/site/src/modules/provisioners/ProvisionerGroup.tsx +++ b/site/src/modules/provisioners/ProvisionerGroup.tsx @@ -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 = ({ }} >
- +
= ({ = { }, ], }, - decorators: [ - (Story) => { - Date.now = () => new Date("01/01/2014").getTime(); - return ; - }, - ], }; export default meta; diff --git a/site/src/pages/UsersPage/UsersFilter.tsx b/site/src/pages/UsersPage/UsersFilter.tsx index 5f600670dc..2cf91023a0 100644 --- a/site/src/pages/UsersPage/UsersFilter.tsx +++ b/site/src/pages/UsersPage/UsersFilter.tsx @@ -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: , + startIcon: , }, { value: "dormant", label: "Dormant", - startIcon: , + startIcon: , }, { value: "suspended", label: "Suspended", - startIcon: , + startIcon: , }, ]; return useFilterMenu({ diff --git a/site/src/pages/WorkspacesPage/LastUsed.tsx b/site/src/pages/WorkspacesPage/LastUsed.tsx index 7de93efb00..a53ea5ed5b 100644 --- a/site/src/pages/WorkspacesPage/LastUsed.tsx +++ b/site/src/pages/WorkspacesPage/LastUsed.tsx @@ -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 = ({ lastUsedAt }) => { const t = dayjs(lastUsedAt); const now = dayjs(); let message = t.fromNow(); - let circle = ; + let circle = ; if (t.isAfter(now.subtract(1, "hour"))) { - circle = ; + circle = ; // 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 = ; + circle = ; } else if (t.isAfter(now.subtract(1, "month"))) { - circle = ; + circle = ; } else if (t.isAfter(now.subtract(100, "year"))) { - circle = ; + circle = ; } else { message = "Never"; } diff --git a/site/src/pages/WorkspacesPage/filter/menus.tsx b/site/src/pages/WorkspacesPage/filter/menus.tsx index 52e655914c..67892e4494 100644 --- a/site/src/pages/WorkspacesPage/filter/menus.tsx +++ b/site/src/pages/WorkspacesPage/filter/menus.tsx @@ -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: , + startIcon: ( + + ), }; }); return useFilterMenu({ @@ -141,3 +147,26 @@ export const StatusMenu: FC = ({ 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"; + } +}; diff --git a/site/tailwind.config.js b/site/tailwind.config.js index e47048a8b2..2ce6344943 100644 --- a/site/tailwind.config.js +++ b/site/tailwind.config.js @@ -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: {