feat: Initial workspaces page route + skeleton (#220)

This implements an initial route for the workspaces page at `/workspaces/{user}/{workspace}`, and an additional storybook entry:

<img width="1723" alt="image" src="https://user-images.githubusercontent.com/88213859/153271281-801a5889-f60e-458c-809f-396a2d6385fa.png">

Just the header is rendering right now. Will be adding the additional sections prototyped in #219 in subsequent PRs

Related to https://github.com/coder/coder/issues/67
This commit is contained in:
Bryan
2022-02-15 20:49:56 -08:00
committed by GitHub
parent 6009c90d1d
commit 4954a6b0a3
5 changed files with 191 additions and 0 deletions
@@ -0,0 +1,17 @@
import { Story } from "@storybook/react"
import React from "react"
import { Workspace, WorkspaceProps } from "./Workspace"
import { MockWorkspace } from "../../test_helpers"
export default {
title: "Workspace",
component: Workspace,
argTypes: {},
}
const Template: Story<WorkspaceProps> = (args) => <Workspace {...args} />
export const Example = Template.bind({})
Example.args = {
workspace: MockWorkspace,
}
@@ -0,0 +1,15 @@
import { render, screen } from "@testing-library/react"
import React from "react"
import { Workspace } from "./Workspace"
import { MockWorkspace } from "../../test_helpers"
describe("Workspace", () => {
it("renders", async () => {
// When
render(<Workspace workspace={MockWorkspace} />)
// Then
const element = await screen.findByText(MockWorkspace.name)
expect(element).toBeDefined()
})
})
+90
View File
@@ -0,0 +1,90 @@
import Box from "@material-ui/core/Box"
import Paper from "@material-ui/core/Paper"
import Typography from "@material-ui/core/Typography"
import { makeStyles } from "@material-ui/core/styles"
import CloudCircleIcon from "@material-ui/icons/CloudCircle"
import Link from "next/link"
import React from "react"
import * as API from "../../api"
export interface WorkspaceProps {
workspace: API.Workspace
}
namespace Constants {
export const TitleIconSize = 48
export const CardRadius = 8
export const CardPadding = 20
}
/**
* Workspace is the top-level component for viewing an individual workspace
*/
export const Workspace: React.FC<WorkspaceProps> = ({ workspace }) => {
const styles = useStyles()
return (
<div className={styles.root}>
<WorkspaceHeader workspace={workspace} />
</div>
)
}
/**
* Component for the header at the top of the workspace page
*/
export const WorkspaceHeader: React.FC<WorkspaceProps> = ({ workspace }) => {
const styles = useStyles()
return (
<Paper elevation={0} className={styles.section}>
<div className={styles.horizontal}>
<WorkspaceHeroIcon />
<div className={styles.vertical}>
<Typography variant="h4">{workspace.name}</Typography>
<Typography variant="body2" color="textSecondary">
<Link href="javascript:;">{workspace.project_id}</Link>
</Typography>
</div>
</div>
</Paper>
)
}
/**
* Component to render the 'Hero Icon' in the header of a workspace
*/
export const WorkspaceHeroIcon: React.FC = () => {
return (
<Box mr="1em">
<CloudCircleIcon width={Constants.TitleIconSize} height={Constants.TitleIconSize} />
</Box>
)
}
export const useStyles = makeStyles((theme) => {
return {
root: {
display: "flex",
flexDirection: "column",
},
horizontal: {
display: "flex",
flexDirection: "row",
},
vertical: {
display: "flex",
flexDirection: "column",
},
section: {
border: `1px solid ${theme.palette.divider}`,
borderRadius: Constants.CardRadius,
padding: Constants.CardPadding,
},
icon: {
width: Constants.TitleIconSize,
height: Constants.TitleIconSize,
},
}
})
+1
View File
@@ -0,0 +1 @@
export * from "./Workspace"
@@ -0,0 +1,68 @@
import React from "react"
import useSWR from "swr"
import { makeStyles } from "@material-ui/core/styles"
import { useRouter } from "next/router"
import { Navbar } from "../../../components/Navbar"
import { Footer } from "../../../components/Page"
import { useUser } from "../../../contexts/UserContext"
import { firstOrItem } from "../../../util/array"
import { ErrorSummary } from "../../../components/ErrorSummary"
import { FullScreenLoader } from "../../../components/Loader/FullScreenLoader"
import { Workspace } from "../../../components/Workspace"
import * as API from "../../../api"
const WorkspacesPage: React.FC = () => {
const styles = useStyles()
const router = useRouter()
const { me, signOut } = useUser(true)
const { user: userQueryParam, workspace: workspaceQueryParam } = router.query
const { data: workspace, error: workspaceError } = useSWR<API.Workspace, Error>(() => {
const userParam = firstOrItem(userQueryParam, null)
const workspaceParam = firstOrItem(workspaceQueryParam, null)
// TODO(Bryan): Getting non-personal users isn't supported yet in the backend.
// So if the user is the same as 'me', use 'me' as the parameter
const normalizedUserParam = me && userParam === me.id ? "me" : userParam
// The SWR API expects us to 'throw' if the query isn't ready yet, so these casts to `any` are OK
// because the API expects exceptions.
return `/api/v2/workspaces/${(normalizedUserParam as any).toString()}/${(workspaceParam as any).toString()}`
})
if (workspaceError) {
return <ErrorSummary error={workspaceError} />
}
if (!me || !workspace) {
return <FullScreenLoader />
}
return (
<div className={styles.root}>
<Navbar user={me} onSignOut={signOut} />
<div className={styles.inner}>
<Workspace workspace={workspace} />
</div>
<Footer />
</div>
)
}
const useStyles = makeStyles(() => ({
root: {
display: "flex",
flexDirection: "column",
},
inner: {
maxWidth: "1380px",
margin: "1em auto",
width: "100%",
},
}))
export default WorkspacesPage