diff --git a/site/src/pages/TaskPage/TaskPage.stories.tsx b/site/src/pages/TaskPage/TaskPage.stories.tsx index b247a2a16b377..27f0b60e3a16f 100644 --- a/site/src/pages/TaskPage/TaskPage.stories.tsx +++ b/site/src/pages/TaskPage/TaskPage.stories.tsx @@ -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( diff --git a/site/src/pages/TaskPage/TaskStartupWarningButton.stories.tsx b/site/src/pages/TaskPage/TaskStartupWarningButton.stories.tsx new file mode 100644 index 0000000000000..482fa396016d7 --- /dev/null +++ b/site/src/pages/TaskPage/TaskStartupWarningButton.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { TaskStartupWarningButton } from "./TaskStartupWarningButton"; + +const meta: Meta = { + title: "pages/TaskPage/TaskStartupWarningButton", + component: TaskStartupWarningButton, + parameters: { + layout: "padded", + }, +}; + +export default meta; +type Story = StoryObj; + +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, + }, +}; diff --git a/site/src/pages/TaskPage/TaskStartupWarningButton.tsx b/site/src/pages/TaskPage/TaskStartupWarningButton.tsx new file mode 100644 index 0000000000000..fab1b1a9b32e6 --- /dev/null +++ b/site/src/pages/TaskPage/TaskStartupWarningButton.tsx @@ -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 = ({ + lifecycleState, +}) => { + switch (lifecycleState) { + case "start_error": + return ; + case "start_timeout": + return ; + default: + return null; + } +}; + +type StartupWarningButtonBaseProps = { + label: string; + errorMessage: string; +}; + +const StartupWarningButtonBase: FC = ({ + label, + errorMessage, +}) => { + return ( + + + + + + +

+ A workspace{" "} + + {errorMessage} + + . We recommend{" "} + + debugging the startup script + {" "} + because{" "} + + your workspace may be incomplete + + . +

+
+
+
+ ); +}; + +const ErrorScriptButton: FC = () => { + return ( + + ); +}; + +const TimeoutScriptButton: FC = () => { + return ( + + ); +}; diff --git a/site/src/pages/TaskPage/TaskTopbar.tsx b/site/src/pages/TaskPage/TaskTopbar.tsx index 3d22631ae14b8..b5affcbfffe0b 100644 --- a/site/src/pages/TaskPage/TaskTopbar.tsx +++ b/site/src/pages/TaskPage/TaskTopbar.tsx @@ -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 = ({ task, workspace }) => { )}
+ +