chore: refactor BuildIcon and remove useClassName (#25017)

This commit is contained in:
Kayla はな
2026-05-08 14:11:49 -06:00
committed by GitHub
parent 8d919e5411
commit 638e2220e9
10 changed files with 153 additions and 300 deletions
@@ -1,28 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { BuildIcon } from "./BuildIcon";
const meta: Meta<typeof BuildIcon> = {
title: "components/BuildIcon",
component: BuildIcon,
};
export default meta;
type Story = StoryObj<typeof BuildIcon>;
export const Start: Story = {
args: {
transition: "start",
},
};
export const Stop: Story = {
args: {
transition: "stop",
},
};
export const Delete: Story = {
args: {
transition: "delete",
},
};
@@ -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<SVGIcon>;
const iconByTransition: Record<WorkspaceTransition, SVGIcon> = {
start: PlayIcon,
stop: SquareIcon,
delete: TrashIcon,
};
export const BuildIcon = (
props: SVGIconProps & { transition: WorkspaceTransition },
) => {
const Icon = iconByTransition[props.transition];
return <Icon {...props} />;
};
-20
View File
@@ -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;
}
@@ -1,131 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { MockWorkspaceBuild } from "#/testHelpers/entities";
import { BuildAvatar } from "./BuildAvatar";
const meta: Meta<typeof BuildAvatar> = {
title: "components/BuildAvatar",
component: BuildAvatar,
args: {
build: MockWorkspaceBuild,
},
};
export default meta;
type Story = StoryObj<typeof BuildAvatar>;
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",
},
},
},
};
@@ -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<BuildAvatarProps> = ({ build, size }) => {
const theme = useTheme();
const { type } = getDisplayWorkspaceBuildStatus(theme, build);
const iconColor = useClassName(
(css, theme) => css({ color: theme.roles[type].fill.solid }),
[type],
);
return (
<Avatar size={size} variant="icon">
<BuildIcon
transition={build.transition}
className={`w-full h-full ${iconColor}`}
/>
</Avatar>
);
};
@@ -0,0 +1,80 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { BuildIcon } from "./BuildIcon";
const meta: Meta<typeof BuildIcon> = {
title: "modules/workspaces/BuildIcon",
component: BuildIcon,
args: {
jobStatus: "succeeded",
},
};
export default meta;
type Story = StoryObj<typeof BuildIcon>;
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",
},
};
@@ -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<LucideProps>
> = {
start: PlayIcon,
stop: SquareIcon,
delete: TrashIcon,
};
const statusColors: Record<ProvisionerJobStatus, string> = {
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<BuildIconProps> = ({
transition,
jobStatus,
avatar,
className,
...props
}) => {
const Icon = iconByTransition[transition];
return avatar ? (
<Avatar size="lg" variant="icon">
<Icon className={cn("size-full", statusColors[jobStatus], className)} />
</Avatar>
) : (
<Icon
className={cn("size-4", statusColors[jobStatus], className)}
{...props}
/>
);
};
@@ -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<WorkspaceBuildDataProps> = ({
build,
}) => {
return (
<div className="flex flex-row items-center gap-3 leading-normal">
<BuildIcon
transition={build.transition}
className="size-4"
css={{
color: theme.roles[statusType].fill.solid,
}}
/>
<BuildIcon transition={build.transition} jobStatus={build.job.status} />
<div className="overflow-hidden flex flex-col">
<div
className={cn(
@@ -30,10 +30,10 @@ import {
TabsList,
TabsTrigger,
} from "#/components/Tabs/Tabs";
import { BuildAvatar } from "#/modules/builds/BuildAvatar/BuildAvatar";
import { DashboardFullPage } from "#/modules/dashboard/DashboardLayout";
import { AgentLogs } from "#/modules/resources/AgentLogs/AgentLogs";
import { useAgentLogs } from "#/modules/resources/useAgentLogs";
import { BuildIcon } from "#/modules/workspaces/BuildIcon/BuildIcon";
import {
WorkspaceBuildData,
WorkspaceBuildDataSkeleton,
@@ -101,7 +101,11 @@ export const WorkspaceBuildPageView: FC<WorkspaceBuildPageViewProps> = ({
<DashboardFullPage>
<FullWidthPageHeader sticky={false}>
<div className="flex flex-row gap-4">
<BuildAvatar build={build} size="lg" />
<BuildIcon
avatar
transition={build.transition}
jobStatus={build.job.status}
/>
<div>
<PageHeaderTitle>Build #{build.build_number}</PageHeaderTitle>
<PageHeaderSubtitle>{build.initiator_name}</PageHeaderSubtitle>
-56
View File
@@ -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 => {