Skip to content

refactor: improve app status and statuses #18121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions site/src/modules/apps/AppStatusIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@ export const AppStatusIcon: FC<AppStatusIconProps> = ({
switch (status.state) {
case "complete":
return (
<CircleCheckIcon className={cn([className, "text-content-success"])} />
<CircleCheckIcon className={cn(["text-content-success", className])} />
);
case "failure":
return (
<CircleAlertIcon className={cn([className, "text-content-warning"])} />
<CircleAlertIcon className={cn(["text-content-warning", className])} />
);
case "working":
return latest ? (
<Spinner size="sm" className="shrink-0" loading />
) : (
<HourglassIcon className={cn([className, "text-highlight-sky"])} />
<HourglassIcon className={cn(["text-highlight-sky", className])} />
);
default:
return (
<TriangleAlertIcon
className={cn([className, "text-content-secondary"])}
className={cn(["text-content-secondary", className])}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,10 @@ export const LongMessage: Story = {
},
},
};

export const Disabled: Story = {
args: {
status: MockWorkspaceAppStatus,
disabled: true,
},
};
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
import type {
WorkspaceAppStatus as APIWorkspaceAppStatus,
WorkspaceAppStatusState,
} from "api/typesGenerated";
import { Spinner } from "components/Spinner/Spinner";
import type { WorkspaceAppStatus as APIWorkspaceAppStatus } from "api/typesGenerated";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { CircleAlertIcon, CircleCheckIcon } from "lucide-react";
import type { ReactNode } from "react";
import { AppStatusIcon } from "modules/apps/AppStatusIcon";
import { cn } from "utils/cn";

