diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index 19fabffe3f..ea78e5d6ad 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -5,6 +5,7 @@ import { EllipsisIcon, PlayIcon, SquareCheckBigIcon, + TriangleAlertIcon, } from "lucide-react"; import { type FC, @@ -25,6 +26,7 @@ import type { } from "#/api/typesGenerated"; import { CheckIcon } from "#/components/AnimatedIcons/Check"; import { ChevronDownIcon } from "#/components/AnimatedIcons/ChevronDown"; +import { Badge } from "#/components/Badge/Badge"; import { Button } from "#/components/Button/Button"; import { DropdownMenu, @@ -45,6 +47,8 @@ import { useKebabMenu } from "#/components/Tabs/utils/useKebabMenu"; import { useProxy } from "#/contexts/ProxyContext"; import { useClipboard } from "#/hooks/useClipboard"; import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility"; +import { getAgentHealthIssues } from "#/modules/workspaces/health"; +import { AgentAlert } from "#/pages/WorkspacePage/AgentAlert"; import { AppStatuses } from "#/pages/WorkspacePage/AppStatuses"; import { cn } from "#/utils/cn"; import { AgentApps, organizeAgentApps } from "./AgentApps/AgentApps"; @@ -135,9 +139,12 @@ export const AgentRow: FC = ({ const showVSCode = hasVSCodeApp && !browser_only; const hasStartupFeatures = Boolean(agent.logs_length); + const healthIssues = getAgentHealthIssues(agent); + const hasAgentIssues = healthIssues.length > 0; const { proxy } = useProxy(); const [showLogs, setShowLogs] = useState( - ["starting", "start_timeout"].includes(agent.lifecycle_state) && + (["starting", "start_timeout"].includes(agent.lifecycle_state) || + hasAgentIssues) && hasStartupFeatures, ); const agentLogs = useAgentLogs({ agentId: agent.id, enabled: showLogs }); @@ -146,8 +153,11 @@ export const AgentRow: FC = ({ const [bottomOfLogs, setBottomOfLogs] = useState(true); useEffect(() => { - setShowLogs(agent.lifecycle_state !== "ready" && hasStartupFeatures); - }, [agent.lifecycle_state, hasStartupFeatures]); + setShowLogs( + (agent.lifecycle_state !== "ready" || hasAgentIssues) && + hasStartupFeatures, + ); + }, [agent.lifecycle_state, hasAgentIssues, hasStartupFeatures]); // This is a layout effect to remove flicker when we're scrolling to the bottom. // biome-ignore lint/correctness/useExhaustiveDependencies: consider refactoring @@ -208,7 +218,6 @@ export const AgentRow: FC = ({ agent, Boolean(hasDevcontainerErrors || shouldShowWildcardWarning), ); - const [selectedLogTab, setSelectedLogTab] = useState("all"); const sourceLogTabs = agent.log_sources .filter((logSource) => { @@ -459,20 +468,37 @@ export const AgentRow: FC = ({ - {hasStartupFeatures && ( -
-
- -
- -
+
+
+ +
+ +
+ {healthIssues.length > 0 && ( +
+ {healthIssues.map((issue) => ( + + ))} +
+ )} + {hasStartupFeatures && hasAnyLogs && (
= ({
-
-
-
- )} + )} +
+
+
); }; diff --git a/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx b/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx index 0885253833..186296c514 100644 --- a/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx +++ b/site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx @@ -11,7 +11,6 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; -import { getAgentHealthIssue } from "#/modules/workspaces/health"; import { type DisplayWorkspaceStatusType, getDisplayWorkspaceStatus, @@ -68,7 +67,10 @@ export const WorkspaceStatusIndicator: FC = ({ {children} - {getAgentHealthIssue(workspace).detail} + + One or more workspace agents need attention. Expand an agent's logs for + details. + ); }; diff --git a/site/src/modules/workspaces/health.test.ts b/site/src/modules/workspaces/health.test.ts index 357a6ccadb..8800cb8791 100644 --- a/site/src/modules/workspaces/health.test.ts +++ b/site/src/modules/workspaces/health.test.ts @@ -1,16 +1,11 @@ import { describe, expect, it } from "vitest"; import type { - Workspace, + WorkspaceAgent, WorkspaceAgentLifecycle, WorkspaceAgentStatus, } from "#/api/typesGenerated"; -import { - MockWorkspace, - MockWorkspaceAgent, - MockWorkspaceBuild, - MockWorkspaceResource, -} from "#/testHelpers/entities"; -import { getAgentHealthIssue } from "./health"; +import { MockWorkspaceAgent } from "#/testHelpers/entities"; +import { getAgentHealthIssues } from "./health"; interface AgentOverrides { status?: WorkspaceAgentStatus; @@ -18,375 +13,119 @@ interface AgentOverrides { parent_id?: string | null; } -/** - * Build a workspace mock with the given agent configurations and - * failing-agent count. Defaults to status "connected" and lifecycle - * "ready" so each test only needs to specify the fields it cares about. - */ -function buildWorkspace( - agents: AgentOverrides[], - failingAgentCount: number, -): Workspace { +function buildAgent(overrides: AgentOverrides): WorkspaceAgent { return { - ...MockWorkspace, - latest_build: { - ...MockWorkspaceBuild, - resources: [ - { - ...MockWorkspaceResource, - agents: agents.map((overrides, i) => ({ - ...MockWorkspaceAgent, - id: `agent-${i}`, - name: `agent-${i}`, - status: overrides.status ?? "connected", - lifecycle_state: overrides.lifecycle_state ?? "ready", - parent_id: overrides.parent_id ?? null, - })), - }, - ], - }, - health: { - healthy: failingAgentCount === 0, - failing_agents: Array.from( - { length: failingAgentCount }, - (_, i) => `agent-${i}`, - ), - }, + ...MockWorkspaceAgent, + status: overrides.status ?? "connected", + lifecycle_state: overrides.lifecycle_state ?? "ready", + parent_id: overrides.parent_id ?? null, }; } -describe("getAgentHealthIssue", () => { - describe("individual branches", () => { - it("returns disconnected issue for a disconnected agent", () => { - const ws = buildWorkspace( - [{ status: "disconnected", lifecycle_state: "ready" }], - 1, - ); - expect(getAgentHealthIssue(ws)).toEqual({ +describe("getAgentHealthIssues", () => { + it("returns disconnected issue for a disconnected agent", () => { + expect( + getAgentHealthIssues(buildAgent({ status: "disconnected" })), + ).toContainEqual( + expect.objectContaining({ title: "Workspace agent has disconnected", - detail: - "Check the log output for errors. If agents do not reconnect, try restarting the workspace.", severity: "warning", - prominent: true, - }); - }); + prominent: false, + }), + ); + }); - it("returns timeout issue for a timed-out agent", () => { - const ws = buildWorkspace( - [{ status: "timeout", lifecycle_state: "ready" }], - 1, - ); - expect(getAgentHealthIssue(ws)).toEqual({ + it("returns timeout issue for a timed-out agent", () => { + expect( + getAgentHealthIssues(buildAgent({ status: "timeout" })), + ).toContainEqual( + expect.objectContaining({ title: "Agent is taking longer than expected to connect", - detail: - "Continue to wait and check the log output for errors. If agents do not connect, try restarting the workspace.", severity: "warning", prominent: false, - }); - }); + }), + ); + }); - it("returns shutting down issue for shutting_down lifecycle", () => { - const ws = buildWorkspace( - [{ status: "connected", lifecycle_state: "shutting_down" }], - 1, - ); - expect(getAgentHealthIssue(ws)).toEqual({ + it("returns shutdown issue for shutdown lifecycle states", () => { + expect( + getAgentHealthIssues(buildAgent({ lifecycle_state: "shutting_down" })), + ).toContainEqual( + expect.objectContaining({ title: "Workspace agent is shutting down", - detail: "The workspace is not available while agents shut down.", severity: "info", - prominent: false, - }); - }); - - it("returns shutting down issue for shutdown_error lifecycle", () => { - const ws = buildWorkspace( - [{ status: "connected", lifecycle_state: "shutdown_error" }], - 1, - ); - expect(getAgentHealthIssue(ws)).toEqual({ + }), + ); + expect( + getAgentHealthIssues(buildAgent({ lifecycle_state: "shutdown_error" })), + ).toContainEqual( + expect.objectContaining({ title: "Workspace agent is shutting down", - detail: "The workspace is not available while agents shut down.", severity: "info", - prominent: false, - }); - }); - - it("returns shutting down issue for shutdown_timeout lifecycle", () => { - const ws = buildWorkspace( - [{ status: "connected", lifecycle_state: "shutdown_timeout" }], - 1, - ); - expect(getAgentHealthIssue(ws)).toEqual({ + }), + ); + expect( + getAgentHealthIssues(buildAgent({ lifecycle_state: "shutdown_timeout" })), + ).toContainEqual( + expect.objectContaining({ title: "Workspace agent is shutting down", - detail: "The workspace is not available while agents shut down.", severity: "info", - prominent: false, - }); - }); + }), + ); + }); - it("returns start error issue for start_error lifecycle", () => { - const ws = buildWorkspace( - [{ status: "connected", lifecycle_state: "start_error" }], - 1, - ); - expect(getAgentHealthIssue(ws)).toEqual({ + it("returns startup script issues", () => { + expect( + getAgentHealthIssues(buildAgent({ lifecycle_state: "start_error" })), + ).toContainEqual( + expect.objectContaining({ title: "Startup script failed", - detail: - "A startup script exited with an error. Check the agent logs for details.", - severity: "warning", - prominent: true, - }); - }); - - it("returns start timeout issue for start_timeout lifecycle", () => { - const ws = buildWorkspace( - [{ status: "connected", lifecycle_state: "start_timeout" }], - 1, - ); - expect(getAgentHealthIssue(ws)).toEqual({ - title: "Startup script is taking longer than expected", - detail: - "A startup script has exceeded the expected time. Check the agent logs for details.", severity: "warning", prominent: false, - }); - }); + }), + ); + expect( + getAgentHealthIssues(buildAgent({ lifecycle_state: "start_timeout" })), + ).toContainEqual( + expect.objectContaining({ + title: "Startup script is taking longer than expected", + severity: "warning", + prominent: false, + }), + ); + }); - it("returns connecting issue for a connecting agent", () => { - const ws = buildWorkspace( - [{ status: "connecting", lifecycle_state: "starting" }], - 1, - ); - expect(getAgentHealthIssue(ws)).toEqual({ + it("returns connecting issue for a connecting agent", () => { + expect( + getAgentHealthIssues( + buildAgent({ status: "connecting", lifecycle_state: "starting" }), + ), + ).toContainEqual( + expect.objectContaining({ title: "Workspace agent is connecting", - detail: - "The workspace agent has not connected yet. Wait for it to connect or check the logs if it does not.", severity: "info", prominent: false, - }); - }); + }), + ); }); - describe("plural path", () => { - it("uses plural title when multiple agents are disconnected", () => { - const ws = buildWorkspace( - [ - { status: "disconnected", lifecycle_state: "ready" }, - { status: "disconnected", lifecycle_state: "ready" }, - { status: "disconnected", lifecycle_state: "ready" }, - ], - 3, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe("3 workspace agents have disconnected"); - }); - - it("uses plural title when multiple agents time out", () => { - const ws = buildWorkspace( - [ - { status: "timeout", lifecycle_state: "ready" }, - { status: "timeout", lifecycle_state: "ready" }, - ], - 2, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe( - "2 agents are taking longer than expected to connect", - ); - }); - - it("uses plural title when multiple agents have start errors", () => { - const ws = buildWorkspace( - [ - { status: "connected", lifecycle_state: "start_error" }, - { status: "connected", lifecycle_state: "start_error" }, - ], - 2, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe("Startup scripts failed on 2 agents"); - }); - - it("uses plural title when multiple agents are shutting down", () => { - const ws = buildWorkspace( - [ - { status: "connected", lifecycle_state: "shutting_down" }, - { status: "connected", lifecycle_state: "shutdown_error" }, - ], - 2, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe("2 workspace agents are shutting down"); - }); - - it("uses plural title when multiple agents are connecting", () => { - const ws = buildWorkspace( - [ - { status: "connecting", lifecycle_state: "starting" }, - { status: "connecting", lifecycle_state: "starting" }, - ], - 2, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe("2 workspace agents are connecting"); - }); - - it("uses singular title when only one agent is failing", () => { - const ws = buildWorkspace( - [{ status: "disconnected", lifecycle_state: "ready" }], - 1, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe("Workspace agent has disconnected"); - }); + it("returns empty list for healthy ready connected agent", () => { + expect( + getAgentHealthIssues( + buildAgent({ status: "connected", lifecycle_state: "ready" }), + ), + ).toEqual([]); }); - describe("priority ordering", () => { - it("disconnected takes priority over timeout", () => { - const ws = buildWorkspace( - [ - { status: "disconnected", lifecycle_state: "ready" }, - { status: "timeout", lifecycle_state: "ready" }, - ], - 2, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe("2 workspace agents have disconnected"); - expect(result.severity).toBe("warning"); - expect(result.prominent).toBe(true); - }); - - it("timeout takes priority over shutdown states", () => { - const ws = buildWorkspace( - [ - { status: "timeout", lifecycle_state: "ready" }, - { status: "connected", lifecycle_state: "shutting_down" }, - ], - 2, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe( - "2 agents are taking longer than expected to connect", - ); - expect(result.severity).toBe("warning"); - expect(result.prominent).toBe(false); - }); - - it("shutdown states take priority over start_error", () => { - const ws = buildWorkspace( - [ - { status: "connected", lifecycle_state: "shutting_down" }, - { status: "connected", lifecycle_state: "start_error" }, - ], - 2, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe("2 workspace agents are shutting down"); - expect(result.severity).toBe("info"); - }); - - it("start_error takes priority over start_timeout", () => { - const ws = buildWorkspace( - [ - { status: "connected", lifecycle_state: "start_error" }, - { status: "connected", lifecycle_state: "start_timeout" }, - ], - 2, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe("Startup scripts failed on 2 agents"); - expect(result.severity).toBe("warning"); - expect(result.prominent).toBe(true); - }); - - it("disconnected takes priority over all lifecycle states", () => { - const ws = buildWorkspace( - [ - { status: "disconnected", lifecycle_state: "start_error" }, - { status: "connected", lifecycle_state: "shutting_down" }, - { status: "connected", lifecycle_state: "start_timeout" }, - ], - 3, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe("3 workspace agents have disconnected"); - }); - }); - - describe("sub-agent filtering", () => { - it("ignores a sub-agent whose status would change the result", () => { - const ws = buildWorkspace( - [ - // Parent agent: still connecting. - { status: "connecting", lifecycle_state: "starting" }, - // Sub-agent: disconnected, which would be highest priority - // if not filtered out. - { - status: "disconnected", - lifecycle_state: "ready", - parent_id: "agent-0", - }, - ], - 1, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe("Workspace agent is connecting"); - expect(result.severity).toBe("info"); - }); - - it("ignores a sub-agent whose lifecycle would promote severity", () => { - const ws = buildWorkspace( - [ - // Parent agent: soft start_timeout issue. - { status: "connected", lifecycle_state: "start_timeout" }, - // Sub-agent: start_error, which would take priority over - // start_timeout if not filtered. - { - status: "connected", - lifecycle_state: "start_error", - parent_id: "agent-0", - }, - ], - 1, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe( - "Startup script is taking longer than expected", - ); - expect(result.prominent).toBe(false); - }); - }); - - describe("start_timeout reachability", () => { - it("is overshadowed by start_error in a multi-agent workspace", () => { - const ws = buildWorkspace( - [ - { status: "connected", lifecycle_state: "start_timeout" }, - { status: "connected", lifecycle_state: "start_error" }, - ], - 2, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe("Startup scripts failed on 2 agents"); - expect(result.prominent).toBe(true); - }); - - it("is returned when it is the sole lifecycle issue", () => { - // In a multi-agent workspace another agent may have triggered - // the unhealthy flag while this agent only has start_timeout. - const ws = buildWorkspace( - [ - { status: "connected", lifecycle_state: "start_timeout" }, - { status: "connected", lifecycle_state: "ready" }, - ], - 1, - ); - const result = getAgentHealthIssue(ws); - expect(result.title).toBe( - "Startup script is taking longer than expected", - ); - expect(result.severity).toBe("warning"); - expect(result.prominent).toBe(false); - }); + it("returns multiple issues when multiple conditions match", () => { + const issues = getAgentHealthIssues( + buildAgent({ status: "disconnected", lifecycle_state: "start_error" }), + ); + expect(issues).toContainEqual( + expect.objectContaining({ title: "Workspace agent has disconnected" }), + ); + expect(issues).toContainEqual( + expect.objectContaining({ title: "Startup script failed" }), + ); }); }); diff --git a/site/src/modules/workspaces/health.ts b/site/src/modules/workspaces/health.ts index 88d8ff43b7..e1faa72597 100644 --- a/site/src/modules/workspaces/health.ts +++ b/site/src/modules/workspaces/health.ts @@ -1,10 +1,10 @@ -import type { Workspace, WorkspaceAgentStatus } from "#/api/typesGenerated"; +import type { WorkspaceAgent } from "#/api/typesGenerated"; /** * Canonical messages for startup and shutdown script issues. * Used by the per-agent-row tooltips in AgentStatus; the - * start-related entries are also shared with the workspace-level - * health classification in getAgentHealthIssue. + * start-related entries are also shared with per-agent health + * classification in getAgentHealthIssues. */ export const agentScriptMessages = { start_error: { @@ -62,121 +62,70 @@ interface AgentHealthIssue { } /** - * Classifies the health issue affecting a workspace based on agent - * status and lifecycle state. Returns a title and detail message - * that accurately describes the root cause rather than using a - * generic "unhealthy" label. + * Classifies all health issues for an individual agent. */ -export function getAgentHealthIssue(workspace: Workspace): AgentHealthIssue { - const failingAgentCount = workspace.health.failing_agents.length; - const statusSet = new Set(); - let hasStartError = false; - let hasStartTimeout = false; - let hasShutdownState = false; +export function getAgentHealthIssues( + agent: WorkspaceAgent, +): AgentHealthIssue[] { + const issues: AgentHealthIssue[] = []; - for (const resource of workspace.latest_build.resources) { - for (const agent of resource.agents ?? []) { - // Skip sub-agents (devcontainer agents) to match the - // backend health calculation which excludes them. - if (agent.parent_id !== null) { - continue; - } - statusSet.add(agent.status); - if (agent.lifecycle_state === "start_error") { - hasStartError = true; - } - if (agent.lifecycle_state === "start_timeout") { - hasStartTimeout = true; - } - if ( - agent.lifecycle_state === "shutting_down" || - agent.lifecycle_state === "shutdown_error" || - agent.lifecycle_state === "shutdown_timeout" - ) { - hasShutdownState = true; - } - } - } - - const plural = failingAgentCount > 1; - - if (statusSet.has("disconnected")) { - return { - title: plural - ? `${failingAgentCount} workspace agents have disconnected` - : agentConnectionMessages.disconnected.title, + if (agent.status === "disconnected") { + issues.push({ + title: agentConnectionMessages.disconnected.title, detail: agentConnectionMessages.disconnected.detail, severity: "warning", - prominent: true, - }; + prominent: false, + }); } - if (statusSet.has("timeout")) { - return { - title: plural - ? `${failingAgentCount} agents are taking longer than expected to connect` - : agentConnectionMessages.timeout.title, + if (agent.status === "timeout") { + issues.push({ + title: agentConnectionMessages.timeout.title, detail: agentConnectionMessages.timeout.detail, severity: "warning", prominent: false, - }; + }); } - if (hasShutdownState) { - return { - title: plural - ? `${failingAgentCount} workspace agents are shutting down` - : "Workspace agent is shutting down", + if ( + agent.lifecycle_state === "shutting_down" || + agent.lifecycle_state === "shutdown_error" || + agent.lifecycle_state === "shutdown_timeout" + ) { + issues.push({ + title: "Workspace agent is shutting down", detail: "The workspace is not available while agents shut down.", severity: "info", prominent: false, - }; + }); } - if (hasStartError) { - return { - title: plural - ? `Startup scripts failed on ${failingAgentCount} agents` - : agentScriptMessages.start_error.title, + if (agent.lifecycle_state === "start_error") { + issues.push({ + title: agentScriptMessages.start_error.title, detail: agentScriptMessages.start_error.detail, severity: "warning", - prominent: true, - }; + prominent: false, + }); } - // The backend does not mark start_timeout agents as unhealthy on - // their own (it treats it as a soft issue). This branch is only - // reachable in multi-agent workspaces where a different agent - // triggered the unhealthy flag but none of the higher-priority - // branches matched. - if (hasStartTimeout) { - return { - title: plural - ? `Startup scripts are taking longer than expected on ${failingAgentCount} agents` - : agentScriptMessages.start_timeout.title, + if (agent.lifecycle_state === "start_timeout") { + issues.push({ + title: agentScriptMessages.start_timeout.title, detail: agentScriptMessages.start_timeout.detail, severity: "warning", prominent: false, - }; + }); } - if (statusSet.has("connecting")) { - return { - title: plural - ? `${failingAgentCount} workspace agents are connecting` - : agentConnectionMessages.connecting.title, + if (agent.status === "connecting") { + issues.push({ + title: agentConnectionMessages.connecting.title, detail: agentConnectionMessages.connecting.detail, severity: "info", prominent: false, - }; + }); } - return { - title: plural - ? `${failingAgentCount} workspace agents are still connecting` - : "Workspace agent is still connecting", - detail: "Check the log output if the connection does not complete.", - severity: "info", - prominent: false, - }; + return issues; } diff --git a/site/src/pages/WorkspacePage/WorkspaceAlert.stories.tsx b/site/src/pages/WorkspacePage/AgentAlert.stories.tsx similarity index 79% rename from site/src/pages/WorkspacePage/WorkspaceAlert.stories.tsx rename to site/src/pages/WorkspacePage/AgentAlert.stories.tsx index 4ca68e98e6..464e7cb8ab 100644 --- a/site/src/pages/WorkspacePage/WorkspaceAlert.stories.tsx +++ b/site/src/pages/WorkspacePage/AgentAlert.stories.tsx @@ -1,9 +1,9 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { WorkspaceAlert } from "./WorkspaceAlert"; +import { AgentAlert } from "./AgentAlert"; -const meta: Meta = { - title: "pages/WorkspacePage/WorkspaceAlert", - component: WorkspaceAlert, +const meta: Meta = { + title: "pages/WorkspacePage/AgentAlert", + component: AgentAlert, args: { title: "Something went wrong", detail: @@ -13,7 +13,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const WarningProminent: Story = { args: { diff --git a/site/src/pages/WorkspacePage/WorkspaceAlert.tsx b/site/src/pages/WorkspacePage/AgentAlert.tsx similarity index 56% rename from site/src/pages/WorkspacePage/WorkspaceAlert.tsx rename to site/src/pages/WorkspacePage/AgentAlert.tsx index 0343309856..3d40843c02 100644 --- a/site/src/pages/WorkspacePage/WorkspaceAlert.tsx +++ b/site/src/pages/WorkspacePage/AgentAlert.tsx @@ -1,16 +1,16 @@ import type { FC } from "react"; import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert"; -import { Link } from "#/components/Link/Link"; +import { Button } from "#/components/Button/Button"; -interface WorkspaceAlertProps { +interface AgentAlertProps { title: string; detail: string; severity: "info" | "warning"; prominent: boolean; - troubleshootingURL: string | undefined; + troubleshootingURL?: string; } -export const WorkspaceAlert: FC = ({ +export const AgentAlert: FC = ({ title, detail, severity, @@ -21,14 +21,14 @@ export const WorkspaceAlert: FC = ({ {title} -

{detail}

-

- {troubleshootingURL && ( - +

{detail}
+ {troubleshootingURL && ( + + )}
); diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 8a3e6c8d55..f1ca8fdb66 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -7,21 +7,18 @@ import { SidebarIconButton } from "#/components/FullPageLayout/Sidebar"; import { useSearchParamsKey } from "#/hooks/useSearchParamsKey"; import { ProvisionerStatusAlert } from "#/modules/provisioners/ProvisionerStatusAlert"; import { AgentRow } from "#/modules/resources/AgentRow"; -import { getAgentHealthIssue } from "#/modules/workspaces/health"; import { WorkspaceTimings } from "#/modules/workspaces/WorkspaceTiming/WorkspaceTimings"; import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; 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, WorkspaceBuildProgress, } from "./WorkspaceBuildProgress"; import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner"; -import { findTroubleshootingURL } from "./WorkspaceNotifications/WorkspaceNotifications"; import { WorkspaceTopbar } from "./WorkspaceTopbar"; interface WorkspaceProps { @@ -99,7 +96,6 @@ export const Workspace: FC = ({ (workspace.latest_build.matched_provisioners?.available ?? 1) > 0; const shouldShowProvisionerAlert = workspacePending && !haveBuildLogs && !provisionersHealthy && !isRestarting; - const troubleshootingURL = findTroubleshootingURL(workspace.latest_build); return (
@@ -195,13 +191,6 @@ export const Workspace: FC = ({ )} - {!workspace.health.healthy && ( - - )} - {transitionStats !== undefined && ( expect(screen.getByRole("tooltip")).toHaveTextContent( - /startup script failed/i, + /one or more workspace agents need attention/i, ), ); }); @@ -177,7 +177,7 @@ export const AgentDisconnected: Story = { await userEvent.hover(screen.getByTestId("warning-notifications")); await waitFor(() => expect(screen.getByRole("tooltip")).toHaveTextContent( - /agent has disconnected/i, + /one or more workspace agents need attention/i, ), ); }); @@ -198,7 +198,7 @@ export const AgentTimeout: Story = { await userEvent.hover(screen.getByTestId("warning-notifications")); await waitFor(() => expect(screen.getByRole("tooltip")).toHaveTextContent( - /taking longer than expected/i, + /one or more workspace agents need attention/i, ), ); }); diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx index bc34eb6c97..019a656667 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx @@ -13,7 +13,6 @@ import type { import { MemoizedInlineMarkdown } from "#/components/Markdown/InlineMarkdown"; import { useDashboard } from "#/modules/dashboard/useDashboard"; import { TemplateUpdateMessage } from "#/modules/templates/TemplateUpdateMessage"; -import { getAgentHealthIssue } from "#/modules/workspaces/health"; dayjs.extend(relativeTime); @@ -93,12 +92,10 @@ export const WorkspaceNotifications: FC = ({ ) { const troubleshootingURL = findTroubleshootingURL(workspace.latest_build); const hasActions = permissions.updateWorkspace || troubleshootingURL; - const healthIssue = getAgentHealthIssue(workspace); - notifications.push({ - title: healthIssue.title, - severity: healthIssue.severity, - detail: healthIssue.detail, + title: "One or more workspace agents need attention", + severity: "warning", + detail: "Expand an agent's logs to view per-agent health details.", actions: hasActions ? ( <> {permissions.updateWorkspace && ( @@ -270,7 +267,7 @@ const styles = { }, } satisfies Record>; -export const findTroubleshootingURL = ( +const findTroubleshootingURL = ( workspaceBuild: WorkspaceBuild, ): string | undefined => { for (const resource of workspaceBuild.resources) {