mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
chore: refactor BuildIcon and remove useClassName (#25017)
This commit is contained in:
@@ -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} />;
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user