fix(site): show accurate health messages in workspace hover menu and status tooltip (#23591)

This commit is contained in:
Atif Ali
2026-03-25 21:54:15 +05:00
committed by GitHub
parent 110af9e834
commit 6ce35b4af2
9 changed files with 658 additions and 177 deletions
@@ -1,4 +1,8 @@
import { MockWorkspace } from "testHelpers/entities";
import {
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceResource,
} from "testHelpers/entities";
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Workspace, WorkspaceStatus } from "api/typesGenerated";
import { WorkspaceStatusIndicator } from "./WorkspaceStatusIndicator";
@@ -33,7 +37,24 @@ export const Unhealthy: Story = {
...createWorkspaceWithStatus("running"),
health: {
healthy: false,
failing_agents: [],
failing_agents: [MockWorkspaceAgent.id],
},
latest_build: {
...MockWorkspace.latest_build,
status: "running",
resources: [
{
...MockWorkspaceResource,
agents: [
{
...MockWorkspaceAgent,
status: "connected",
lifecycle_state: "start_error",
health: { healthy: false },
},
],
},
],
},
},
},
@@ -1,4 +1,5 @@
import type { Workspace } from "api/typesGenerated";
import { getAgentHealthIssue } from "modules/workspaces/health";
import type React from "react";
import type { FC } from "react";
import {
@@ -67,9 +68,7 @@ export const WorkspaceStatusIndicator: FC<WorkspaceStatusIndicatorProps> = ({
{children}
</StatusIndicator>
</TooltipTrigger>
<TooltipContent>
Your workspace is running but some agents are unhealthy.
</TooltipContent>
<TooltipContent>{getAgentHealthIssue(workspace).detail}</TooltipContent>
</Tooltip>
);
};
+391
View File
@@ -0,0 +1,391 @@
import {
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceBuild,
MockWorkspaceResource,
} from "testHelpers/entities";
import type {
Workspace,
WorkspaceAgentLifecycle,
WorkspaceAgentStatus,
} from "api/typesGenerated";
import { describe, expect, it } from "vitest";
import { getAgentHealthIssue } from "./health";
interface AgentOverrides {
status?: WorkspaceAgentStatus;
lifecycle_state?: WorkspaceAgentLifecycle;
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 {
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}`,
),
},
};
}
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({
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,
});
});
it("returns timeout issue for a timed-out agent", () => {
const ws = buildWorkspace(
[{ status: "timeout", lifecycle_state: "ready" }],
1,
);
expect(getAgentHealthIssue(ws)).toEqual({
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({
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({
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({
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({
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,
});
});
it("returns connecting issue as default fallback", () => {
const ws = buildWorkspace(
[{ status: "connecting", lifecycle_state: "starting" }],
1,
);
expect(getAgentHealthIssue(ws)).toEqual({
title: "Workspace agent is still connecting",
detail: "Check the log output if the connection does not complete.",
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 still 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");
});
});
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 still 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);
});
});
});
+124
View File
@@ -0,0 +1,124 @@
import type { Workspace, WorkspaceAgentStatus } from "api/typesGenerated";
interface AgentHealthIssue {
title: string;
detail: string;
severity: "info" | "warning";
// Whether the alert should be visually prominent. Usually true for
// warnings, but connection timeout and startup timeout are
// exceptions (warning severity without prominent styling).
prominent: boolean;
}
/**
* 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.
*/
export function getAgentHealthIssue(workspace: Workspace): AgentHealthIssue {
const failingAgentCount = workspace.health.failing_agents.length;
const statusSet = new Set<WorkspaceAgentStatus>();
let hasStartError = false;
let hasStartTimeout = false;
let hasShutdownState = false;
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`
: "Workspace agent has disconnected",
detail:
"Check the log output for errors. If agents do not reconnect, try restarting the workspace.",
severity: "warning",
prominent: true,
};
}
if (statusSet.has("timeout")) {
return {
title: plural
? `${failingAgentCount} agents are taking longer than expected to connect`
: "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,
};
}
if (hasShutdownState) {
return {
title: plural
? `${failingAgentCount} workspace agents are shutting down`
: "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`
: "Startup script failed",
detail:
"A startup script exited with an error. Check the agent logs for details.",
severity: "warning",
prominent: true,
};
}
// 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`
: "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,
};
}
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,
};
}
+2 -1
View File
@@ -3,6 +3,7 @@ import { useSearchParamsKey } from "hooks/useSearchParamsKey";
import { BlocksIcon, HistoryIcon } from "lucide-react";
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 { FC } from "react";
import { useNavigate } from "react-router";
@@ -193,7 +194,7 @@ export const Workspace: FC<WorkspaceProps> = ({
{!workspace.health.healthy && (
<WorkspaceAlert
workspace={workspace}
{...getAgentHealthIssue(workspace)}
troubleshootingURL={troubleshootingURL}
/>
)}
@@ -1,106 +1,52 @@
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<WorkspaceAgent>,
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<typeof WorkspaceAlert> = {
title: "pages/WorkspacePage/WorkspaceAlert",
component: WorkspaceAlert,
args: {
title: "Something went wrong",
detail:
"A useful description of what happened and what the user can do about it.",
troubleshootingURL: "https://coder.com/docs/troubleshoot",
},
};
export default meta;
type Story = StoryObj<typeof WorkspaceAlert>;
export const Disconnected: Story = {
export const WarningProminent: Story = {
args: {
workspace: createUnhealthyWorkspace({ status: "disconnected" }),
troubleshootingURL: "https://coder.com/docs/troubleshoot",
severity: "warning",
prominent: true,
},
};
export const DisconnectedMultipleAgents: Story = {
export const WarningSubtle: Story = {
args: {
workspace: createUnhealthyWorkspace({ status: "disconnected" }, 3),
troubleshootingURL: "https://coder.com/docs/troubleshoot",
severity: "warning",
prominent: false,
},
};
export const TimeoutWarning: Story = {
export const InfoProminent: Story = {
args: {
workspace: createUnhealthyWorkspace({ status: "timeout" }),
troubleshootingURL: "https://coder.com/docs/troubleshoot",
severity: "info",
prominent: true,
},
};
export const StartupScriptFailed: Story = {
export const InfoSubtle: 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" }),
severity: "info",
prominent: false,
},
};
export const WithoutTroubleshootingURL: Story = {
args: {
workspace: createUnhealthyWorkspace({ status: "disconnected" }),
severity: "warning",
prominent: true,
troubleshootingURL: undefined,
},
};
@@ -1,88 +1,27 @@
import type * as TypesGen from "api/typesGenerated";
import type { WorkspaceAgentStatus } from "api/typesGenerated";
import type { FC } from "react";
import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert";
import { Link } from "#/components/Link/Link";
interface WorkspaceAlertProps {
workspace: TypesGen.Workspace;
title: string;
detail: string;
severity: "info" | "warning";
prominent: boolean;
troubleshootingURL: string | undefined;
}
export const WorkspaceAlert: FC<WorkspaceAlertProps> = ({
workspace,
title,
detail,
severity,
prominent,
troubleshootingURL,
}) => {
const failingAgentCount = workspace.health.failing_agents.length;
const statusSet = new Set<WorkspaceAgentStatus>();
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 (
<Alert severity={severity} prominent={prominent}>
<AlertTitle>{title}</AlertTitle>
<AlertDescription>
<p>{message}</p>
<p>{detail}</p>
<p>
{troubleshootingURL && (
<Link href={troubleshootingURL} target="_blank">
@@ -4,10 +4,13 @@ import {
MockTemplateVersion,
MockTemplateVersionWithMarkdownMessage,
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceResource,
} from "testHelpers/entities";
import { withDashboardProvider } from "testHelpers/storybook";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { getWorkspaceResolveAutostartQueryKey } from "api/queries/workspaceQuota";
import type { Workspace } from "api/typesGenerated";
import type { WorkspacePermissions } from "modules/workspaces/permissions";
import { expect, screen, userEvent, waitFor } from "storybook/test";
import { WorkspaceNotifications } from "./WorkspaceNotifications";
@@ -112,19 +115,40 @@ export const RequiresManualUpdate: Story = {
},
};
export const Unhealthy: Story = {
args: {
workspace: {
...MockWorkspace,
health: {
...MockWorkspace.health,
healthy: false,
},
latest_build: {
...MockWorkspace.latest_build,
status: "running",
},
/**
* Creates a workspace with unhealthy agents using the given agent
* overrides, for use in notification stories.
*/
function createUnhealthyWorkspace(
agentOverrides: Partial<typeof MockWorkspaceAgent>,
): Workspace {
const agent = { ...MockWorkspaceAgent, ...agentOverrides };
return {
...MockWorkspace,
health: {
healthy: false,
failing_agents: [agent.id],
},
latest_build: {
...MockWorkspace.latest_build,
status: "running",
resources: [
{
...MockWorkspaceResource,
agents: [agent],
},
],
},
};
}
export const StartupScriptFailed: Story = {
args: {
workspace: createUnhealthyWorkspace({
status: "connected",
lifecycle_state: "start_error",
health: { healthy: false },
}),
},
play: async ({ step }) => {
@@ -132,23 +156,65 @@ export const Unhealthy: Story = {
await userEvent.hover(screen.getByTestId("warning-notifications"));
await waitFor(() =>
expect(screen.getByRole("tooltip")).toHaveTextContent(
/workspace is unhealthy/i,
/startup script failed/i,
),
);
});
},
};
export const UnhealthyWithoutUpdatePermission: Story = {
export const AgentDisconnected: Story = {
args: {
...Unhealthy.args,
workspace: createUnhealthyWorkspace({
status: "disconnected",
lifecycle_state: "ready",
health: { healthy: false },
}),
},
play: async ({ step }) => {
await step("activate hover trigger", async () => {
await userEvent.hover(screen.getByTestId("warning-notifications"));
await waitFor(() =>
expect(screen.getByRole("tooltip")).toHaveTextContent(
/agent has disconnected/i,
),
);
});
},
};
export const AgentTimeout: Story = {
args: {
workspace: createUnhealthyWorkspace({
status: "timeout",
lifecycle_state: "starting",
health: { healthy: false },
}),
},
play: async ({ step }) => {
await step("activate hover trigger", async () => {
await userEvent.hover(screen.getByTestId("warning-notifications"));
await waitFor(() =>
expect(screen.getByRole("tooltip")).toHaveTextContent(
/taking longer than expected/i,
),
);
});
},
};
export const StartupScriptFailedWithoutUpdatePermission: Story = {
args: {
...StartupScriptFailed.args,
permissions: {
...defaultPermissions,
updateWorkspace: false,
},
},
play: Unhealthy.play,
play: StartupScriptFailed.play,
};
const DormantWorkspace = {
@@ -11,6 +11,7 @@ import relativeTime from "dayjs/plugin/relativeTime";
import { InfoIcon, TriangleAlertIcon } from "lucide-react";
import { useDashboard } from "modules/dashboard/useDashboard";
import { TemplateUpdateMessage } from "modules/templates/TemplateUpdateMessage";
import { getAgentHealthIssue } from "modules/workspaces/health";
import { type FC, useEffect, useState } from "react";
import { MemoizedInlineMarkdown } from "#/components/Markdown/Markdown";
@@ -92,19 +93,12 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = ({
) {
const troubleshootingURL = findTroubleshootingURL(workspace.latest_build);
const hasActions = permissions.updateWorkspace || troubleshootingURL;
const healthIssue = getAgentHealthIssue(workspace);
notifications.push({
title: "Workspace is unhealthy",
severity: "warning",
detail: (
<>
Your workspace is running but{" "}
{workspace.health.failing_agents.length > 1
? `${workspace.health.failing_agents.length} agents are unhealthy`
: "1 agent is unhealthy"}
.
</>
),
title: healthIssue.title,
severity: healthIssue.severity,
detail: healthIssue.detail,
actions: hasActions ? (
<>
{permissions.updateWorkspace && (