Skip to content

Commit 75a4562

Browse files
committed
feat: add visual distinction for workspaces with running startup scripts
- Add helper function hasStartingAgents to check agent lifecycle states - Update getDisplayWorkspaceStatus to show 'Running (Starting...)' when agents are still starting - Enhance WorkspaceStatusIndicator with tooltips for starting workspaces - Add comprehensive tests for the new functionality
1 parent aa1a985 commit 75a4562

File tree

3 files changed

+173
-21
lines changed

3 files changed

+173
-21
lines changed

site/src/modules/workspaces/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,24 @@ export const WorkspaceStatusIndicator: FC<WorkspaceStatusIndicatorProps> = ({
4141
let { text, type } = getDisplayWorkspaceStatus(
4242
workspace.latest_build.status,
4343
workspace.latest_build.job,
44+
workspace,
4445
);
4546

4647
if (!workspace.health.healthy) {
4748
type = "warning";
4849
}
4950

51+
// Check if workspace is running but agents are still starting
52+
const isStarting =
53+
workspace.latest_build.status === "running" &&
54+
workspace.latest_build.resources.some((resource) =>
55+
resource.agents?.some(
56+
(agent) =>
57+
agent.lifecycle_state === "starting" ||
58+
agent.lifecycle_state === "created",
59+
),
60+
);
61+
5062
const statusIndicator = (
5163
<StatusIndicator variant={variantByStatusType[type]}>
5264
<StatusIndicatorDot />
@@ -55,24 +67,27 @@ export const WorkspaceStatusIndicator: FC<WorkspaceStatusIndicatorProps> = ({
5567
</StatusIndicator>
5668
);
5769

58-
if (workspace.health.healthy) {
59-
return statusIndicator;
70+
// Show tooltip for unhealthy or starting workspaces
71+
if (!workspace.health.healthy || isStarting) {
72+
const tooltipMessage = !workspace.health.healthy
73+
? "Your workspace is running but some agents are unhealthy."
74+
: "Your workspace is running but startup scripts are still executing.";
75+
76+
return (
77+
<TooltipProvider>
78+
<Tooltip>
79+
<TooltipTrigger asChild>
80+
<StatusIndicator variant={variantByStatusType[type]}>
81+
<StatusIndicatorDot />
82+
<span className="sr-only">Workspace status:</span> {text}
83+
{children}
84+
</StatusIndicator>
85+
</TooltipTrigger>
86+
<TooltipContent>{tooltipMessage}</TooltipContent>
87+
</Tooltip>
88+
</TooltipProvider>
89+
);
6090
}
6191

62-
return (
63-
<TooltipProvider>
64-
<Tooltip>
65-
<TooltipTrigger asChild>
66-
<StatusIndicator variant={variantByStatusType[type]}>
67-
<StatusIndicatorDot />
68-
<span className="sr-only">Workspace status:</span> {text}
69-
{children}
70-
</StatusIndicator>
71-
</TooltipTrigger>
72-
<TooltipContent>
73-
Your workspace is running but some agents are unhealthy.
74-
</TooltipContent>
75-
</Tooltip>
76-
</TooltipProvider>
77-
);
92+
return statusIndicator;
7893
};

site/src/utils/workspace.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
getDisplayVersionStatus,
88
getDisplayWorkspaceBuildInitiatedBy,
99
getDisplayWorkspaceTemplateName,
10+
getDisplayWorkspaceStatus,
11+
hasStartingAgents,
1012
isWorkspaceOn,
1113
} from "./workspace";
1214

@@ -157,4 +159,125 @@ describe("util > workspace", () => {
157159
expect(displayed).toEqual(workspace.template_display_name);
158160
});
159161
});
162+
163+
describe("hasStartingAgents", () => {
164+
it("returns true when agents are starting", () => {
165+
const workspace: TypesGen.Workspace = {
166+
...Mocks.MockWorkspace,
167+
latest_build: {
168+
...Mocks.MockWorkspaceBuild,
169+
resources: [
170+
{
171+
...Mocks.MockWorkspaceResource,
172+
agents: [
173+
{
174+
...Mocks.MockWorkspaceAgent,
175+
lifecycle_state: "starting",
176+
},
177+
],
178+
},
179+
],
180+
},
181+
};
182+
expect(hasStartingAgents(workspace)).toBe(true);
183+
});
184+
185+
it("returns true when agents are created", () => {
186+
const workspace: TypesGen.Workspace = {
187+
...Mocks.MockWorkspace,
188+
latest_build: {
189+
...Mocks.MockWorkspaceBuild,
190+
resources: [
191+
{
192+
...Mocks.MockWorkspaceResource,
193+
agents: [
194+
{
195+
...Mocks.MockWorkspaceAgent,
196+
lifecycle_state: "created",
197+
},
198+
],
199+
},
200+
],
201+
},
202+
};
203+
expect(hasStartingAgents(workspace)).toBe(true);
204+
});
205+
206+
it("returns false when all agents are ready", () => {
207+
const workspace: TypesGen.Workspace = {
208+
...Mocks.MockWorkspace,
209+
latest_build: {
210+
...Mocks.MockWorkspaceBuild,
211+
resources: [
212+
{
213+
...Mocks.MockWorkspaceResource,
214+
agents: [
215+
{
216+
...Mocks.MockWorkspaceAgent,
217+
lifecycle_state: "ready",
218+
},
219+
],
220+
},
221+
],
222+
},
223+
};
224+
expect(hasStartingAgents(workspace)).toBe(false);
225+
});
226+
});
227+
228+
describe("getDisplayWorkspaceStatus with starting agents", () => {
229+
it("shows 'Running (Starting...)' when workspace is running with starting agents", () => {
230+
const workspace: TypesGen.Workspace = {
231+
...Mocks.MockWorkspace,
232+
latest_build: {
233+
...Mocks.MockWorkspaceBuild,
234+
status: "running",
235+
resources: [
236+
{
237+
...Mocks.MockWorkspaceResource,
238+
agents: [
239+
{
240+
...Mocks.MockWorkspaceAgent,
241+
lifecycle_state: "starting",
242+
},
243+
],
244+
},
245+
],
246+
},
247+
};
248+
const status = getDisplayWorkspaceStatus("running", undefined, workspace);
249+
expect(status.text).toBe("Running (Starting...)");
250+
expect(status.type).toBe("active");
251+
});
252+
253+
it("shows 'Running' when workspace is running with all agents ready", () => {
254+
const workspace: TypesGen.Workspace = {
255+
...Mocks.MockWorkspace,
256+
latest_build: {
257+
...Mocks.MockWorkspaceBuild,
258+
status: "running",
259+
resources: [
260+
{
261+
...Mocks.MockWorkspaceResource,
262+
agents: [
263+
{
264+
...Mocks.MockWorkspaceAgent,
265+
lifecycle_state: "ready",
266+
},
267+
],
268+
},
269+
],
270+
},
271+
};
272+
const status = getDisplayWorkspaceStatus("running", undefined, workspace);
273+
expect(status.text).toBe("Running");
274+
expect(status.type).toBe("success");
275+
});
276+
277+
it("shows 'Running' when workspace parameter is not provided", () => {
278+
const status = getDisplayWorkspaceStatus("running", undefined);
279+
expect(status.text).toBe("Running");
280+
expect(status.type).toBe("success");
281+
});
282+
});
160283
});

