Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"history": "5.3.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-markdown": "^8.0.3",
"react-router-dom": "6.3.0",
"sourcemapped-stacktrace": "1.1.11",
"swr": "1.2.2",
Expand Down
10 changes: 10 additions & 0 deletions site/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { OrgsPage } from "./pages/OrgsPage/OrgsPage"
import { SettingsPage } from "./pages/SettingsPage/SettingsPage"
import { AccountPage } from "./pages/SettingsPages/AccountPage/AccountPage"
import { SSHKeysPage } from "./pages/SettingsPages/SSHKeysPage/SSHKeysPage"
import { TemplatePage } from "./pages/TemplatePage/TemplatePage"
import TemplatesPage from "./pages/TemplatesPage/TemplatesPage"
import { CreateUserPage } from "./pages/UsersPage/CreateUserPage/CreateUserPage"
import { UsersPage } from "./pages/UsersPage/UsersPage"
Expand Down Expand Up @@ -95,6 +96,15 @@ export const AppRouter: React.FC = () => (
</AuthAndFrame>
}
/>

<Route
path=":template"
element={
<AuthAndFrame>
<TemplatePage />
</AuthAndFrame>
}
/>
</Route>

<Route path="users">
Expand Down
7 changes: 7 additions & 0 deletions site/src/__mocks__/react-markdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react"

const ReactMarkdown: React.FC = ({ children }) => {
return <div data-testid="markdown">{children}</div>
}

export default ReactMarkdown
5 changes: 5 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export const getTemplateVersionSchema = async (versionId: string): Promise<Types
return response.data
}

export const getTemplateVersionResources = async (versionId: string): Promise<TypesGen.WorkspaceResource[]> => {
const response = await axios.get<TypesGen.WorkspaceResource[]>(`/api/v2/templateversions/${versionId}/resources`)
return response.data
}

