Skip to content

Commit e40c683

Browse files
authored
feat: resources card (#1627)
* Set up table * Format * Hook up api and test - bug assigning resources * Remove debugging code * Format * Remove unnecessary cards * Fix test * Fix assignment * Fix tests * Lint
1 parent c189fc5 commit e40c683

File tree

10 files changed

+250
-17
lines changed

10 files changed

+250
-17
lines changed

site/src/components/BuildsTable/BuildsTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const BuildsTable: React.FC<BuildsTableProps> = ({ builds, className }) =
3434
const styles = useStyles()
3535

3636
return (
37-
<Table className={className}>
37+
<Table className={className} data-testid="builds-table">
3838
<TableHead>
3939
<TableRow>
4040
<TableCell width="20%">{Language.actionLabel}</TableCell>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { makeStyles, Theme } from "@material-ui/core/styles"
2+
import Table from "@material-ui/core/Table"
3+
import TableBody from "@material-ui/core/TableBody"
4+
import TableCell from "@material-ui/core/TableCell"
5+
import TableHead from "@material-ui/core/TableHead"
6+
import TableRow from "@material-ui/core/TableRow"
7+
import useTheme from "@material-ui/styles/useTheme"
8+
import React from "react"
9+
import { WorkspaceResource } from "../../api/typesGenerated"
10+
import { getDisplayAgentStatus } from "../../util/workspace"
11+
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
12+
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
13+
14+
const Language = {
15+
resources: "Resources",
16+
resourceLabel: "Resource",
17+
agentsLabel: "Agents",
18+
agentLabel: "Agent",
19+
statusLabel: "Status",
20+
}
21+
22+
interface ResourcesProps {
23+
resources?: WorkspaceResource[]
24+
getResourcesError?: Error
25+
}
26+
27+
export const Resources: React.FC<ResourcesProps> = ({ resources, getResourcesError }) => {
28+
const styles = useStyles()
29+
const theme: Theme = useTheme()
30+
31+
return (
32+
<WorkspaceSection title={Language.resources} contentsProps={{ className: styles.sectionContents }}>
33+
{getResourcesError ? (
34+
{ getResourcesError }
35+
) : (
36+
<Table className={styles.table}>
37+
<TableHead>
38+
<TableHeaderRow>
39+
<TableCell>{Language.resourceLabel}</TableCell>
40+
<TableCell className={styles.agentColumn}>{Language.agentLabel}</TableCell>
41+
<TableCell>{Language.statusLabel}</TableCell>
42+
</TableHeaderRow>
43+
</TableHead>
44+
<TableBody>
45+
{resources?.map((resource) => {
46+
{
47+
/* We need to initialize the agents to display the resource */
48+
}
49+
const agents = resource.agents ?? [null]
50+
return agents.map((agent, agentIndex) => {
51+
{
52+
/* If there is no agent, just display the resource name */
53+
}
54+
if (!agent) {
55+
return (
56+
<TableRow>
57+
<TableCell className={styles.resourceNameCell}>{resource.name}</TableCell>
58+
<TableCell colSpan={2}></TableCell>
59+
</TableRow>
60+
)
61+
}
62+
63+
return (
64+
<TableRow key={`${resource.id}-${agent.id}`}>
65+
{/* We only want to display the name in the first row because we are using rowSpan */}
66+
{/* The rowspan should be the same than the number of agents */}
67+
{agentIndex === 0 && (
68+
<TableCell className={styles.resourceNameCell} rowSpan={agents.length}>
69+
{resource.name}
70+
</TableCell>
71+
)}
72+
73+
<TableCell className={styles.agentColumn}>
74+
<span style={{ color: theme.palette.text.secondary }}>{agent.name}</span>
75+
</TableCell>
76+
<TableCell>
77+
<span style={{ color: getDisplayAgentStatus(theme, agent).color }}>
78+
{getDisplayAgentStatus(theme, agent).status}
79+
</span>
80+
</TableCell>
81+
</TableRow>
82+
)
83+
})
84+
})}
85+
</TableBody>
86+
</Table>
87+
)}
88+
</WorkspaceSection>
89+
)
90+
}
91+
92+
const useStyles = makeStyles((theme) => ({
93+
sectionContents: {
94+
margin: 0,
95+
},
96+
97+
table: {
98+
border: 0,
99+
},
100+
101+
resourceNameCell: {
102+
borderRight: `1px solid ${theme.palette.divider}`,
103+
},
104+
105+
// Adds some left spacing
106+
agentColumn: {
107+
paddingLeft: `${theme.spacing(2)}px !important`,
108+
},
109+
}))

site/src/components/Workspace/Workspace.stories.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { action } from "@storybook/addon-actions"
22
import { Story } from "@storybook/react"
33
import React from "react"
4-
import { MockOutdatedWorkspace, MockWorkspace } from "../../testHelpers/renderHelpers"
4+
import {
5+
MockOutdatedWorkspace,
6+
MockWorkspace,
7+
MockWorkspaceResource,
8+
MockWorkspaceResource2,
9+
} from "../../testHelpers/renderHelpers"
510
import { Workspace, WorkspaceProps } from "./Workspace"
611

712
export default {
@@ -19,6 +24,7 @@ Started.args = {
1924
handleStop: action("stop"),
2025
handleRetry: action("retry"),
2126
workspaceStatus: "started",
27+
resources: [MockWorkspaceResource, MockWorkspaceResource2],
2228
}
2329

2430
export const Starting = Template.bind({})

site/src/components/Workspace/Workspace.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as TypesGen from "../../api/typesGenerated"
55
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
66
import { WorkspaceStatus } from "../../util/workspace"
77
import { BuildsTable } from "../BuildsTable/BuildsTable"
8+
import { Resources } from "../Resources/Resources"
89
import { Stack } from "../Stack/Stack"
910
import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
1011
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
@@ -17,6 +18,8 @@ export interface WorkspaceProps {
1718
handleUpdate: () => void
1819
workspace: TypesGen.Workspace
1920
workspaceStatus: WorkspaceStatus
21+
resources?: TypesGen.WorkspaceResource[]
22+
getResourcesError?: Error
2023
builds?: TypesGen.WorkspaceBuild[]
2124
}
2225

@@ -30,6 +33,8 @@ export const Workspace: React.FC<WorkspaceProps> = ({
3033
handleUpdate,
3134
workspace,
3235
workspaceStatus,
36+
resources,
37+
getResourcesError,
3338
builds,
3439
}) => {
3540
const styles = useStyles()
@@ -61,6 +66,7 @@ export const Workspace: React.FC<WorkspaceProps> = ({
6166

6267
<Stack spacing={3}>
6368
<WorkspaceStats workspace={workspace} />
69+
<Resources resources={resources} getResourcesError={getResourcesError} />
6470
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
6571
<BuildsTable builds={builds} className={styles.timelineTable} />
6672
</WorkspaceSection>

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

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ import {
1616
MockStoppingWorkspace,
1717
MockTemplate,
1818
MockWorkspace,
19+
MockWorkspaceAgent,
20+
MockWorkspaceAgentDisconnected,
1921
MockWorkspaceBuild,
2022
renderWithAuth,
2123
} from "../../testHelpers/renderHelpers"
2224
import { server } from "../../testHelpers/server"
23-
import { DisplayStatusLanguage } from "../../util/workspace"
25+
import { DisplayAgentStatusLanguage, DisplayStatusLanguage } from "../../util/workspace"
2426
import { WorkspacePage } from "./WorkspacePage"
2527

2628
// It renders the workspace page and waits for it be loaded
@@ -157,10 +159,27 @@ describe("Workspace Page", () => {
157159
it("shows the Deleted status when the workspace is deleted", async () => {
158160
await testStatus(MockDeletedWorkspace, DisplayStatusLanguage.deleted)
159161
})
160-
it("shows the timeline build", async () => {
161-
await renderWorkspacePage()
162-
const table = await screen.findByRole("table")
163-
const rows = table.querySelectorAll("tbody > tr")
164-
expect(rows).toHaveLength(MockBuilds.length)
162+
163+
describe("Timeline", () => {
164+
it("shows the timeline build", async () => {
165+
await renderWorkspacePage()
166+
const table = await screen.findByTestId("builds-table")
167+
const rows = table.querySelectorAll("tbody > tr")
168+
expect(rows).toHaveLength(MockBuilds.length)
169+
})
170+
})
171+
172+
describe("Resources", () => {
173+
it("shows the status of each agent in each resource", async () => {
174+
renderWithAuth(<WorkspacePage />, { route: `/workspaces/${MockWorkspace.id}`, path: "/workspaces/:workspace" })
175+
const agent1Names = await screen.findAllByText(MockWorkspaceAgent.name)
176+
expect(agent1Names.length).toEqual(2)
177+
const agent2Names = await screen.findAllByText(MockWorkspaceAgentDisconnected.name)
178+
expect(agent2Names.length).toEqual(2)
179+
const agent1Status = await screen.findAllByText(DisplayAgentStatusLanguage[MockWorkspaceAgent.status])
180+
expect(agent1Status.length).toEqual(2)
181+
const agent2Status = await screen.findAllByText(DisplayAgentStatusLanguage[MockWorkspaceAgentDisconnected.status])
182+
expect(agent2Status.length).toEqual(2)
183+
})
165184
})
166185
})

site/src/pages/WorkspacePage/WorkspacePage.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useActor, useSelector } from "@xstate/react"
1+
import { useActor } from "@xstate/react"
22
import React, { useContext, useEffect } from "react"
33
import { useParams } from "react-router-dom"
44
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
@@ -16,10 +16,8 @@ export const WorkspacePage: React.FC = () => {
1616

1717
const xServices = useContext(XServiceContext)
1818
const [workspaceState, workspaceSend] = useActor(xServices.workspaceXService)
19-
const { workspace, getWorkspaceError, getTemplateError, getOrganizationError, builds } = workspaceState.context
20-
const workspaceStatus = useSelector(xServices.workspaceXService, (state) => {
21-
return getWorkspaceStatus(state.context.workspace?.latest_build)
22-
})
19+
const { workspace, resources, getWorkspaceError, getResourcesError, builds } = workspaceState.context
20+
const workspaceStatus = getWorkspaceStatus(workspace?.latest_build)
2321

2422
/**
2523
* Get workspace, template, and organization on mount and whenever workspaceId changes.
@@ -30,7 +28,7 @@ export const WorkspacePage: React.FC = () => {
3028
}, [workspaceId, workspaceSend])
3129

3230
if (workspaceState.matches("error")) {
33-
return <ErrorSummary error={getWorkspaceError || getTemplateError || getOrganizationError} />
31+
return <ErrorSummary error={getWorkspaceError} />
3432
} else if (!workspace) {
3533
return <FullScreenLoader />
3634
} else {
@@ -44,6 +42,8 @@ export const WorkspacePage: React.FC = () => {
4442
handleRetry={() => workspaceSend("RETRY")}
4543
handleUpdate={() => workspaceSend("UPDATE")}
4644
workspaceStatus={workspaceStatus}
45+
resources={resources}
46+
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
4747
builds={builds}
4848
/>
4949
</Stack>

site/src/testHelpers/entities.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,15 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {
188188
updated_at: "",
189189
}
190190

191+
export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = {
192+
...MockWorkspaceAgent,
193+
id: "test-workspace-agent-2",
194+
name: "another-workspace-agent",
195+
status: "disconnected",
196+
}
197+
191198
export const MockWorkspaceResource: TypesGen.WorkspaceResource = {
192-
agents: [MockWorkspaceAgent],
199+
agents: [MockWorkspaceAgent, MockWorkspaceAgentDisconnected],
193200
created_at: "",
194201
id: "test-workspace-resource",
195202
job_id: "",
@@ -198,6 +205,12 @@ export const MockWorkspaceResource: TypesGen.WorkspaceResource = {
198205
workspace_transition: "start",
199206
}
200207

208+
export const MockWorkspaceResource2 = {
209+
...MockWorkspaceResource,
210+
id: "test-workspace-resource-2",
211+
name: "another-workspace-resource",
212+
}
213+
201214
export const MockUserAgent: Types.UserAgent = {
202215
browser: "Chrome 99.0.4844",
203216
device: "Other",

site/src/testHelpers/handlers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export const handlers = [
125125
return res(ctx.status(200), ctx.json(M.MockWorkspaceBuild))
126126
}),
127127
rest.get("/api/v2/workspacebuilds/:workspaceBuildId/resources", (req, res, ctx) => {
128-
return res(ctx.status(200), ctx.json([M.MockWorkspaceResource]))
128+
return res(ctx.status(200), ctx.json([M.MockWorkspaceResource, M.MockWorkspaceResource2]))
129129
}),
130130
rest.get("/api/v2/workspacebuilds/:workspaceBuildId/logs", (req, res, ctx) => {
131131
return res(ctx.status(200), ctx.json(M.MockWorkspaceBuildLogs))

site/src/util/workspace.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Theme } from "@material-ui/core/styles"
22
import dayjs from "dayjs"
33
import { WorkspaceBuildTransition } from "../api/types"
4-
import { WorkspaceBuild } from "../api/typesGenerated"
4+
import { WorkspaceAgent, WorkspaceBuild } from "../api/typesGenerated"
55

66
export type WorkspaceStatus =
77
| "queued"
@@ -148,3 +148,40 @@ export const displayWorkspaceBuildDuration = (build: WorkspaceBuild, inProgressL
148148
const duration = getWorkspaceBuildDurationInSeconds(build)
149149
return duration ? `${duration} seconds` : inProgressLabel
150150
}
151+
152+
export const DisplayAgentStatusLanguage = {
153+
connected: "⦿ Connected",
154+
connecting: "⦿ Connecting",
155+
disconnected: "◍ Disconnected",
156+
}
157+
158+
export const getDisplayAgentStatus = (
159+
theme: Theme,
160+
agent: WorkspaceAgent,
161+
): {
162+
color: string
163+
status: string
164+
} => {
165+
switch (agent.status) {
166+
case undefined:
167+
return {
168+
color: theme.palette.text.secondary,
169+
status: DisplayStatusLanguage.loading,
170+
}
171+
case "connected":
172+
return {
173+
color: theme.palette.success.main,
174+
status: DisplayAgentStatusLanguage["connected"],
175+
}
176+
case "connecting":
177+
return {
178+
color: theme.palette.success.main,
179+
status: DisplayAgentStatusLanguage["connecting"],
180+
}
181+
case "disconnected":
182+
return {
183+
color: theme.palette.text.secondary,
184+
status: DisplayAgentStatusLanguage["disconnected"],
185+
}
186+
}
187+
}

0 commit comments

Comments
 (0)