Skip to content

Commit 8e22cd7

Browse files
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 #20418
1 parent 8ee6e94 commit 8e22cd7

File tree

4 files changed

+263
-0
lines changed

4 files changed

+263
-0
lines changed

site/src/pages/TaskPage/TaskPage.stories.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
MockTasks,
88
MockUserOwner,
99
MockWorkspace,
10+
MockWorkspaceAgent,
1011
MockWorkspaceAgentLogSource,
1112
MockWorkspaceAgentReady,
1213
MockWorkspaceAgentStarting,
@@ -218,6 +219,117 @@ export const WaitingStartupScripts: Story = {
218219
},
219220
};
220221

222+
export const StartupScriptError: Story = {
223+
decorators: [withWebSocket],
224+
parameters: {
225+
queries: [
226+
{
227+
key: ["tasks", MockTask.owner_name, MockTask.id],
228+
data: {
229+
...MockTask,
230+
workspace_agent_lifecycle: "start_error",
231+
},
232+
},
233+
{
234+
key: [
235+
"workspace",
236+
MockTask.owner_name,
237+
MockTask.workspace_name,
238+
"settings",
239+
],
240+
data: {
241+
...MockWorkspace,
242+
latest_build: {
243+
...MockWorkspace.latest_build,
244+
has_ai_task: true,
245+
resources: [
246+
{
247+
...MockWorkspaceResource,
248+
agents: [MockWorkspaceAgent],
249+
},
250+
],
251+
},
252+
},
253+
},
254+
],
255+
webSocket: [
256+
{
257+
event: "message",
258+
data: JSON.stringify(
259+
[
260+
"Cloning Git repository...",
261+
"Starting application...",
262+
"\x1b[91mError: Failed to connect to database",
263+
"\x1b[91mStartup script exited with code 1",
264+
].map((line, index) => ({
265+
id: index,
266+
level: index >= 2 ? "error" : "info",
267+
output: line,
268+
source_id: MockWorkspaceAgentLogSource.id,
269+
created_at: new Date("2024-01-01T12:00:00Z").toISOString(),
270+
})),
271+
),
272+
},
273+
],
274+
},
275+
};
276+
277+
export const StartupScriptTimeout: Story = {
278+
decorators: [withWebSocket],
279+
parameters: {
280+
queries: [
281+
{
282+
key: ["tasks", MockTask.owner_name, MockTask.id],
283+
data: {
284+
...MockTask,
285+
workspace_agent_lifecycle: "start_timeout",
286+
},
287+
},
288+
{
289+
key: [
290+
"workspace",
291+
MockTask.owner_name,
292+
MockTask.workspace_name,
293+
"settings",
294+
],
295+
data: {
296+
...MockWorkspace,
297+
latest_build: {
298+
...MockWorkspace.latest_build,
299+
has_ai_task: true,
300+
resources: [
301+
{
302+
...MockWorkspaceResource,
303+
agents: [MockWorkspaceAgent],
304+
},
305+
],
306+
},
307+
},
308+
},
309+
],
310+
webSocket: [
311+
{
312+
event: "message",
313+
data: JSON.stringify(
314+
[
315+
"Cloning Git repository...",
316+
"Starting application...",
317+
"Waiting for dependencies...",
318+
"Still waiting...",
319+
"\x1b[93mWarning: Startup script exceeded timeout limit",
320+
].map((line, index) => ({
321+
id: index,
322+
level: index === 4 ? "warn" : "info",
323+
output: line,
324+
source_id: MockWorkspaceAgentLogSource.id,
325+
created_at: new Date("2024-01-01T12:00:00Z").toISOString(),
326+
})),
327+
),
328+
},
329+
],
330+
},
331+
};
332+
221333
export const SidebarAppNotFound: Story = {
222334
beforeEach: () => {
223335
const [task, workspace] = mockTaskWithWorkspace(
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { Meta, StoryObj } from "@storybook/react-vite";
2+
import { TaskStartupWarningButton } from "./TaskStartupWarningButton";
3+
4+
const meta: Meta<typeof TaskStartupWarningButton> = {
5+
title: "pages/TaskPage/TaskStartupWarningButton",
6+
component: TaskStartupWarningButton,
7+
parameters: {
8+
layout: "padded",
9+
},
10+
};
11+
12+
export default meta;
13+
type Story = StoryObj<typeof TaskStartupWarningButton>;
14+
15+
export const StartError: Story = {
16+
args: {
17+
lifecycleState: "start_error",
18+
},
19+
};
20+
21+
export const StartTimeout: Story = {
22+
args: {
23+
lifecycleState: "start_timeout",
24+
},
25+
};
26+
27+
export const NoWarning: Story = {
28+
args: {
29+
lifecycleState: "ready",
30+
},
31+
};
32+
33+
export const NullLifecycle: Story = {
34+
args: {
35+
lifecycleState: null,
36+
},
37+
};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { WorkspaceAgentLifecycle } from "api/typesGenerated";
2+
import { Button } from "components/Button/Button";
3+
import { Link } from "components/Link/Link";
4+
import {
5+
Tooltip,
6+
TooltipContent,
7+
TooltipProvider,
8+
TooltipTrigger,
9+
} from "components/Tooltip/Tooltip";
10+
import { TriangleAlertIcon } from "lucide-react";
11+
import type { FC } from "react";
12+
import { docs } from "utils/docs";
13+
14+
type TaskStartupWarningButtonProps = {
15+
lifecycleState?: WorkspaceAgentLifecycle | null;
16+
};
17+
18+
export const TaskStartupWarningButton: FC<TaskStartupWarningButtonProps> = ({
19+
lifecycleState,
20+
}) => {
21+
switch (lifecycleState) {
22+
case "start_error":
23+
return <ErrorScriptButton />;
24+
case "start_timeout":
25+
return <TimeoutScriptButton />;
26+
default:
27+
return null;
28+
}
29+
};
30+
31+
type StartupWarningButtonBaseProps = {
32+
label: string;
33+
errorMessage: string;
34+
};
35+
36+
const StartupWarningButtonBase: FC<StartupWarningButtonBaseProps> = ({
37+
label,
38+
errorMessage,
39+
}) => {
40+
return (
41+
<TooltipProvider delayDuration={250}>
42+
<Tooltip>
43+
<TooltipTrigger asChild>
44+
<Button
45+
variant="outline"
46+
size="sm"
47+
className="border-amber-500 text-amber-600 dark:border-amber-600 dark:text-amber-400"
48+
>
49+
<TriangleAlertIcon />
50+
{label}
51+
</Button>
52+
</TooltipTrigger>
53+
<TooltipContent className="max-w-sm bg-surface-secondary p-4">
54+
<p className="m-0 text-sm font-normal text-content-primary leading-snug">
55+
A workspace{" "}
56+
<Link
57+
href={docs(
58+
"/admin/templates/troubleshooting#startup-script-exited-with-an-error",
59+
)}
60+
target="_blank"
61+
rel="noreferrer"
62+
>
63+
{errorMessage}
64+
</Link>
65+
. We recommend{" "}
66+
<Link
67+
href={docs(
68+
"/admin/templates/troubleshooting#startup-script-issues",
69+
)}
70+
target="_blank"
71+
rel="noreferrer"
72+
>
73+
debugging the startup script
74+
</Link>{" "}
75+
because{" "}
76+
<Link
77+
href={docs(
78+
"/admin/templates/troubleshooting#your-workspace-may-be-incomplete",
79+
)}
80+
target="_blank"
81+
rel="noreferrer"
82+
>
83+
your workspace may be incomplete
84+
</Link>
85+
.
86+
</p>
87+
</TooltipContent>
88+
</Tooltip>
89+
</TooltipProvider>
90+
);
91+
};
92+
93+
const ErrorScriptButton: FC = () => {
94+
return (
95+
<StartupWarningButtonBase
96+
label="Startup Error"
97+
errorMessage="startup script has exited with an error"
98+
/>
99+
);
100+
};
101+
102+
const TimeoutScriptButton: FC = () => {
103+
return (
104+
<StartupWarningButtonBase
105+
label="Startup Timeout"
106+
errorMessage="startup script has timed out"
107+
/>
108+
);
109+
};

site/src/pages/TaskPage/TaskTopbar.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from "lucide-react";
1717
import type { FC } from "react";
1818
import { Link as RouterLink } from "react-router";
19+
import { TaskStartupWarningButton } from "./TaskStartupWarningButton";
1920
import { TaskStatusLink } from "./TaskStatusLink";
2021

2122
type TaskTopbarProps = { task: Task; workspace: Workspace };
@@ -46,6 +47,10 @@ export const TaskTopbar: FC<TaskTopbarProps> = ({ task, workspace }) => {
4647
)}
4748

4849
<div className="ml-auto gap-2 flex items-center">
50+
<TaskStartupWarningButton
51+
lifecycleState={task.workspace_agent_lifecycle}
52+
/>
53+
4954
<TooltipProvider delayDuration={250}>
5055
<Tooltip>
5156
<TooltipTrigger asChild>

0 commit comments

Comments
 (0)