From 638e2220e92911f20286a51cb3224756469594af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kayla=20=E3=81=AF=E3=81=AA?= Date: Fri, 8 May 2026 14:11:49 -0600 Subject: [PATCH] chore: refactor `BuildIcon` and remove `useClassName` (#25017) --- .../BuildIcon/BuildIcon.stories.tsx | 28 ---- site/src/components/BuildIcon/BuildIcon.tsx | 20 --- site/src/hooks/useClassName.ts | 20 --- .../BuildAvatar/BuildAvatar.stories.tsx | 131 ------------------ .../builds/BuildAvatar/BuildAvatar.tsx | 30 ---- .../BuildIcon/BuildIcon.stories.tsx | 80 +++++++++++ .../workspaces/BuildIcon/BuildIcon.tsx | 59 ++++++++ .../WorkspaceBuildData/WorkspaceBuildData.tsx | 21 ++- .../WorkspaceBuildPageView.tsx | 8 +- site/src/utils/workspace.tsx | 56 -------- 10 files changed, 153 insertions(+), 300 deletions(-) delete mode 100644 site/src/components/BuildIcon/BuildIcon.stories.tsx delete mode 100644 site/src/components/BuildIcon/BuildIcon.tsx delete mode 100644 site/src/hooks/useClassName.ts delete mode 100644 site/src/modules/builds/BuildAvatar/BuildAvatar.stories.tsx delete mode 100644 site/src/modules/builds/BuildAvatar/BuildAvatar.tsx create mode 100644 site/src/modules/workspaces/BuildIcon/BuildIcon.stories.tsx create mode 100644 site/src/modules/workspaces/BuildIcon/BuildIcon.tsx diff --git a/site/src/components/BuildIcon/BuildIcon.stories.tsx b/site/src/components/BuildIcon/BuildIcon.stories.tsx deleted file mode 100644 index 22481719bb..0000000000 --- a/site/src/components/BuildIcon/BuildIcon.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { BuildIcon } from "./BuildIcon"; - -const meta: Meta = { - title: "components/BuildIcon", - component: BuildIcon, -}; - -export default meta; -type Story = StoryObj; - -export const Start: Story = { - args: { - transition: "start", - }, -}; - -export const Stop: Story = { - args: { - transition: "stop", - }, -}; - -export const Delete: Story = { - args: { - transition: "delete", - }, -}; diff --git a/site/src/components/BuildIcon/BuildIcon.tsx b/site/src/components/BuildIcon/BuildIcon.tsx deleted file mode 100644 index 8e72d8a1ab..0000000000 --- a/site/src/components/BuildIcon/BuildIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { PlayIcon, SquareIcon, TrashIcon } from "lucide-react"; -import type { ComponentProps } from "react"; -import type { WorkspaceTransition } from "#/api/typesGenerated"; - -type SVGIcon = typeof PlayIcon; - -type SVGIconProps = ComponentProps; - -const iconByTransition: Record = { - start: PlayIcon, - stop: SquareIcon, - delete: TrashIcon, -}; - -export const BuildIcon = ( - props: SVGIconProps & { transition: WorkspaceTransition }, -) => { - const Icon = iconByTransition[props.transition]; - return ; -}; diff --git a/site/src/hooks/useClassName.ts b/site/src/hooks/useClassName.ts deleted file mode 100644 index 80a86e965b..0000000000 --- a/site/src/hooks/useClassName.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { css } from "@emotion/css"; -import { type Theme, useTheme } from "@emotion/react"; -import { type DependencyList, useMemo } from "react"; - -type ClassName = (cssFn: typeof css, theme: Theme) => string; - -/** - * @deprecated This hook was used as an escape hatch to generate class names - * using emotion when no other styling method would work. There is no valid new - * usage of this hook. Use Tailwind classes instead. - */ -export function useClassName(styles: ClassName, deps: DependencyList): string { - const theme = useTheme(); - // biome-ignore lint/correctness/useExhaustiveDependencies: depends on deps - const className = useMemo(() => { - return styles(css, theme); - }, [...deps, theme]); - - return className; -} diff --git a/site/src/modules/builds/BuildAvatar/BuildAvatar.stories.tsx b/site/src/modules/builds/BuildAvatar/BuildAvatar.stories.tsx deleted file mode 100644 index 5e0d441a13..0000000000 --- a/site/src/modules/builds/BuildAvatar/BuildAvatar.stories.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { MockWorkspaceBuild } from "#/testHelpers/entities"; -import { BuildAvatar } from "./BuildAvatar"; - -const meta: Meta = { - title: "components/BuildAvatar", - component: BuildAvatar, - args: { - build: MockWorkspaceBuild, - }, -}; - -export default meta; -type Story = StoryObj; - -export const SmSize: Story = { - args: { - size: "sm", - }, -}; - -export const MdSize: Story = { - args: { - size: "md", - }, -}; - -export const LgSize: Story = { - args: { - size: "lg", - }, -}; - -export const Start: Story = { - args: { - build: { - ...MockWorkspaceBuild, - transition: "start", - }, - }, -}; - -export const Stop: Story = { - args: { - build: { - ...MockWorkspaceBuild, - transition: "stop", - }, - }, -}; - -export const Delete: Story = { - args: { - build: { - ...MockWorkspaceBuild, - transition: "delete", - }, - }, -}; - -export const Succeeded: Story = { - args: { - build: { - ...MockWorkspaceBuild, - job: { - ...MockWorkspaceBuild.job, - status: "succeeded", - }, - }, - }, -}; - -export const Pending: Story = { - args: { - build: { - ...MockWorkspaceBuild, - job: { - ...MockWorkspaceBuild.job, - status: "pending", - }, - }, - }, -}; - -export const Running: Story = { - args: { - build: { - ...MockWorkspaceBuild, - job: { - ...MockWorkspaceBuild.job, - status: "running", - }, - }, - }, -}; - -export const Failed: Story = { - args: { - build: { - ...MockWorkspaceBuild, - job: { - ...MockWorkspaceBuild.job, - status: "failed", - }, - }, - }, -}; - -export const Canceling: Story = { - args: { - build: { - ...MockWorkspaceBuild, - job: { - ...MockWorkspaceBuild.job, - status: "canceling", - }, - }, - }, -}; - -export const Canceled: Story = { - args: { - build: { - ...MockWorkspaceBuild, - job: { - ...MockWorkspaceBuild.job, - status: "canceled", - }, - }, - }, -}; diff --git a/site/src/modules/builds/BuildAvatar/BuildAvatar.tsx b/site/src/modules/builds/BuildAvatar/BuildAvatar.tsx deleted file mode 100644 index 64a681e7c2..0000000000 --- a/site/src/modules/builds/BuildAvatar/BuildAvatar.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useTheme } from "@emotion/react"; -import type { FC } from "react"; -import type { WorkspaceBuild } from "#/api/typesGenerated"; -import { Avatar, type AvatarProps } from "#/components/Avatar/Avatar"; -import { BuildIcon } from "#/components/BuildIcon/BuildIcon"; -import { useClassName } from "#/hooks/useClassName"; -import { getDisplayWorkspaceBuildStatus } from "#/utils/workspace"; - -interface BuildAvatarProps { - build: WorkspaceBuild; - size?: AvatarProps["size"]; -} - -export const BuildAvatar: FC = ({ build, size }) => { - const theme = useTheme(); - const { type } = getDisplayWorkspaceBuildStatus(theme, build); - const iconColor = useClassName( - (css, theme) => css({ color: theme.roles[type].fill.solid }), - [type], - ); - - return ( - - - - ); -}; diff --git a/site/src/modules/workspaces/BuildIcon/BuildIcon.stories.tsx b/site/src/modules/workspaces/BuildIcon/BuildIcon.stories.tsx new file mode 100644 index 0000000000..69a00a5100 --- /dev/null +++ b/site/src/modules/workspaces/BuildIcon/BuildIcon.stories.tsx @@ -0,0 +1,80 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { BuildIcon } from "./BuildIcon"; + +const meta: Meta = { + title: "modules/workspaces/BuildIcon", + component: BuildIcon, + args: { + jobStatus: "succeeded", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Start: Story = { + args: { + transition: "start", + }, +}; + +export const StartPending: Story = { + args: { + transition: "start", + jobStatus: "pending", + }, +}; + +export const StartRunning: Story = { + args: { + transition: "start", + jobStatus: "running", + }, +}; + +export const StartCanceling: Story = { + args: { + transition: "start", + jobStatus: "canceling", + }, +}; + +export const StartCanceled: Story = { + args: { + transition: "start", + jobStatus: "canceled", + }, +}; + +export const Stop: Story = { + args: { + transition: "stop", + }, +}; + +export const PendingStop: Story = { + args: { + transition: "stop", + jobStatus: "pending", + }, +}; + +export const UnknownStop: Story = { + args: { + transition: "stop", + jobStatus: "unknown", + }, +}; + +export const Delete: Story = { + args: { + transition: "delete", + }, +}; + +export const DeleteFailed: Story = { + args: { + transition: "delete", + jobStatus: "failed", + }, +}; diff --git a/site/src/modules/workspaces/BuildIcon/BuildIcon.tsx b/site/src/modules/workspaces/BuildIcon/BuildIcon.tsx new file mode 100644 index 0000000000..58754e718c --- /dev/null +++ b/site/src/modules/workspaces/BuildIcon/BuildIcon.tsx @@ -0,0 +1,59 @@ +import { + type LucideProps, + PlayIcon, + SquareIcon, + TrashIcon, +} from "lucide-react"; +import type { + ProvisionerJobStatus, + WorkspaceTransition, +} from "#/api/typesGenerated"; +import { Avatar } from "#/components/Avatar/Avatar"; +import { cn } from "#/utils/cn"; + +type BuildIconProps = LucideProps & { + transition: WorkspaceTransition; + jobStatus: ProvisionerJobStatus; + avatar?: boolean; +}; + +const iconByTransition: Record< + WorkspaceTransition, + React.ComponentType +> = { + start: PlayIcon, + stop: SquareIcon, + delete: TrashIcon, +}; + +const statusColors: Record = { + pending: "text-content-secondary", + running: "text-content-primary", + succeeded: "text-content-success", + + canceling: "text-content-warning", + canceled: "text-content-warning", + failed: "text-content-destructive", + unknown: "text-content-disabled", +}; + +export const BuildIcon: React.FC = ({ + transition, + jobStatus, + avatar, + className, + ...props +}) => { + const Icon = iconByTransition[transition]; + + return avatar ? ( + + + + ) : ( + + ); +}; diff --git a/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx b/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx index 196e276846..f8afc0048f 100644 --- a/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx +++ b/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx @@ -1,35 +1,30 @@ -import { useTheme } from "@emotion/react"; import { InfoIcon } from "lucide-react"; import type { WorkspaceBuild } from "#/api/typesGenerated"; -import { BuildIcon } from "#/components/BuildIcon/BuildIcon"; import { Skeleton } from "#/components/Skeleton/Skeleton"; import { Tooltip, TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; +import { BuildIcon } from "#/modules/workspaces/BuildIcon/BuildIcon"; import { cn } from "#/utils/cn"; import { createDayString } from "#/utils/createDayString"; import { buildReasonLabels, getDisplayWorkspaceBuildInitiatedBy, - getDisplayWorkspaceBuildStatus, systemBuildReasons, } from "#/utils/workspace"; -export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => { - const theme = useTheme(); - const statusType = getDisplayWorkspaceBuildStatus(theme, build).type; +type WorkspaceBuildDataProps = { + build: WorkspaceBuild; +}; +export const WorkspaceBuildData: React.FC = ({ + build, +}) => { return (
- +
= ({
- +
Build #{build.build_number} {build.initiator_name} diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index 00f3a5a2cb..61abebabef 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -1,4 +1,3 @@ -import type { Theme } from "@emotion/react"; import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import minMax from "dayjs/plugin/minMax"; @@ -18,65 +17,10 @@ dayjs.extend(duration); dayjs.extend(utc); dayjs.extend(minMax); -const DisplayWorkspaceBuildStatusLanguage = { - succeeded: "Succeeded", - pending: "Pending", - running: "Running", - canceling: "Canceling", - canceled: "Canceled", - failed: "Failed", -}; - const DisplayAgentVersionLanguage = { unknown: "Unknown", }; -export const getDisplayWorkspaceBuildStatus = ( - theme: Theme, - build: TypesGen.WorkspaceBuild, -) => { - switch (build.job.status) { - case "succeeded": - return { - type: "success", - color: theme.roles.success.text, - status: DisplayWorkspaceBuildStatusLanguage.succeeded, - } as const; - case "pending": - return { - type: "inactive", - color: theme.roles.active.text, - status: DisplayWorkspaceBuildStatusLanguage.pending, - } as const; - case "running": - return { - type: "active", - color: theme.roles.active.text, - status: DisplayWorkspaceBuildStatusLanguage.running, - } as const; - // Just handle unknown as failed - case "unknown": - case "failed": - return { - type: "error", - color: theme.roles.error.text, - status: DisplayWorkspaceBuildStatusLanguage.failed, - } as const; - case "canceling": - return { - type: "warning", - color: theme.roles.warning.text, - status: DisplayWorkspaceBuildStatusLanguage.canceling, - } as const; - case "canceled": - return { - type: "inactive", - color: theme.roles.warning.text, - status: DisplayWorkspaceBuildStatusLanguage.canceled, - } as const; - } -}; - export const getDisplayWorkspaceBuildInitiatedBy = ( build: TypesGen.WorkspaceBuild, ): string | undefined => {