diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index e488c31365..883a5bdf49 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -1,8 +1,6 @@ import type * as TypesGen from "api/typesGenerated"; -import type { WorkspaceAgentStatus } from "api/typesGenerated"; import { Alert, AlertDescription, AlertTitle } from "components/Alert/Alert"; import { SidebarIconButton } from "components/FullPageLayout/Sidebar"; -import { Link } from "components/Link/Link"; import { useSearchParamsKey } from "hooks/useSearchParamsKey"; import { BlocksIcon, HistoryIcon } from "lucide-react"; import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert"; @@ -15,6 +13,7 @@ import { HistorySidebar } from "./HistorySidebar"; import { ResourceMetadata } from "./ResourceMetadata"; import { ResourcesSidebar } from "./ResourcesSidebar"; import { resourceOptionValue, useResourcesNav } from "./useResourcesNav"; +import { WorkspaceAlert } from "./WorkspaceAlert"; import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection"; import { getActiveTransitionStats, @@ -193,7 +192,7 @@ export const Workspace: FC = ({ )} {!workspace.health.healthy && ( - @@ -256,65 +255,6 @@ export const Workspace: FC = ({ ); }; -interface UnhealthyWorkspaceAlertProps { - workspace: TypesGen.Workspace; - troubleshootingURL: string | undefined; -} - -const UnhealthyWorkspaceAlert: FC = ({ - workspace, - troubleshootingURL, -}) => { - const failingAgentCount = workspace.health.failing_agents.length; - const failureSet = new Set(); - - workspace.latest_build.resources.forEach((resource) => { - resource.agents?.forEach((agent) => { - failureSet.add(agent.status); - }); - }); - - var title = "Workspace agents are not connected"; - var message = - "Your workspace cannot be used until an agent connects. Continue to wait and check the log output of your workspace for any errors."; - - // Disconnected is a more serious failure than timeout, so we can - // prioritize handling it first. - if (failureSet.has("disconnected")) { - title = "Workspace agents have disconnected"; - message = - "Continue to wait and check the log output of your workspace for any errors. If the agent does not reconnect, restarting the workspace can be used to try again."; - } else if (failureSet.has("timeout")) { - // Handle timeout case - title = "Your workspace is starting, but the agent has not yet connected."; - message = - "The agent is taking longer than expected to connect. Continue to wait and check the log output of your workspace for any errors. If the agent does not connect, restarting the workspace can be used to try again."; - } - - return ( - - {title} - -

- Your workspace is running but{" "} - {failingAgentCount > 1 - ? `${failingAgentCount} agents have not connected yet.` - : "the agent has not connected yet."} - .{" "} -

-

{message}

-

- {troubleshootingURL && ( - - View docs to troubleshoot - - )} -

-
-
- ); -}; - const countAgents = (resource: TypesGen.WorkspaceResource) => { return resource.agents ? resource.agents.length : 0; }; diff --git a/site/src/pages/WorkspacePage/WorkspaceAlert.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceAlert.stories.tsx new file mode 100644 index 0000000000..c3c78ae04c --- /dev/null +++ b/site/src/pages/WorkspacePage/WorkspaceAlert.stories.tsx @@ -0,0 +1,106 @@ +import { + MockWorkspace, + MockWorkspaceAgent, + MockWorkspaceResource, +} from "testHelpers/entities"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Workspace, WorkspaceAgent } from "api/typesGenerated"; +import { WorkspaceAlert } from "./WorkspaceAlert"; + +const createUnhealthyWorkspace = ( + agentOverrides: Partial, + agentCount = 1, +): Workspace => { + const agents = Array.from({ length: agentCount }, (_, i) => ({ + ...MockWorkspaceAgent, + id: `test-agent-${i}`, + name: `agent-${i}`, + health: { healthy: false }, + ...agentOverrides, + })); + return { + ...MockWorkspace, + health: { + healthy: false, + failing_agents: agents.map((a) => a.id), + }, + latest_build: { + ...MockWorkspace.latest_build, + resources: [{ ...MockWorkspaceResource, agents }], + }, + }; +}; + +const meta: Meta = { + title: "pages/WorkspacePage/WorkspaceAlert", + component: WorkspaceAlert, +}; + +export default meta; +type Story = StoryObj; + +export const Disconnected: Story = { + args: { + workspace: createUnhealthyWorkspace({ status: "disconnected" }), + troubleshootingURL: "https://coder.com/docs/troubleshoot", + }, +}; + +export const DisconnectedMultipleAgents: Story = { + args: { + workspace: createUnhealthyWorkspace({ status: "disconnected" }, 3), + troubleshootingURL: "https://coder.com/docs/troubleshoot", + }, +}; + +export const TimeoutWarning: Story = { + args: { + workspace: createUnhealthyWorkspace({ status: "timeout" }), + troubleshootingURL: "https://coder.com/docs/troubleshoot", + }, +}; + +export const StartupScriptFailed: Story = { + args: { + workspace: createUnhealthyWorkspace({ + status: "connected", + lifecycle_state: "start_error", + }), + troubleshootingURL: "https://coder.com/docs/troubleshoot", + }, +}; + +export const StartupScriptFailedMultipleAgents: Story = { + args: { + workspace: createUnhealthyWorkspace( + { + status: "connected", + lifecycle_state: "start_error", + }, + 2, + ), + troubleshootingURL: "https://coder.com/docs/troubleshoot", + }, +}; + +export const ShuttingDownInformational: Story = { + args: { + workspace: createUnhealthyWorkspace({ + status: "connected", + lifecycle_state: "shutting_down", + }), + }, +}; + +export const NotConnected: Story = { + args: { + workspace: createUnhealthyWorkspace({ status: "connecting" }), + }, +}; + +export const WithoutTroubleshootingURL: Story = { + args: { + workspace: createUnhealthyWorkspace({ status: "disconnected" }), + troubleshootingURL: undefined, + }, +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceAlert.tsx b/site/src/pages/WorkspacePage/WorkspaceAlert.tsx new file mode 100644 index 0000000000..4c7001df39 --- /dev/null +++ b/site/src/pages/WorkspacePage/WorkspaceAlert.tsx @@ -0,0 +1,96 @@ +import type * as TypesGen from "api/typesGenerated"; +import type { WorkspaceAgentStatus } from "api/typesGenerated"; +import { Alert, AlertDescription, AlertTitle } from "components/Alert/Alert"; +import { Link } from "components/Link/Link"; +import type { FC } from "react"; + +interface WorkspaceAlertProps { + workspace: TypesGen.Workspace; + troubleshootingURL: string | undefined; +} + +export const WorkspaceAlert: FC = ({ + workspace, + troubleshootingURL, +}) => { + const failingAgentCount = workspace.health.failing_agents.length; + const statusSet = new Set(); + let hasStartError = false; + let hasShuttingDown = false; + + for (const resource of workspace.latest_build.resources) { + for (const agent of resource.agents ?? []) { + statusSet.add(agent.status); + if (agent.lifecycle_state === "start_error") { + hasStartError = true; + } + if ( + agent.lifecycle_state === "shutting_down" || + agent.lifecycle_state === "shutdown_error" || + agent.lifecycle_state === "shutdown_timeout" + ) { + hasShuttingDown = true; + } + } + } + + const plural = failingAgentCount > 1; + + let title: string; + let message: string; + let severity: "info" | "warning" = "warning"; + let prominent = true; + + if (statusSet.has("disconnected")) { + title = plural + ? `${failingAgentCount} workspace agents have disconnected` + : "Workspace agent has disconnected"; + message = + "Check the log output for errors. If the agent does not reconnect, try restarting the workspace."; + } else if (statusSet.has("timeout")) { + title = plural + ? `${failingAgentCount} agents are taking longer than expected to connect` + : "Agent is taking longer than expected to connect"; + message = + "Continue to wait and check the log output for errors. If the agent does not connect, try restarting the workspace."; + severity = "warning"; + prominent = false; + } else if (hasShuttingDown) { + title = plural + ? `${failingAgentCount} workspace agents are shutting down` + : "Workspace agent is shutting down"; + message = "The workspace is not available while the agent shuts down."; + severity = "info"; + prominent = false; + } else if (hasStartError) { + title = plural + ? `Startup scripts failed on ${failingAgentCount} agents` + : "Startup script failed"; + message = + "The workspace is running but a startup script exited with an error. Check the agent logs for details."; + } else { + title = plural + ? `${failingAgentCount} workspace agents are still connecting` + : "Workspace agent is still connecting"; + message = + "The workspace agent is still connecting. Check the log output if the connection does not complete."; + severity = "info"; + prominent = false; + } + + return ( + + {title} + +

{message}

+

+ {troubleshootingURL && ( + + View docs to troubleshoot + + )} +

+
+
+ ); +};