const iconByState: Record<WorkspaceAppStatusState, ReactNode> = {
complete: (
<CircleCheckIcon className="size-4 shrink-0 text-content-success" />
),
failure: <CircleAlertIcon className="size-4 shrink-0 text-content-warning" />,
working: <Spinner size="sm" className="shrink-0" loading />,
type WorkspaceAppStatusProps = {
status: APIWorkspaceAppStatus | null;
disabled?: boolean;
};

export const WorkspaceAppStatus = ({
status,
}: {
status: APIWorkspaceAppStatus | null;
}) => {
disabled,
}: WorkspaceAppStatusProps) => {
if (!status) {
return (
<span className="text-content-disabled text-sm">
Expand All @@ -39,7 +31,13 @@ export const WorkspaceAppStatus = ({
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2">
{iconByState[status.state]}
<AppStatusIcon
status={status}
latest
className={cn({
"text-content-disabled": disabled,
})}
/>
<span className="whitespace-nowrap max-w-72 overflow-hidden text-ellipsis text-sm text-content-primary font-medium">
{status.message}
</span>
Expand Down
212 changes: 62 additions & 150 deletions site/src/pages/WorkspacePage/AppStatuses.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { Meta, StoryObj } from "@storybook/react";
import type { WorkspaceAppStatus } from "api/typesGenerated";
import {
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceApp,
MockWorkspaceAppStatus,
MockWorkspaceAppStatuses,
createTimestamp,
} from "testHelpers/entities";
import { withProxyProvider } from "testHelpers/storybook";
import { AppStatuses } from "./AppStatuses";
Expand All @@ -13,6 +16,8 @@ const meta: Meta<typeof AppStatuses> = {
component: AppStatuses,
args: {
referenceDate: new Date("2024-03-26T15:15:00Z"),
agent: mockAgent(MockWorkspaceAppStatuses),
workspace: MockWorkspace,
},
decorators: [withProxyProvider()],
};
Expand All @@ -21,163 +26,70 @@ export default meta;

type Story = StoryObj<typeof AppStatuses>;

export const Default: Story = {
export const Default: Story = {};

// Add a story with a "Working" status as the latest
export const WorkingState: Story = {
args: {
workspace: MockWorkspace,
agent: {
...MockWorkspaceAgent,
apps: [
{
...MockWorkspaceApp,
statuses: [
{
// This is the latest status chronologically (15:04:38)
...MockWorkspaceAppStatus,
id: "status-7",
icon: "/emojis/1f4dd.png", // 📝
message: "Creating PR with gh CLI",
created_at: createTimestamp(4, 38), // 15:04:38
uri: "https://github.com/coder/coder/pull/5678",
state: "complete" as const,
},
{
// (15:03:56)
...MockWorkspaceAppStatus,
id: "status-6",
icon: "/emojis/1f680.png", // 🚀
message: "Pushing branch to remote",
created_at: createTimestamp(3, 56), // 15:03:56
uri: "",
state: "complete" as const,
},
{
// (15:02:29)
...MockWorkspaceAppStatus,
id: "status-5",
icon: "/emojis/1f527.png", // 🔧
message: "Configuring git identity",
created_at: createTimestamp(2, 29), // 15:02:29
uri: "",
state: "complete" as const,
},
{
// (15:02:04)
...MockWorkspaceAppStatus,
id: "status-4",
icon: "/emojis/1f4be.png", // 💾
message: "Committing changes",
created_at: createTimestamp(2, 4), // 15:02:04
uri: "",
state: "complete" as const,
},
{
// (15:01:44)
...MockWorkspaceAppStatus,
id: "status-3",
icon: "/emojis/2795.png", // +
message: "Adding files to staging",
created_at: createTimestamp(1, 44), // 15:01:44
uri: "",
state: "complete" as const,
},
{
// (15:01:32)
...MockWorkspaceAppStatus,
id: "status-2",
icon: "/emojis/1f33f.png", // 🌿
message: "Creating a new branch for PR",
created_at: createTimestamp(1, 32), // 15:01:32
uri: "",
state: "complete" as const,
},
{
// (15:01:00) - Oldest
...MockWorkspaceAppStatus,
id: "status-1",
icon: "/emojis/1f680.png", // 🚀
message: "Starting to create a PR",
created_at: createTimestamp(1, 0), // 15:01:00
uri: "",
state: "complete" as const,
},
].sort(
(a, b) =>
new Date(b.created_at).getTime() -
new Date(a.created_at).getTime(),
), // Ensure sorted correctly for component input if needed
},
],
},
agent: mockAgent([
{
// This is now the latest (15:05:15) and is "working"
...MockWorkspaceAppStatus,
id: "status-8",
icon: "", // Let the component handle the spinner icon
message: "Processing final checks...",
created_at: createTimestamp(5, 15), // 15:05:15 (after referenceDate)
uri: "",
state: "working" as const,
},
...MockWorkspaceAppStatuses,
]),
},
};

// Pass the reference date to the component for Storybook rendering
export const LongStatusText: Story = {
args: {
agent: mockAgent([
{
// This is now the latest (15:05:15) and is "working"
...MockWorkspaceAppStatus,
id: "status-8",
icon: "", // Let the component handle the spinner icon
message:
"Processing final checks with a very long message that exceeds the usual length to test how the component handles overflow and truncation in the UI. This should be long enough to ensure it wraps correctly and doesn't break the layout.",
created_at: createTimestamp(5, 15), // 15:05:15 (after referenceDate)
uri: "",
state: "complete" as const,
},
...MockWorkspaceAppStatuses,
]),
},
};

// Add a story with a "Working" status as the latest
export const WorkingState: Story = {
export const SingleStatus: Story = {
args: {
workspace: MockWorkspace,
agent: {
...MockWorkspaceAgent,
apps: [
{
...MockWorkspaceApp,
statuses: [
{
// This is now the latest (15:05:15) and is "working"
...MockWorkspaceAppStatus,
id: "status-8",
icon: "", // Let the component handle the spinner icon
message: "Processing final checks...",
created_at: createTimestamp(5, 15), // 15:05:15 (after referenceDate)
uri: "",
state: "working" as const,
},
{
// Previous latest (15:04:38)
...MockWorkspaceAppStatus,
id: "status-7",
icon: "/emojis/1f4dd.png", // 📝
message: "Creating PR with gh CLI",
created_at: createTimestamp(4, 38), // 15:04:38
uri: "https://github.com/coder/coder/pull/5678",
state: "complete" as const,
},
{
// (15:03:56)
...MockWorkspaceAppStatus,
id: "status-6",
icon: "/emojis/1f680.png", // 🚀
message: "Pushing branch to remote",
created_at: createTimestamp(3, 56), // 15:03:56
uri: "",
state: "complete" as const,
},
// ... include other older statuses if desired ...
{
// (15:01:00) - Oldest
...MockWorkspaceAppStatus,
id: "status-1",
icon: "/emojis/1f680.png", // 🚀
message: "Starting to create a PR",
created_at: createTimestamp(1, 0), // 15:01:00
uri: "",
state: "complete" as const,
},
].sort(
(a, b) =>
new Date(b.created_at).getTime() -
new Date(a.created_at).getTime(),
),
},
],
},
agent: mockAgent([
{
...MockWorkspaceAppStatus,
id: "status-1",
icon: "",
message: "Initial setup complete.",
created_at: createTimestamp(5, 10), // 15:05:10 (after referenceDate)
uri: "",
state: "complete" as const,
},
]),
},
};

function createTimestamp(minuteOffset: number, secondOffset: number) {
const baseDate = new Date("2024-03-26T15:00:00Z");
baseDate.setMinutes(baseDate.getMinutes() + minuteOffset);
baseDate.setSeconds(baseDate.getSeconds() + secondOffset);
return baseDate.toISOString();
function mockAgent(statuses: WorkspaceAppStatus[]) {
return {
...MockWorkspaceAgent,
apps: [
{
...MockWorkspaceApp,
statuses,
},
],
};
}
13 changes: 8 additions & 5 deletions site/src/pages/WorkspacePage/AppStatuses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,17 @@ export const AppStatuses: FC<AppStatusesProps> = ({
<div className="flex flex-col border border-solid border-border rounded-lg">
<div
className={`
flex items-center justify-between px-4 py-3
flex items-center justify-between px-4 py-3 gap-6
border-0 [&:not(:last-child)]:border-b border-solid border-border
`}
>
<div className="flex flex-col">
<span className="text-sm font-medium text-content-primary flex items-center gap-2">
<div className="flex flex-col overflow-hidden">
<div className="text-sm font-medium text-content-primary flex items-center gap-2 ">
<AppStatusIcon status={latestStatus} latest />
{latestStatus.message}
</span>
<span className="block flex-1 whitespace-nowrap overflow-hidden text-ellipsis">
{latestStatus.message}
</span>
</div>
<span className="text-xs text-content-secondary first-letter:uppercase block pl-[26px]">
{timeFrom(new Date(latestStatus.created_at), comparisonDate)}
</span>
Expand Down Expand Up @@ -154,6 +156,7 @@ export const AppStatuses: FC<AppStatusesProps> = ({
<Tooltip>
<TooltipTrigger asChild>
<Button
disabled={otherStatuses.length === 0}
size="icon"
variant="subtle"
onClick={() => {
Expand Down
5 changes: 4 additions & 1 deletion site/src/pages/WorkspacesPage/WorkspacesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,10 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({

{hasActivity && (
<TableCell>
<WorkspaceAppStatus status={workspace.latest_app_status} />
<WorkspaceAppStatus
status={workspace.latest_app_status}
disabled={workspace.latest_build.status !== "running"}
/>
</TableCell>
)}

Expand Down
Loading
Loading