-
Notifications
You must be signed in to change notification settings - Fork 899
feat: Add templates page #1510
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
feat: Add templates page #1510
Changes from 8 commits
a72ba40
c2699a8
eb6644f
3d70c67
c63c563
08069d3
584d3a3
09e8a27
a005711
8eaf97d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import Avatar from "@material-ui/core/Avatar" | ||
import Button from "@material-ui/core/Button" | ||
import Link from "@material-ui/core/Link" | ||
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 AddCircleOutline from "@material-ui/icons/AddCircleOutline" | ||
import dayjs from "dayjs" | ||
import relativeTime from "dayjs/plugin/relativeTime" | ||
import React from "react" | ||
import { Link as RouterLink } from "react-router-dom" | ||
import * as TypesGen from "../../api/typesGenerated" | ||
import { Margins } from "../../components/Margins/Margins" | ||
import { Stack } from "../../components/Stack/Stack" | ||
import { firstLetter } from "../../util/firstLetter" | ||
|
||
dayjs.extend(relativeTime) | ||
|
||
export const Language = { | ||
createButton: "Create Template", | ||
emptyViewCreate: "to standardize development workspaces for your team.", | ||
emptyViewNoPerms: "No templates have been created! Contact your Coder administrator.", | ||
} | ||
|
||
export interface TemplatesPageViewProps { | ||
loading?: boolean | ||
canCreateTemplate?: boolean | ||
templates?: TypesGen.Template[] | ||
error?: unknown | ||
kylecarbs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
export const TemplatesPageView: React.FC<TemplatesPageViewProps> = (props) => { | ||
const styles = useStyles() | ||
return ( | ||
<Stack spacing={4}> | ||
<Margins> | ||
<div className={styles.actions}> | ||
{props.canCreateTemplate && <Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>} | ||
</div> | ||
<Table> | ||
<TableHead> | ||
<TableRow> | ||
<TableCell>Name</TableCell> | ||
<TableCell>Used By</TableCell> | ||
<TableCell>Last Updated</TableCell> | ||
Comment on lines
+46
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: Language for these as well There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm happy to add it, but I'm curious why do it for small labels like this? Seems like it's most valuable for testing, and I don't think we'd want to test that each column appears. |
||
</TableRow> | ||
</TableHead> | ||
<TableBody> | ||
{!props.loading && !props.templates?.length && ( | ||
<TableRow> | ||
<TableCell colSpan={999}> | ||
<div className={styles.welcome}> | ||
{props.canCreateTemplate ? ( | ||
<span> | ||
<Link component={RouterLink} to="/templates/new"> | ||
Create a template | ||
</Link> | ||
{Language.emptyViewCreate} | ||
</span> | ||
) : ( | ||
<span>{Language.emptyViewNoPerms}</span> | ||
)} | ||
</div> | ||
</TableCell> | ||
</TableRow> | ||
)} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added! |
||
{props.templates?.map((template) => { | ||
return ( | ||
<TableRow key={template.id} className={styles.templateRow}> | ||
<TableCell> | ||
<div className={styles.templateName}> | ||
<Avatar variant="square" className={styles.templateAvatar}> | ||
{firstLetter(template.name)} | ||
</Avatar> | ||
<Link component={RouterLink} to={`/templates/${template.id}`} className={styles.templateLink}> | ||
<b>{template.name}</b> | ||
<span>{template.description}</span> | ||
</Link> | ||
</div> | ||
</TableCell> | ||
<TableCell> | ||
kylecarbs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{template.workspace_owner_count} developer{template.workspace_owner_count !== 1 && "s"} | ||
kylecarbs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</TableCell> | ||
<TableCell>{dayjs().to(dayjs(template.updated_at))}</TableCell> | ||
kylecarbs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</TableRow> | ||
) | ||
})} | ||
kylecarbs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</TableBody> | ||
</Table> | ||
</Margins> | ||
</Stack> | ||
) | ||
} | ||
|
||
const useStyles = makeStyles((theme) => ({ | ||
actions: { | ||
marginTop: theme.spacing(3), | ||
marginBottom: theme.spacing(3), | ||
display: "flex", | ||
height: theme.spacing(6), | ||
|
||
"& button": { | ||
marginLeft: "auto", | ||
}, | ||
}, | ||
welcome: { | ||
paddingTop: theme.spacing(12), | ||
paddingBottom: theme.spacing(12), | ||
display: "flex", | ||
flexDirection: "column", | ||
alignItems: "center", | ||
justifyContent: "center", | ||
"& span": { | ||
maxWidth: 600, | ||
textAlign: "center", | ||
fontSize: theme.spacing(2), | ||
lineHeight: `${theme.spacing(3)}px`, | ||
}, | ||
}, | ||
templateRow: { | ||
"& > td": { | ||
paddingTop: theme.spacing(2), | ||
paddingBottom: theme.spacing(2), | ||
}, | ||
}, | ||
templateAvatar: { | ||
borderRadius: 2, | ||
marginRight: theme.spacing(1), | ||
width: 24, | ||
height: 24, | ||
fontSize: 16, | ||
}, | ||
templateName: { | ||
display: "flex", | ||
alignItems: "center", | ||
}, | ||
kylecarbs marked this conversation as resolved.
Show resolved
Hide resolved
|
||
templateLink: { | ||
display: "flex", | ||
flexDirection: "column", | ||
color: theme.palette.text.primary, | ||
textDecoration: "none", | ||
"&:hover": { | ||
textDecoration: "underline", | ||
}, | ||
"& span": { | ||
fontSize: 12, | ||
color: theme.palette.text.secondary, | ||
}, | ||
}, | ||
})) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { screen } from "@testing-library/react" | ||
import { rest } from "msw" | ||
import React from "react" | ||
import { MockTemplate } from "../../testHelpers/entities" | ||
import { history, render } from "../../testHelpers/renderHelpers" | ||
import { server } from "../../testHelpers/server" | ||
import TemplatesPage from "./TemplatesPage" | ||
import { Language } from "./TemplatesPageView" | ||
|
||
describe("TemplatesPage", () => { | ||
beforeEach(() => { | ||
history.replace("/workspaces") | ||
}) | ||
|
||
it("renders an empty templates page", async () => { | ||
// Given | ||
server.use( | ||
rest.get("/api/v2/organizations/:organizationId/templates", (req, res, ctx) => { | ||
return res(ctx.status(200), ctx.json([])) | ||
}), | ||
rest.post("/api/v2/users/:userId/authorization", async (req, res, ctx) => { | ||
return res( | ||
ctx.status(200), | ||
ctx.json({ | ||
createTemplates: true, | ||
}), | ||
) | ||
}), | ||
) | ||
|
||
// When | ||
render(<TemplatesPage />) | ||
|
||
// Then | ||
await screen.findByText(Language.emptyViewCreate) | ||
}) | ||
|
||
it("renders a filled templates page", async () => { | ||
// When | ||
render(<TemplatesPage />) | ||
|
||
// Then | ||
await screen.findByText(MockTemplate.name) | ||
}) | ||
|
||
it("shows empty view without permissions to create", async () => { | ||
server.use( | ||
rest.get("/api/v2/organizations/:organizationId/templates", (req, res, ctx) => { | ||
return res(ctx.status(200), ctx.json([])) | ||
}), | ||
rest.post("/api/v2/users/:userId/authorization", async (req, res, ctx) => { | ||
return res( | ||
ctx.status(200), | ||
ctx.json({ | ||
createTemplates: false, | ||
}), | ||
) | ||
}), | ||
) | ||
|
||
// When | ||
render(<TemplatesPage />) | ||
|
||
// Then | ||
await screen.findByText(Language.emptyViewNoPerms) | ||
}) | ||
}) | ||
kylecarbs marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { useMachine } from "@xstate/react" | ||
import React from "react" | ||
import { templatesMachine } from "../../xServices/templates/templatesXService" | ||
import { TemplatesPageView } from "./TemplatesPageView" | ||
|
||
const TemplatesPage: React.FC = () => { | ||
const [templatesState] = useMachine(templatesMachine) | ||
|
||
return ( | ||
<TemplatesPageView | ||
templates={templatesState.context.templates} | ||
canCreateTemplate={templatesState.context.canCreateTemplate} | ||
loading={templatesState.hasTag("loading")} | ||
/> | ||
) | ||
} | ||
|
||
export default TemplatesPage |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { ComponentMeta, Story } from "@storybook/react" | ||
import React from "react" | ||
import { MockTemplate } from "../../testHelpers/entities" | ||
import { TemplatesPageView, TemplatesPageViewProps } from "./TemplatesPageView" | ||
|
||
export default { | ||
title: "pages/TemplatesPageView", | ||
component: TemplatesPageView, | ||
} as ComponentMeta<typeof TemplatesPageView> | ||
|
||
const Template: Story<TemplatesPageViewProps> = (args) => <TemplatesPageView {...args} /> | ||
|
||
export const AllStates = Template.bind({}) | ||
AllStates.args = { | ||
canCreateTemplate: true, | ||
templates: [ | ||
MockTemplate, | ||
{ | ||
...MockTemplate, | ||
description: "🚀 Some magical template that does some magical things!", | ||
}, | ||
{ | ||
...MockTemplate, | ||
workspace_owner_count: 150, | ||
description: "😮 Wow, this one has a bunch of usage!", | ||
}, | ||
], | ||
} | ||
|
||
export const EmptyCanCreate = Template.bind({}) | ||
EmptyCanCreate.args = { | ||
canCreateTemplate: true, | ||
} | ||
|
||
export const EmptyCannotCreate = Template.bind({}) | ||
EmptyCannotCreate.args = {} | ||
kylecarbs marked this conversation as resolved.
Show resolved
Hide resolved
|
Uh oh!
There was an error while loading. Please reload this page.