feat(site): add startup script error alerts to Task Page (#20820)

Refactors Task page UI to show startup script errors as compact warning
buttons in the topbar.

Closes https://github.com/coder/coder/issues/20418
This commit is contained in:
Danielle Maywood
2025-11-19 11:44:22 +00:00
committed by GitHub
parent 8ee6e9457e
commit 8e22cd707a
4 changed files with 263 additions and 0 deletions
@@ -7,6 +7,7 @@ import {
MockTasks,
MockUserOwner,
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceAgentLogSource,
MockWorkspaceAgentReady,
MockWorkspaceAgentStarting,
@@ -218,6 +219,117 @@ export const WaitingStartupScripts: Story = {
},
};
export const StartupScriptError: Story = {
decorators: [withWebSocket],
parameters: {
queries: [
{
key: ["tasks", MockTask.owner_name, MockTask.id],
data: {
...MockTask,
workspace_agent_lifecycle: "start_error",
},
},
{
key: [
"workspace",
MockTask.owner_name,
MockTask.workspace_name,
"settings",
],
data: {
...MockWorkspace,
latest_build: {
...MockWorkspace.latest_build,
has_ai_task: true,
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgent],
},
],
},
},
},
],
webSocket: [
{
event: "message",
data: JSON.stringify(
[
"Cloning Git repository...",
"Starting application...",
"\x1b[91mError: Failed to connect to database",
"\x1b[91mStartup script exited with code 1",
].map((line, index) => ({
id: index,
level: index >= 2 ? "error" : "info",
output: line,
source_id: MockWorkspaceAgentLogSource.id,
created_at: new Date("2024-01-01T12:00:00Z").toISOString(),
})),
),
},
],
},
};
export const StartupScriptTimeout: Story = {
decorators: [withWebSocket],
parameters: {
queries: [
{
key: ["tasks", MockTask.owner_name, MockTask.id],
data: {
...MockTask,
workspace_agent_lifecycle: "start_timeout",
},
},
{
key: [
"workspace",
MockTask.owner_name,
MockTask.workspace_name,
"settings",
],
data: {
...MockWorkspace,
latest_build: {
...MockWorkspace.latest_build,
has_ai_task: true,
resources: [
{
...MockWorkspaceResource,
agents: [MockWorkspaceAgent],
},
],
},
},
},
],
webSocket: [
{
event: "message",
data: JSON.stringify(
[
"Cloning Git repository...",
"Starting application...",
"Waiting for dependencies...",
"Still waiting...",
"\x1b[93mWarning: Startup script exceeded timeout limit",
].map((line, index) => ({
id: index,
level: index === 4 ? "warn" : "info",
output: line,
source_id: MockWorkspaceAgentLogSource.id,
created_at: new Date("2024-01-01T12:00:00Z").toISOString(),
})),
),
},
],
},
};
export const SidebarAppNotFound: Story = {
beforeEach: () => {
const [task, workspace] = mockTaskWithWorkspace(
@@ -0,0 +1,37 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { TaskStartupWarningButton } from "./TaskStartupWarningButton";
const meta: Meta<typeof TaskStartupWarningButton> = {
title: "pages/TaskPage/TaskStartupWarningButton",
component: TaskStartupWarningButton,
parameters: {
layout: "padded",
},
};
export default meta;
type Story = StoryObj<typeof TaskStartupWarningButton>;
export const StartError: Story = {
args: {
lifecycleState: "start_error",
},
};
export const StartTimeout: Story = {
args: {
lifecycleState: "start_timeout",
},
};
export const NoWarning: Story = {
args: {
lifecycleState: "ready",
},
};
export const NullLifecycle: Story = {
args: {
lifecycleState: null,
},
};
@@ -0,0 +1,109 @@
import type { WorkspaceAgentLifecycle } from "api/typesGenerated";
import { Button } from "components/Button/Button";
import { Link } from "components/Link/Link";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { TriangleAlertIcon } from "lucide-react";
import type { FC } from "react";
import { docs } from "utils/docs";
type TaskStartupWarningButtonProps = {
lifecycleState?: WorkspaceAgentLifecycle | null;
};
export const TaskStartupWarningButton: FC<TaskStartupWarningButtonProps> = ({
lifecycleState,
}) => {
switch (lifecycleState) {
case "start_error":
return <ErrorScriptButton />;
case "start_timeout":
return <TimeoutScriptButton />;
default:
return null;
}
};
type StartupWarningButtonBaseProps = {
label: string;
errorMessage: string;
};
const StartupWarningButtonBase: FC<StartupWarningButtonBaseProps> = ({
label,
errorMessage,
}) => {
return (
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
className="border-amber-500 text-amber-600 dark:border-amber-600 dark:text-amber-400"
>
<TriangleAlertIcon />
{label}
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-sm bg-surface-secondary p-4">
<p className="m-0 text-sm font-normal text-content-primary leading-snug">
A workspace{" "}
<Link
href={docs(
"/admin/templates/troubleshooting#startup-script-exited-with-an-error",
)}
target="_blank"
rel="noreferrer"
>
{errorMessage}
</Link>
. We recommend{" "}
<Link
href={docs(
"/admin/templates/troubleshooting#startup-script-issues",
)}
target="_blank"
rel="noreferrer"
>
debugging the startup script
</Link>{" "}
because{" "}
<Link
href={docs(
"/admin/templates/troubleshooting#your-workspace-may-be-incomplete",
)}
target="_blank"
rel="noreferrer"
>
your workspace may be incomplete
</Link>
.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
const ErrorScriptButton: FC = () => {
return (
<StartupWarningButtonBase
label="Startup Error"
errorMessage="startup script has exited with an error"
/>
);
};
const TimeoutScriptButton: FC = () => {
return (
<StartupWarningButtonBase
label="Startup Timeout"
errorMessage="startup script has timed out"
/>
);
};
+5
View File
@@ -16,6 +16,7 @@ import {
} from "lucide-react";
import type { FC } from "react";
import { Link as RouterLink } from "react-router";
import { TaskStartupWarningButton } from "./TaskStartupWarningButton";
import { TaskStatusLink } from "./TaskStatusLink";
type TaskTopbarProps = { task: Task; workspace: Workspace };
@@ -46,6 +47,10 @@ export const TaskTopbar: FC<TaskTopbarProps> = ({ task, workspace }) => {
)}
<div className="ml-auto gap-2 flex items-center">
<TaskStartupWarningButton
lifecycleState={task.workspace_agent_lifecycle}
/>
<TooltipProvider delayDuration={250}>
<Tooltip>
<TooltipTrigger asChild>