Skip to content

Commit 104d07f

Browse files
feat: Add the template page (#1754)
1 parent 7c59ec4 commit 104d07f

File tree

19 files changed

+1082
-23
lines changed

19 files changed

+1082
-23
lines changed

site/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"history": "5.3.0",
4242
"react": "17.0.2",
4343
"react-dom": "17.0.2",
44+
"react-markdown": "8.0.3",
4445
"react-router-dom": "6.3.0",
4546
"sourcemapped-stacktrace": "1.1.11",
4647
"swr": "1.2.2",

site/src/AppRouter.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { OrgsPage } from "./pages/OrgsPage/OrgsPage"
1212
import { SettingsPage } from "./pages/SettingsPage/SettingsPage"
1313
import { AccountPage } from "./pages/SettingsPages/AccountPage/AccountPage"
1414
import { SSHKeysPage } from "./pages/SettingsPages/SSHKeysPage/SSHKeysPage"
15+
import { TemplatePage } from "./pages/TemplatePage/TemplatePage"
1516
import TemplatesPage from "./pages/TemplatesPage/TemplatesPage"
1617
import { CreateUserPage } from "./pages/UsersPage/CreateUserPage/CreateUserPage"
1718
import { UsersPage } from "./pages/UsersPage/UsersPage"
@@ -104,6 +105,15 @@ export const AppRouter: React.FC = () => (
104105
</AuthAndFrame>
105106
}
106107
/>
108+
109+
<Route
110+
path=":template"
111+
element={
112+
<AuthAndFrame>
113+
<TemplatePage />
114+
</AuthAndFrame>
115+
}
116+
/>
107117
</Route>
108118

109119
<Route path="users">

site/src/__mocks__/react-markdown.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from "react"
2+
3+
const ReactMarkdown: React.FC = ({ children }) => {
4+
return <div data-testid="markdown">{children}</div>
5+
}
6+
7+
export default ReactMarkdown

site/src/api/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ export const getTemplateVersionSchema = async (versionId: string): Promise<Types
102102
return response.data
103103
}
104104

