Skip to content

feat: resources card #1627

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/src/components/BuildsTable/BuildsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const BuildsTable: React.FC<BuildsTableProps> = ({ builds, className }) =
const styles = useStyles()

return (
<Table className={className}>
<Table className={className} data-testid="builds-table">
<TableHead>
<TableRow>
<TableCell width="20%">{Language.actionLabel}</TableCell>
Expand Down
109 changes: 109 additions & 0 deletions site/src/components/Resources/Resources.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { makeStyles, Theme } from "@material-ui/core/styles"
import Table from "@material-ui/core/Table"
import TableBody from "@material-ui/core/TableBody"
import TableCell from "@material-ui/core/TableCell"
import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import useTheme from "@material-ui/styles/useTheme"
import React from "react"
import { WorkspaceResource } from "../../api/typesGenerated"
import { getDisplayAgentStatus } from "../../util/workspace"
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"

const Language = {
resources: "Resources",
resourceLabel: "Resource",
agentsLabel: "Agents",
agentLabel: "Agent",
statusLabel: "Status",
}

interface ResourcesProps {
resources?: WorkspaceResource[]
getResourcesError?: Error
}

export const Resources: React.FC<ResourcesProps> = ({ resources, getResourcesError }) => {
const styles = useStyles()
const theme: Theme = useTheme()

return (
<WorkspaceSection title={Language.resources} contentsProps={{ className: styles.sectionContents }}>
{getResourcesError ? (
{ getResourcesError }
) : (
<Table className={styles.table}>
<TableHead>
<TableHeaderRow>
<TableCell>{Language.resourceLabel}</TableCell>
<TableCell className={styles.agentColumn}>{Language.agentLabel}</TableCell>
<TableCell>{Language.statusLabel}</TableCell>
</TableHeaderRow>
</TableHead>
<TableBody>
{resources?.map((resource) => {
{
/* We need to initialize the agents to display the resource */
}
const agents = resource.agents ?? [null]
return agents.map((agent, agentIndex) => {
{
/* If there is no agent, just display the resource name */
}
if (!agent) {
return (
<TableRow>
<TableCell className={styles.resourceNameCell}>{resource.name}</TableCell>
<TableCell colSpan={2}></TableCell>
</TableRow>
)
}

return (
<TableRow key={`${resource.id}-${agent.id}`}>
{/* We only want to display the name in the first row because we are using rowSpan */}
{/* The rowspan should be the same than the number of agents */}
{agentIndex === 0 && (
<TableCell className={styles.resourceNameCell} rowSpan={agents.length}>
{resource.name}
</TableCell>
)}

<TableCell className={styles.agentColumn}>
<span style={{ color: theme.palette.text.secondary }}>{agent.name}</span>
</TableCell>
<TableCell>
<span style={{ color: getDisplayAgentStatus(theme, agent).color }}>
{getDisplayAgentStatus(theme, agent).status}
</span>
</TableCell>
</TableRow>
)
})
})}
</TableBody>
</Table>
)}
</WorkspaceSection>
)
}

const useStyles = makeStyles((theme) => ({
sectionContents: {
margin: 0,
},

table: {
border: 0,
},

resourceNameCell: {
borderRight: `1px solid ${theme.palette.divider}`,
},

// Adds some left spacing
agentColumn: {
paddingLeft: `${theme.spacing(2)}px !important`,
},
}))
8 changes: 7 additions & 1 deletion site/src/components/Workspace/Workspace.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { action } from "@storybook/addon-actions"
import { Story } from "@storybook/react"
import React from "react"
import { MockOutdatedWorkspace, MockWorkspace } from "../../testHelpers/renderHelpers"
import {
MockOutdatedWorkspace,
MockWorkspace,
MockWorkspaceResource,
MockWorkspaceResource2,
} from "../../testHelpers/renderHelpers"
import { Workspace, WorkspaceProps } from "./Workspace"

export default {
Expand All @@ -19,6 +24,7 @@ Started.args = {
handleStop: action("stop"),
handleRetry: action("retry"),
workspaceStatus: "started",
resources: [MockWorkspaceResource, MockWorkspaceResource2],
}

export const Starting = Template.bind({})
Expand Down
6 changes: 6 additions & 0 deletions site/src/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as TypesGen from "../../api/typesGenerated"
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
import { WorkspaceStatus } from "../../util/workspace"
import { BuildsTable } from "../BuildsTable/BuildsTable"
import { Resources } from "../Resources/Resources"
import { Stack } from "../Stack/Stack"
import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
Expand All @@ -17,6 +18,8 @@ export interface WorkspaceProps {
handleUpdate: () => void
workspace: TypesGen.Workspace
workspaceStatus: WorkspaceStatus
resources?: TypesGen.WorkspaceResource[]
getResourcesError?: Error
builds?: TypesGen.WorkspaceBuild[]
}

Expand All @@ -30,6 +33,8 @@ export const Workspace: React.FC<WorkspaceProps> = ({
handleUpdate,
workspace,
workspaceStatus,
resources,
getResourcesError,
builds,
}) => {
const styles = useStyles()
Expand Down Expand Up @@ -61,6 +66,7 @@ export const Workspace: React.FC<WorkspaceProps> = ({

<Stack spacing={3}>
<WorkspaceStats workspace={workspace} />
<Resources resources={resources} getResourcesError={getResourcesError} />
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
<BuildsTable builds={builds} className={styles.timelineTable} />
</WorkspaceSection>
Expand Down
31 changes: 25 additions & 6 deletions site/src/pages/WorkspacePage/WorkspacePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import {
MockStoppingWorkspace,
MockTemplate,
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceAgentDisconnected,
MockWorkspaceBuild,
renderWithAuth,
} from "../../testHelpers/renderHelpers"
import { server } from "../../testHelpers/server"
import { DisplayStatusLanguage } from "../../util/workspace"
import { DisplayAgentStatusLanguage, DisplayStatusLanguage } from "../../util/workspace"
import { WorkspacePage } from "./WorkspacePage"

// It renders the workspace page and waits for it be loaded
Expand Down Expand Up @@ -157,10 +159,27 @@ describe("Workspace Page", () => {
it("shows the Deleted status when the workspace is deleted", async () => {
await testStatus(MockDeletedWorkspace, DisplayStatusLanguage.deleted)
})
it("shows the timeline build", async () => {
await renderWorkspacePage()
const table = await screen.findByRole("table")
const rows = table.querySelectorAll("tbody > tr")
expect(rows).toHaveLength(MockBuilds.length)

describe("Timeline", () => {
it("shows the timeline build", async () => {
await renderWorkspacePage()
const table = await screen.findByTestId("builds-table")
const rows = table.querySelectorAll("tbody > tr")
expect(rows).toHaveLength(MockBuilds.length)
})
})

describe("Resources", () => {
it("shows the status of each agent in each resource", async () => {
renderWithAuth(<WorkspacePage />, { route: `/workspaces/${MockWorkspace.id}`, path: "/workspaces/:workspace" })
const agent1Names = await screen.findAllByText(MockWorkspaceAgent.name)
expect(agent1Names.length).toEqual(2)
const agent2Names = await screen.findAllByText(MockWorkspaceAgentDisconnected.name)
expect(agent2Names.length).toEqual(2)
const agent1Status = await screen.findAllByText(DisplayAgentStatusLanguage[MockWorkspaceAgent.status])
expect(agent1Status.length).toEqual(2)
const agent2Status = await screen.findAllByText(DisplayAgentStatusLanguage[MockWorkspaceAgentDisconnected.status])
expect(agent2Status.length).toEqual(2)
})
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

})
12 changes: 6 additions & 6 deletions site/src/pages/WorkspacePage/WorkspacePage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useActor, useSelector } from "@xstate/react"
import { useActor } from "@xstate/react"
import React, { useContext, useEffect } from "react"
import { useParams } from "react-router-dom"
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
Expand All @@ -16,10 +16,8 @@ export const WorkspacePage: React.FC = () => {

const xServices = useContext(XServiceContext)
const [workspaceState, workspaceSend] = useActor(xServices.workspaceXService)
const { workspace, getWorkspaceError, getTemplateError, getOrganizationError, builds } = workspaceState.context
const workspaceStatus = useSelector(xServices.workspaceXService, (state) => {
return getWorkspaceStatus(state.context.workspace?.latest_build)
})
const { workspace, resources, getWorkspaceError, getResourcesError, builds } = workspaceState.context
const workspaceStatus = getWorkspaceStatus(workspace?.latest_build)

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

if (workspaceState.matches("error")) {
return <ErrorSummary error={getWorkspaceError || getTemplateError || getOrganizationError} />
return <ErrorSummary error={getWorkspaceError} />
} else if (!workspace) {
return <FullScreenLoader />
} else {
Expand All @@ -44,6 +42,8 @@ export const WorkspacePage: React.FC = () => {
handleRetry={() => workspaceSend("RETRY")}
handleUpdate={() => workspaceSend("UPDATE")}
workspaceStatus={workspaceStatus}
resources={resources}
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
builds={builds}
/>
</Stack>
Expand Down
15 changes: 14 additions & 1 deletion site/src/testHelpers/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,15 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {
updated_at: "",
}

export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = {
...MockWorkspaceAgent,
id: "test-workspace-agent-2",
name: "another-workspace-agent",
status: "disconnected",
}

export const MockWorkspaceResource: TypesGen.WorkspaceResource = {
agents: [MockWorkspaceAgent],
agents: [MockWorkspaceAgent, MockWorkspaceAgentDisconnected],
created_at: "",
id: "test-workspace-resource",
job_id: "",
Expand All @@ -198,6 +205,12 @@ export const MockWorkspaceResource: TypesGen.WorkspaceResource = {
workspace_transition: "start",
}

export const MockWorkspaceResource2 = {
...MockWorkspaceResource,
id: "test-workspace-resource-2",
name: "another-workspace-resource",
}

export const MockUserAgent: Types.UserAgent = {
browser: "Chrome 99.0.4844",
device: "Other",
Expand Down
2 changes: 1 addition & 1 deletion site/src/testHelpers/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export const handlers = [
return res(ctx.status(200), ctx.json(M.MockWorkspaceBuild))
}),
rest.get("/api/v2/workspacebuilds/:workspaceBuildId/resources", (req, res, ctx) => {
return res(ctx.status(200), ctx.json([M.MockWorkspaceResource]))
return res(ctx.status(200), ctx.json([M.MockWorkspaceResource, M.MockWorkspaceResource2]))
}),
rest.get("/api/v2/workspacebuilds/:workspaceBuildId/logs", (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspaceBuildLogs))
Expand Down
39 changes: 38 additions & 1 deletion site/src/util/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Theme } from "@material-ui/core/styles"
import dayjs from "dayjs"
import { WorkspaceBuildTransition } from "../api/types"
import { WorkspaceBuild } from "../api/typesGenerated"
import { WorkspaceAgent, WorkspaceBuild } from "../api/typesGenerated"

export type WorkspaceStatus =
| "queued"
Expand Down Expand Up @@ -148,3 +148,40 @@ export const displayWorkspaceBuildDuration = (build: WorkspaceBuild, inProgressL
const duration = getWorkspaceBuildDurationInSeconds(build)
return duration ? `${duration} seconds` : inProgressLabel
}

export const DisplayAgentStatusLanguage = {
connected: "⦿ Connected",
connecting: "⦿ Connecting",
disconnected: "◍ Disconnected",
}

export const getDisplayAgentStatus = (
theme: Theme,
agent: WorkspaceAgent,
): {
color: string
status: string
} => {
switch (agent.status) {
case undefined:
return {
color: theme.palette.text.secondary,
status: DisplayStatusLanguage.loading,
}
case "connected":
return {
color: theme.palette.success.main,
status: DisplayAgentStatusLanguage["connected"],
}
case "connecting":
return {
color: theme.palette.success.main,
status: DisplayAgentStatusLanguage["connecting"],
}
case "disconnected":
return {
color: theme.palette.text.secondary,
status: DisplayAgentStatusLanguage["disconnected"],
}
}
}
Loading