mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
@@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user