mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user