Skip to content
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
112 changes: 112 additions & 0 deletions site/src/pages/TaskPage/TaskPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
MockTasks,
MockUserOwner,
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceAgentLogSource,
MockWorkspaceAgentReady,
MockWorkspaceAgentStarting,
Expand Down Expand Up @@ -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(
Expand Down
37 changes: 37 additions & 0 deletions site/src/pages/TaskPage/TaskStartupWarningButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -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,
},
};
109 changes: 109 additions & 0 deletions site/src/pages/TaskPage/TaskStartupWarningButton.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions site/src/pages/TaskPage/TaskTopbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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>
Expand Down
Loading