export const getWorkspace = async (workspaceId: string): Promise<TypesGen.Workspace> => {
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`)
return response.data
Expand Down
11 changes: 10 additions & 1 deletion site/src/components/Resources/Resources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ export const Resources: React.FC<ResourcesProps> = ({ resources, getResourcesErr
)}

<TableCell className={styles.agentColumn}>
<span style={{ color: theme.palette.text.secondary }}>{agent.name}</span>
{agent.name}
<span className={styles.operatingSystem}>{agent.operating_system}</span>
</TableCell>
<TableCell>
<span style={{ color: getDisplayAgentStatus(theme, agent).color }}>
Expand Down Expand Up @@ -143,4 +144,12 @@ const useStyles = makeStyles((theme) => ({
marginRight: theme.spacing(1.5),
},
},

operatingSystem: {
fontSize: 14,
Copy link
Member

Choose a reason for hiding this comment

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

should we try to use a typography property here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Probably yes

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Since there are more styles involved I will keep it right now.

color: theme.palette.text.secondary,
marginTop: theme.spacing(0.5),
display: "block",
textTransform: "capitalize",
},
}))
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { makeStyles } 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 React from "react"
import { WorkspaceResource } from "../../api/typesGenerated"
import { TableHeaderRow } from "../TableHeaders/TableHeaders"

const Language = {
resourceLabel: "Resource",
agentLabel: "Agent",
}

interface TemplateResourcesProps {
resources: WorkspaceResource[]
}

export const TemplateResourcesTable: React.FC<TemplateResourcesProps> = ({ resources }) => {
const styles = useStyles()

return (
<Table className={styles.table}>
<TableHead>
<TableHeaderRow>
<TableCell>{Language.resourceLabel}</TableCell>
<TableCell className={styles.agentColumn}>{Language.agentLabel}</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}
<span className={styles.resourceType}>{resource.type}</span>
</TableCell>
<TableCell colSpan={3}></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}
<span className={styles.resourceType}>{resource.type}</span>
</TableCell>
)}

<TableCell className={styles.agentColumn}>
{agent.name}
<span className={styles.operatingSystem}>{agent.operating_system}</span>
</TableCell>
</TableRow>
)
})
})}
</TableBody>
</Table>
)
}

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

table: {
border: 0,
},

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

resourceType: {
fontSize: 14,
color: theme.palette.text.secondary,
marginTop: theme.spacing(0.5),
display: "block",
},

// Adds some left spacing
agentColumn: {
paddingLeft: `${theme.spacing(2)}px !important`,
},

operatingSystem: {
fontSize: 14,
color: theme.palette.text.secondary,
marginTop: theme.spacing(0.5),
display: "block",
textTransform: "capitalize",
},
}))
26 changes: 26 additions & 0 deletions site/src/components/TemplateStats/TemplateStats.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Story } from "@storybook/react"
import React from "react"
import * as Mocks from "../../testHelpers/renderHelpers"
import { TemplateStats, TemplateStatsProps } from "../TemplateStats/TemplateStats"

export default {
title: "components/TemplateStats",
component: TemplateStats,
}

const Template: Story<TemplateStatsProps> = (args) => <TemplateStats {...args} />

export const Example = Template.bind({})
Example.args = {
template: Mocks.MockTemplate,
activeVersion: Mocks.MockTemplateVersion,
}

export const UsedByMany = Template.bind({})
UsedByMany.args = {
template: {
...Mocks.MockTemplate,
workspace_owner_count: 15,
},
activeVersion: Mocks.MockTemplateVersion,
}
90 changes: 90 additions & 0 deletions site/src/components/TemplateStats/TemplateStats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { makeStyles } from "@material-ui/core/styles"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import React from "react"
import { Template, TemplateVersion } from "../../api/typesGenerated"
import { CardRadius, MONOSPACE_FONT_FAMILY } from "../../theme/constants"

dayjs.extend(relativeTime)

const Language = {
usedByLabel: "Used by",
activeVersionLabel: "Active version",
lastUpdateLabel: "Last updated",
userPlural: "users",
userSingular: "user",
}

export interface TemplateStatsProps {
template: Template
activeVersion: TemplateVersion
}

export const TemplateStats: React.FC<TemplateStatsProps> = ({ template, activeVersion }) => {
const styles = useStyles()

return (
<div className={styles.stats}>
<div className={styles.statItem}>
<span className={styles.statsLabel}>{Language.usedByLabel}</span>

<span className={styles.statsValue}>
{template.workspace_owner_count}{" "}
{template.workspace_owner_count === 1 ? Language.userSingular : Language.userPlural}
</span>
</div>
<div className={styles.statsDivider} />
<div className={styles.statItem}>
<span className={styles.statsLabel}>{Language.activeVersionLabel}</span>
<span className={styles.statsValue}>{activeVersion.name}</span>
</div>
<div className={styles.statsDivider} />
<div className={styles.statItem}>
<span className={styles.statsLabel}>{Language.lastUpdateLabel}</span>
<span className={styles.statsValue} data-chromatic="ignore">
{dayjs().to(dayjs(template.updated_at))}
</span>
</div>
</div>
)
}

const useStyles = makeStyles((theme) => ({
stats: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
backgroundColor: theme.palette.background.paper,
borderRadius: CardRadius,
display: "flex",
alignItems: "center",
color: theme.palette.text.secondary,
fontFamily: MONOSPACE_FONT_FAMILY,
border: `1px solid ${theme.palette.divider}`,
},

statItem: {
minWidth: theme.spacing(20),
padding: theme.spacing(2),
paddingTop: theme.spacing(1.75),
},

statsLabel: {
fontSize: 12,
textTransform: "uppercase",
display: "block",
fontWeight: 600,
},

statsValue: {
fontSize: 16,
marginTop: theme.spacing(0.25),
display: "inline-block",
},

statsDivider: {
width: 1,
height: theme.spacing(5),
backgroundColor: theme.palette.divider,
marginRight: theme.spacing(2),
},
}))
15 changes: 15 additions & 0 deletions site/src/hooks/useOrganizationID.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useActor } from "@xstate/react"
import { useContext } from "react"
import { XServiceContext } from "../xServices/StateContext"

export const useOrganizationID = (): string => {
const xServices = useContext(XServiceContext)
const [authState] = useActor(xServices.authXService)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should do useInterpret and useSelector here so it doesn't cause rerenders for any data except the org id.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Since this is getting authentication data which is a global state I would use the XServiceContext. About the selector for organization Id, I think it is a good idea.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh you're right, we don't need useInterpret here. But I think we should not have useActor. In fact I don't know if we need a hook for this, you could do it like this

const myOrgId = useSelector(xServices.authXService, selectOrgId)

const organizationId = authState.context.me?.organization_ids[0]

if (!organizationId) {
throw new Error("No organization ID found")
}

return organizationId
}
20 changes: 4 additions & 16 deletions site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
import { useActor, useMachine } from "@xstate/react"
import React, { useContext } from "react"
import { useMachine } from "@xstate/react"
import React from "react"
import { useNavigate, useSearchParams } from "react-router-dom"
import { Template } from "../../api/typesGenerated"
import { useOrganizationID } from "../../hooks/useOrganizationID"
import { createWorkspaceMachine } from "../../xServices/createWorkspace/createWorkspaceXService"
import { XServiceContext } from "../../xServices/StateContext"
import { CreateWorkspacePageView } from "./CreateWorkspacePageView"

const useOrganizationId = () => {
const xServices = useContext(XServiceContext)
const [authState] = useActor(xServices.authXService)
const organizationId = authState.context.me?.organization_ids[0]

if (!organizationId) {
throw new Error("No organization ID found")
}

return organizationId
}

const CreateWorkspacePage: React.FC = () => {
const organizationId = useOrganizationId()
const organizationId = useOrganizationID()
const [searchParams] = useSearchParams()
const preSelectedTemplateName = searchParams.get("template")
const navigate = useNavigate()
Expand Down
13 changes: 13 additions & 0 deletions site/src/pages/TemplatePage/TemplatePage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { screen } from "@testing-library/react"
import React from "react"
import { MockTemplate, MockWorkspaceResource, renderWithAuth } from "../../testHelpers/renderHelpers"
import { TemplatePage } from "./TemplatePage"

describe("TemplatePage", () => {
it("shows the template name, readme and resources", async () => {
renderWithAuth(<TemplatePage />, { route: `/templates/${MockTemplate.id}`, path: "/templates/:template" })
await screen.findByText(MockTemplate.name)
screen.getByTestId("markdown")
screen.getByText(MockWorkspaceResource.name)
})
})
Loading