site/src/utils/workspace.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,21 @@ type DisplayWorkspaceStatus = {
182182
icon: React.ReactNode;
183183
};
184184

185+
// Helper function to check if any agents are still starting
186+
export const hasStartingAgents = (workspace: TypesGen.Workspace): boolean => {
187+
return workspace.latest_build.resources.some((resource) =>
188+
resource.agents?.some(
189+
(agent) =>
190+
agent.lifecycle_state === "starting" ||
191+
agent.lifecycle_state === "created",
192+
),
193+
);
194+
};
195+
185196
export const getDisplayWorkspaceStatus = (
186197
workspaceStatus: TypesGen.WorkspaceStatus,
187198
provisionerJob?: TypesGen.ProvisionerJob,
199+
workspace?: TypesGen.Workspace,
188200
): DisplayWorkspaceStatus => {
189201
switch (workspaceStatus) {
190202
case undefined:
@@ -194,10 +206,12 @@ export const getDisplayWorkspaceStatus = (
194206
icon: <PillSpinner />,
195207
} as const;
196208
case "running":
209+
// Check if workspace has agents that are still starting
210+
const isStarting = workspace && hasStartingAgents(workspace);
197211
return {
198-
type: "success",
199-
text: "Running",
200-
icon: <PlayIcon />,
212+
type: isStarting ? "active" : "success",
213+
text: isStarting ? "Running (Starting...)" : "Running",
214+
icon: isStarting ? <PillSpinner /> : <PlayIcon />,
201215
} as const;
202216
case "starting":
203217
return {

0 commit comments

Comments
 (0)