105+
export const getTemplateVersionResources = async (versionId: string): Promise<TypesGen.WorkspaceResource[]> => {
106+
const response = await axios.get<TypesGen.WorkspaceResource[]>(`/api/v2/templateversions/${versionId}/resources`)
107+
return response.data
108+
}
109+
105110
export const getWorkspace = async (workspaceId: string): Promise<TypesGen.Workspace> => {
106111
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`)
107112
return response.data

site/src/components/Resources/Resources.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ export const Resources: React.FC<ResourcesProps> = ({ resources, getResourcesErr
7979
)}
8080

8181
<TableCell className={styles.agentColumn}>
82-
<span style={{ color: theme.palette.text.secondary }}>{agent.name}</span>
82+
{agent.name}
83+
<span className={styles.operatingSystem}>{agent.operating_system}</span>
8384
</TableCell>
8485
<TableCell>
8586
<span style={{ color: getDisplayAgentStatus(theme, agent).color }}>
@@ -143,4 +144,12 @@ const useStyles = makeStyles((theme) => ({
143144
marginRight: theme.spacing(1.5),
144145
},
145146
},
147+
148+
operatingSystem: {
149+
fontSize: 14,
150+
color: theme.palette.text.secondary,
151+
marginTop: theme.spacing(0.5),
152+
display: "block",
153+
textTransform: "capitalize",
154+
},
146155
}))
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { makeStyles } 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 React from "react"
8+
import { WorkspaceResource } from "../../api/typesGenerated"
9+
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
10+
11+
const Language = {
12+
resourceLabel: "Resource",
13+
agentLabel: "Agent",
14+
}
15+
16+
interface TemplateResourcesProps {
17+
resources: WorkspaceResource[]
18+
}
19+
20+
export const TemplateResourcesTable: React.FC<TemplateResourcesProps> = ({ resources }) => {
21+
const styles = useStyles()
22+
23+
return (
24+
<Table className={styles.table}>
25+
<TableHead>
26+
<TableHeaderRow>
27+
<TableCell>{Language.resourceLabel}</TableCell>
28+
<TableCell className={styles.agentColumn}>{Language.agentLabel}</TableCell>
29+
</TableHeaderRow>
30+
</TableHead>
31+
<TableBody>
32+
{resources.map((resource) => {
33+
// We need to initialize the agents to display the resource
34+
const agents = resource.agents ?? [null]
35+
return agents.map((agent, agentIndex) => {
36+
// If there is no agent, just display the resource name
37+
if (!agent) {
38+
return (
39+
<TableRow>
40+
<TableCell className={styles.resourceNameCell}>
41+
{resource.name}
42+
<span className={styles.resourceType}>{resource.type}</span>
43+
</TableCell>
44+
<TableCell colSpan={3}></TableCell>
45+
</TableRow>
46+
)
47+
}
48+
49+
return (
50+
<TableRow key={`${resource.id}-${agent.id}`}>
51+
{/* We only want to display the name in the first row because we are using rowSpan */}
52+
{/* The rowspan should be the same than the number of agents */}
53+
{agentIndex === 0 && (
54+
<TableCell className={styles.resourceNameCell} rowSpan={agents.length}>
55+
{resource.name}
56+
<span className={styles.resourceType}>{resource.type}</span>
57+
</TableCell>
58+
)}
59+
60+
<TableCell className={styles.agentColumn}>
61+
{agent.name}
62+
<span className={styles.operatingSystem}>{agent.operating_system}</span>
63+
</TableCell>
64+
</TableRow>
65+
)
66+
})
67+
})}
68+
</TableBody>
69+
</Table>
70+
)
71+
}
72+
73+
const useStyles = makeStyles((theme) => ({
74+
sectionContents: {
75+
margin: 0,
76+
},
77+
78+
table: {
79+
border: 0,
80+
},
81+
82+
resourceNameCell: {
83+
borderRight: `1px solid ${theme.palette.divider}`,
84+
},
85+
86+
resourceType: {
87+
fontSize: 14,
88+
color: theme.palette.text.secondary,
89+
marginTop: theme.spacing(0.5),
90+
display: "block",
91+
},
92+
93+
// Adds some left spacing
94+
agentColumn: {
95+
paddingLeft: `${theme.spacing(2)}px !important`,
96+
},
97+
98+
operatingSystem: {
99+
fontSize: 14,
100+
color: theme.palette.text.secondary,
101+
marginTop: theme.spacing(0.5),
102+
display: "block",
103+
textTransform: "capitalize",
104+
},
105+
}))
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Story } from "@storybook/react"
2+
import React from "react"
3+
import * as Mocks from "../../testHelpers/renderHelpers"
4+
import { TemplateStats, TemplateStatsProps } from "../TemplateStats/TemplateStats"
5+
6+
export default {
7+
title: "components/TemplateStats",
8+
component: TemplateStats,
9+
}
10+
11+
const Template: Story<TemplateStatsProps> = (args) => <TemplateStats {...args} />
12+
13+
export const Example = Template.bind({})
14+
Example.args = {
15+
template: Mocks.MockTemplate,
16+
activeVersion: Mocks.MockTemplateVersion,
17+
}
18+
19+
export const UsedByMany = Template.bind({})
20+
UsedByMany.args = {
21+
template: {
22+
...Mocks.MockTemplate,
23+
workspace_owner_count: 15,
24+
},
25+
activeVersion: Mocks.MockTemplateVersion,
26+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { makeStyles } from "@material-ui/core/styles"
2+
import dayjs from "dayjs"
3+
import relativeTime from "dayjs/plugin/relativeTime"
4+
import React from "react"
5+
import { Template, TemplateVersion } from "../../api/typesGenerated"
6+
import { CardRadius, MONOSPACE_FONT_FAMILY } from "../../theme/constants"
7+
8+
dayjs.extend(relativeTime)
9+
10+
const Language = {
11+
usedByLabel: "Used by",
12+
activeVersionLabel: "Active version",
13+
lastUpdateLabel: "Last updated",
14+
userPlural: "users",
15+
userSingular: "user",
16+
}
17+
18+
export interface TemplateStatsProps {
19+
template: Template
20+
activeVersion: TemplateVersion
21+
}
22+
23+
export const TemplateStats: React.FC<TemplateStatsProps> = ({ template, activeVersion }) => {
24+
const styles = useStyles()
25+
26+
return (
27+
<div className={styles.stats}>
28+
<div className={styles.statItem}>
29+
<span className={styles.statsLabel}>{Language.usedByLabel}</span>
30+
31+
<span className={styles.statsValue}>
32+
{template.workspace_owner_count}{" "}
33+
{template.workspace_owner_count === 1 ? Language.userSingular : Language.userPlural}
34+
</span>
35+
</div>
36+
<div className={styles.statsDivider} />
37+
<div className={styles.statItem}>
38+
<span className={styles.statsLabel}>{Language.activeVersionLabel}</span>
39+
<span className={styles.statsValue}>{activeVersion.name}</span>
40+
</div>
41+
<div className={styles.statsDivider} />
42+
<div className={styles.statItem}>
43+
<span className={styles.statsLabel}>{Language.lastUpdateLabel}</span>
44+
<span className={styles.statsValue} data-chromatic="ignore">
45+
{dayjs().to(dayjs(template.updated_at))}
46+
</span>
47+
</div>
48+
</div>
49+
)
50+
}
51+
52+
const useStyles = makeStyles((theme) => ({
53+
stats: {
54+
paddingLeft: theme.spacing(2),
55+
paddingRight: theme.spacing(2),
56+
backgroundColor: theme.palette.background.paper,
57+
borderRadius: CardRadius,
58+
display: "flex",
59+
alignItems: "center",
60+
color: theme.palette.text.secondary,
61+
fontFamily: MONOSPACE_FONT_FAMILY,
62+
border: `1px solid ${theme.palette.divider}`,
63+
},
64+
65+
statItem: {
66+
minWidth: theme.spacing(20),
67+
padding: theme.spacing(2),
68+
paddingTop: theme.spacing(1.75),
69+
},
70+
71+
statsLabel: {
72+
fontSize: 12,
73+
textTransform: "uppercase",
74+
display: "block",
75+
fontWeight: 600,
76+
},
77+
78+
statsValue: {
79+
fontSize: 16,
80+
marginTop: theme.spacing(0.25),
81+
display: "inline-block",
82+
},
83+
84+
statsDivider: {
85+
width: 1,
86+
height: theme.spacing(5),
87+
backgroundColor: theme.palette.divider,
88+
marginRight: theme.spacing(2),
89+
},
90+
}))

site/src/hooks/useOrganizationId.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useSelector } from "@xstate/react"
2+
import { useContext } from "react"
3+
import { selectOrgId } from "../xServices/auth/authSelectors"
4+
import { XServiceContext } from "../xServices/StateContext"
5+
6+
export const useOrganizationId = (): string => {
7+
const xServices = useContext(XServiceContext)
8+
const organizationId = useSelector(xServices.authXService, selectOrgId)
9+
10+
if (!organizationId) {
11+
throw new Error("No organization ID found")
12+
}
13+
14+
return organizationId
15+
}

site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,11 @@
1-
import { useActor, useMachine } from "@xstate/react"
2-
import React, { useContext } from "react"
1+
import { useMachine } from "@xstate/react"
2+
import React from "react"
33
import { useNavigate, useSearchParams } from "react-router-dom"
44
import { Template } from "../../api/typesGenerated"
5+
import { useOrganizationId } from "../../hooks/useOrganizationId"
56
import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
6-
import { XServiceContext } from "../../xServices/StateContext"
77
import { CreateWorkspacePageView } from "./CreateWorkspacePageView"
88

9-
const useOrganizationId = () => {
10-
const xServices = useContext(XServiceContext)
11-
const [authState] = useActor(xServices.authXService)
12-
const organizationId = authState.context.me?.organization_ids[0]
13-
14-
if (!organizationId) {
15-
throw new Error("No organization ID found")
16-
}
17-
18-
return organizationId
19-
}
20-
219
const CreateWorkspacePage: React.FC = () => {
2210
const organizationId = useOrganizationId()
2311
const [searchParams] = useSearchParams()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy