Skip to content

Commit bef9e72

Browse files
refactor(site): Update agent status to include the lifecycle (#5835)
1 parent f65c7ca commit bef9e72

File tree

5 files changed

+182
-70
lines changed

5 files changed

+182
-70
lines changed

site/src/components/Resources/AgentRow.stories.tsx

+27
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import {
33
MockWorkspace,
44
MockWorkspaceAgent,
55
MockWorkspaceAgentConnecting,
6+
MockWorkspaceAgentStartError,
7+
MockWorkspaceAgentStarting,
8+
MockWorkspaceAgentStartTimeout,
69
MockWorkspaceAgentTimeout,
710
MockWorkspaceApp,
811
} from "testHelpers/entities"
@@ -86,6 +89,30 @@ Timeout.args = {
8689
showApps: true,
8790
}
8891

92+
export const Starting = Template.bind({})
93+
Starting.args = {
94+
agent: MockWorkspaceAgentStarting,
95+
workspace: MockWorkspace,
96+
applicationsHost: "",
97+
showApps: true,
98+
}
99+
100+
export const StartTimeout = Template.bind({})
101+
StartTimeout.args = {
102+
agent: MockWorkspaceAgentStartTimeout,
103+
workspace: MockWorkspace,
104+
applicationsHost: "",
105+
showApps: true,
106+
}
107+
108+
export const StartError = Template.bind({})
109+
StartError.args = {
110+
agent: MockWorkspaceAgentStartError,
111+
workspace: MockWorkspace,
112+
applicationsHost: "",
113+
showApps: true,
114+
}
115+
89116
export const ShowingPortForward = Template.bind({})
90117
ShowingPortForward.args = {
91118
agent: MockWorkspaceAgent,

site/src/components/Resources/AgentStatus.tsx

+138-3
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,146 @@ import {
1313
import { useRef, useState } from "react"
1414
import Link from "@material-ui/core/Link"
1515

16-
const ConnectedStatus: React.FC = () => {
16+
// If we think in the agent status and lifecycle into a single enum/state I’d
17+
// say we would have: connecting, timeout, disconnected, connected:created,
18+
// connected:starting, connected:start_timeout, connected:start_error,
19+
// connected:ready
20+
21+
const ReadyLifeCycle: React.FC = () => {
1722
const styles = useStyles()
1823
const { t } = useTranslation("workspacePage")
1924

2025
return (
2126
<div
2227
role="status"
23-
aria-label={t("agentStatus.connected")}
28+
aria-label={t("agentStatus.connected.ready")}
2429
className={combineClasses([styles.status, styles.connected])}
2530
/>
2631
)
2732
}
2833

34+
const StartingLifecycle: React.FC = () => {
35+
const styles = useStyles()
36+
const { t } = useTranslation("workspacePage")
37+
38+
return (
39+
<Tooltip title={t("agentStatus.connected.starting")}>
40+
<div
41+
role="status"
42+
aria-label={t("agentStatus.connected.starting")}
43+
className={combineClasses([styles.status, styles.connecting])}
44+
/>
45+
</Tooltip>
46+
)
47+
}
48+
49+
const StartTimeoutLifecycle: React.FC<{
50+
agent: WorkspaceAgent
51+
}> = ({ agent }) => {
52+
const { t } = useTranslation("agent")
53+
const styles = useStyles()
54+
const anchorRef = useRef<SVGSVGElement>(null)
55+
const [isOpen, setIsOpen] = useState(false)
56+
const id = isOpen ? "timeout-popover" : undefined
57+
58+
return (
59+
<>
60+
<WarningRounded
61+
ref={anchorRef}
62+
onMouseEnter={() => setIsOpen(true)}
63+
onMouseLeave={() => setIsOpen(false)}
64+
role="status"
65+
aria-label={t("status.startTimeout")}
66+
className={styles.timeoutWarning}
67+
/>
68+
<HelpPopover
69+
id={id}
70+
open={isOpen}
71+
anchorEl={anchorRef.current}
72+
onOpen={() => setIsOpen(true)}
73+
onClose={() => setIsOpen(false)}
74+
>
75+
<HelpTooltipTitle>{t("startTimeoutTooltip.title")}</HelpTooltipTitle>
76+
<HelpTooltipText>
77+
{t("startTimeoutTooltip.message")}{" "}
78+
<Link
79+
target="_blank"
80+
rel="noreferrer"
81+
href={agent.troubleshooting_url}
82+
>
83+
{t("startTimeoutTooltip.link")}
84+
</Link>
85+
.
86+
</HelpTooltipText>
87+
</HelpPopover>
88+
</>
89+
)
90+
}
91+
92+
const StartErrorLifecycle: React.FC<{
93+
agent: WorkspaceAgent
94+
}> = ({ agent }) => {
95+
const { t } = useTranslation("agent")
96+
const styles = useStyles()
97+
const anchorRef = useRef<SVGSVGElement>(null)
98+
const [isOpen, setIsOpen] = useState(false)
99+
const id = isOpen ? "timeout-popover" : undefined
100+
101+
return (
102+
<>
103+
<WarningRounded
104+
ref={anchorRef}
105+
onMouseEnter={() => setIsOpen(true)}
106+
onMouseLeave={() => setIsOpen(false)}
107+
role="status"
108+
aria-label={t("status.error")}
109+
className={styles.errorWarning}
110+
/>
111+
<HelpPopover
112+
id={id}
113+
open={isOpen}
114+
anchorEl={anchorRef.current}
115+
onOpen={() => setIsOpen(true)}
116+
onClose={() => setIsOpen(false)}
117+
>
118+
<HelpTooltipTitle>{t("startErrorTooltip.title")}</HelpTooltipTitle>
119+
<HelpTooltipText>
120+
{t("startErrorTooltip.message")}{" "}
121+
<Link
122+
target="_blank"
123+
rel="noreferrer"
124+
href={agent.troubleshooting_url}
125+
>
126+
{t("startErrorTooltip.link")}
127+
</Link>
128+
.
129+
</HelpTooltipText>
130+
</HelpPopover>
131+
</>
132+
)
133+
}
134+
135+
const ConnectedStatus: React.FC<{
136+
agent: WorkspaceAgent
137+
}> = ({ agent }) => {
138+
return (
139+
<ChooseOne>
140+
<Cond condition={agent.lifecycle_state === "ready"}>
141+
<ReadyLifeCycle />
142+
</Cond>
143+
<Cond condition={agent.lifecycle_state === "start_timeout"}>
144+
<StartTimeoutLifecycle agent={agent} />
145+
</Cond>
146+
<Cond condition={agent.lifecycle_state === "start_error"}>
147+
<StartErrorLifecycle agent={agent} />
148+
</Cond>
149+
<Cond>
150+
<StartingLifecycle />
151+
</Cond>
152+
</ChooseOne>
153+
)
154+
}
155+
29156
const DisconnectedStatus: React.FC = () => {
30157
const styles = useStyles()
31158
const { t } = useTranslation("workspacePage")
@@ -105,7 +232,7 @@ export const AgentStatus: React.FC<{
105232
return (
106233
<ChooseOne>
107234
<Cond condition={agent.status === "connected"}>
108-
<ConnectedStatus />
235+
<ConnectedStatus agent={agent} />
109236
</Cond>
110237
<Cond condition={agent.status === "disconnected"}>
111238
<DisconnectedStatus />
@@ -160,4 +287,12 @@ const useStyles = makeStyles((theme) => ({
160287
position: "relative",
161288
top: theme.spacing(1),
162289
},
290+
291+
errorWarning: {
292+
color: theme.palette.error.main,
293+
width: theme.spacing(2.5),
294+
height: theme.spacing(2.5),
295+
position: "relative",
296+
top: theme.spacing(1),
297+
},
163298
}))

site/src/i18n/en/agent.json

+13-1
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,24 @@
1212
"noApps": "None"
1313
},
1414
"status": {
15-
"timeout": "Timeout"
15+
"timeout": "Timeout",
16+
"startTimeout": "Start Timeout",
17+
"startError": "Error"
1618
},
1719
"timeoutTooltip": {
1820
"title": "Agent is taking too long to connect",
1921
"message": "We noticed this agent is taking longer than expected to connect.",
2022
"link": "Troubleshoot"
2123
},
24+
"startTimeoutTooltip": {
25+
"title": "Agent is taking too long to start",
26+
"message": "We noticed this agent is taking longer than expected to start.",
27+
"link": "Troubleshoot"
28+
},
29+
"startErrorTooltip": {
30+
"title": "Error starting agent",
31+
"message": "Something went wrong during the agent start.",
32+
"link": "Troubleshoot"
33+
},
2234
"unableToConnect": "Unable to connect"
2335
}

site/src/i18n/en/workspacePage.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@
3636
"pending": "Pending"
3737
},
3838
"agentStatus": {
39-
"connected": "Connected",
39+
"connected": {
40+
"ready": "Ready",
41+
"starting": "Starting..."
42+
},
4043
"connecting": "Connecting...",
4144
"disconnected": "Disconnected",
4245
"timeout": "Timeout"

site/src/pages/WorkspacePage/WorkspacePage.test.tsx

-65
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ import {
1818
MockStoppingWorkspace,
1919
MockTemplate,
2020
MockWorkspace,
21-
MockWorkspaceAgent,
22-
MockWorkspaceAgentConnecting,
23-
MockWorkspaceAgentDisconnected,
2421
MockWorkspaceBuild,
25-
MockWorkspaceResource2,
2622
renderWithAuth,
2723
waitForLoaderToBeRemoved,
2824
} from "../../testHelpers/renderHelpers"
@@ -284,65 +280,4 @@ describe("WorkspacePage", () => {
284280
})
285281
})
286282
})
287-
288-
describe("Resources", () => {
289-
it("shows the status of each agent in each resource", async () => {
290-
const getTemplateMock = jest
291-
.spyOn(api, "getTemplate")
292-
.mockResolvedValueOnce(MockTemplate)
293-
294-
const workspaceWithResources = {
295-
...MockWorkspace,
296-
latest_build: {
297-
...MockWorkspaceBuild,
298-
resources: [
299-
{
300-
...MockWorkspaceResource2,
301-
agents: [
302-
MockWorkspaceAgent,
303-
MockWorkspaceAgentDisconnected,
304-
MockWorkspaceAgentConnecting,
305-
],
306-
},
307-
],
308-
},
309-
}
310-
311-
server.use(
312-
rest.get(
313-
`/api/v2/users/:username/workspace/:workspaceName`,
314-
(req, res, ctx) => {
315-
return res(ctx.status(200), ctx.json(workspaceWithResources))
316-
},
317-
),
318-
)
319-
320-
await renderWorkspacePage()
321-
const agent1Names = await screen.findAllByText(MockWorkspaceAgent.name)
322-
expect(agent1Names.length).toEqual(1)
323-
const agent2Names = await screen.findAllByText(
324-
MockWorkspaceAgentDisconnected.name,
325-
)
326-
expect(agent2Names.length).toEqual(2)
327-
const agent1Status = await screen.findAllByLabelText(
328-
t<string>(`agentStatus.${MockWorkspaceAgent.status}`, {
329-
ns: "workspacePage",
330-
}),
331-
)
332-
expect(agent1Status.length).toEqual(1)
333-
const agentDisconnected = await screen.findAllByLabelText(
334-
t<string>(`agentStatus.${MockWorkspaceAgentDisconnected.status}`, {
335-
ns: "workspacePage",
336-
}),
337-
)
338-
expect(agentDisconnected.length).toEqual(1)
339-
const agentConnecting = await screen.findAllByLabelText(
340-
t<string>(`agentStatus.${MockWorkspaceAgentConnecting.status}`, {
341-
ns: "workspacePage",
342-
}),
343-
)
344-
expect(agentConnecting.length).toEqual(1)
345-
expect(getTemplateMock).toBeCalled()
346-
})
347-
})
348283
})

0 commit comments

Comments
 (0)