Skip to content

Commit 9f19041

Browse files
authored
fix: Update routes for project page, workspace creation page, and workspace page (coder#415)
Some API routes were updated in coder#401, which impacted the UX - the flow is currently broken when trying to navigate to a project: ![2022-03-08 15 30 59](https://user-images.githubusercontent.com/88213859/157343533-3d08edf1-70d5-433b-b4a0-fe68875b1928.gif) This fixes all the routes so that the complete project -> create workspace -> workspace page flow works: ![2022-03-08 16 18 57](https://user-images.githubusercontent.com/88213859/157348186-b9bde553-c602-484e-89bc-208a1d97f703.gif) Because this had to touch a bunch of UI routes, I also opportunistically fixed coder#380 as part of this change. Fixes coder#380
1 parent ac387a1 commit 9f19041

File tree

9 files changed

+152
-97
lines changed

9 files changed

+152
-97
lines changed

site/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export interface Workspace {
8484

8585
export namespace Workspace {
8686
export const create = async (request: CreateWorkspaceRequest): Promise<Workspace> => {
87-
const response = await fetch(`/api/v2/workspaces/me`, {
87+
const response = await fetch(`/api/v2/users/me/workspaces`, {
8888
method: "POST",
8989
headers: {
9090
"Content-Type": "application/json",

site/components/Workspace/Workspace.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { render, screen } from "@testing-library/react"
22
import React from "react"
33
import { Workspace } from "./Workspace"
4-
import { MockWorkspace } from "../../test_helpers"
4+
import { MockOrganization, MockProject, MockWorkspace } from "../../test_helpers"
55

66
describe("Workspace", () => {
77
it("renders", async () => {
88
// When
9-
render(<Workspace workspace={MockWorkspace} />)
9+
render(<Workspace organization={MockOrganization} project={MockProject} workspace={MockWorkspace} />)
1010

1111
// Then
1212
const element = await screen.findByText(MockWorkspace.name)

site/components/Workspace/Workspace.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,21 @@ import * as API from "../../api"
1010
import { WorkspaceSection } from "./WorkspaceSection"
1111

1212
export interface WorkspaceProps {
13+
organization: API.Organization
1314
workspace: API.Workspace
15+
project: API.Project
1416
}
1517

1618
/**
1719
* Workspace is the top-level component for viewing an individual workspace
1820
*/
19-
export const Workspace: React.FC<WorkspaceProps> = ({ workspace }) => {
21+
export const Workspace: React.FC<WorkspaceProps> = ({ organization, project, workspace }) => {
2022
const styles = useStyles()
2123

2224
return (
2325
<div className={styles.root}>
2426
<div className={styles.vertical}>
25-
<WorkspaceHeader workspace={workspace} />
27+
<WorkspaceHeader organization={organization} project={project} workspace={workspace} />
2628
<div className={styles.horizontal}>
2729
<div className={styles.sidebarContainer}>
2830
<WorkspaceSection title="Applications">
@@ -54,17 +56,19 @@ export const Workspace: React.FC<WorkspaceProps> = ({ workspace }) => {
5456
/**
5557
* Component for the header at the top of the workspace page
5658
*/
57-
export const WorkspaceHeader: React.FC<WorkspaceProps> = ({ workspace }) => {
59+
export const WorkspaceHeader: React.FC<WorkspaceProps> = ({ organization, project, workspace }) => {
5860
const styles = useStyles()
5961

62+
const projectLink = `/projects/${organization.name}/${project.name}`
63+
6064
return (
6165
<Paper elevation={0} className={styles.section}>
6266
<div className={styles.horizontal}>
6367
<WorkspaceHeroIcon />
6468
<div className={styles.vertical}>
6569
<Typography variant="h4">{workspace.name}</Typography>
6670
<Typography variant="body2" color="textSecondary">
67-
<Link href="javascript:;">{workspace.project_id}</Link>
71+
<Link href={projectLink}>{project.name}</Link>
6872
</Typography>
6973
</div>
7074
</div>

site/pages/projects/[organization]/[project]/create.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,36 @@ import { useUser } from "../../../../contexts/UserContext"
88
import { ErrorSummary } from "../../../../components/ErrorSummary"
99
import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader"
1010
import { CreateWorkspaceForm } from "../../../../forms/CreateWorkspaceForm"
11+
import { unsafeSWRArgument } from "../../../../util"
1112

1213
const CreateWorkspacePage: React.FC = () => {
1314
const { push, query } = useRouter()
1415
const styles = useStyles()
1516
const { me } = useUser(/* redirectOnError */ true)
16-
const { organization, project: projectName } = query
17-
const { data: project, error: projectError } = useSWR<API.Project, Error>(
18-
`/api/v2/projects/${organization}/${projectName}`,
17+
const { organization: organizationName, project: projectName } = query
18+
19+
const { data: organizationInfo, error: organizationError } = useSWR<API.Organization, Error>(
20+
() => `/api/v2/users/me/organizations/${organizationName}`,
1921
)
2022

23+
const { data: project, error: projectError } = useSWR<API.Project, Error>(() => {
24+
return `/api/v2/organizations/${unsafeSWRArgument(organizationInfo).id}/projects/${projectName}`
25+
})
26+
2127
const onCancel = useCallback(async () => {
22-
await push(`/projects/${organization}/${projectName}`)
23-
}, [push, organization, projectName])
28+
await push(`/projects/${organizationName}/${projectName}`)
29+
}, [push, organizationName, projectName])
2430

2531
const onSubmit = async (req: API.CreateWorkspaceRequest) => {
2632
const workspace = await API.Workspace.create(req)
27-
await push(`/workspaces/me/${workspace.name}`)
33+
await push(`/workspaces/${workspace.id}`)
2834
return workspace
2935
}
3036

37+
if (organizationError) {
38+
return <ErrorSummary error={organizationError} />
39+
}
40+
3141
if (projectError) {
3242
return <ErrorSummary error={projectError} />
3343
}

site/pages/projects/[organization]/[project]/index.tsx

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Link from "next/link"
55
import { useRouter } from "next/router"
66
import useSWR from "swr"
77

8-
import { Project, Workspace } from "../../../../api"
8+
import { Organization, Project, Workspace } from "../../../../api"
99
import { Header } from "../../../../components/Header"
1010
import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader"
1111
import { Navbar } from "../../../../components/Navbar"
@@ -15,21 +15,32 @@ import { useUser } from "../../../../contexts/UserContext"
1515
import { ErrorSummary } from "../../../../components/ErrorSummary"
1616
import { firstOrItem } from "../../../../util/array"
1717
import { EmptyState } from "../../../../components/EmptyState"
18+
import { unsafeSWRArgument } from "../../../../util"
1819

1920
const ProjectPage: React.FC = () => {
2021
const styles = useStyles()
2122
const { me, signOut } = useUser(true)
2223

2324
const router = useRouter()
24-
const { project, organization } = router.query
25+
const { project: projectName, organization: organizationName } = router.query
2526

26-
const { data: projectInfo, error: projectError } = useSWR<Project, Error>(
27-
() => `/api/v2/projects/${organization}/${project}`,
27+
const { data: organizationInfo, error: organizationError } = useSWR<Organization, Error>(
28+
() => `/api/v2/users/me/organizations/${organizationName}`,
2829
)
29-
const { data: workspaces, error: workspacesError } = useSWR<Workspace[], Error>(
30-
() => `/api/v2/projects/${organization}/${project}/workspaces`,
30+
31+
const { data: projectInfo, error: projectError } = useSWR<Project, Error>(
32+
() => `/api/v2/organizations/${unsafeSWRArgument(organizationInfo).id}/projects/${projectName}`,
3133
)
3234

35+
// TODO: The workspaces endpoint was recently changed, so that we can't get
36+
// workspaces per-project. This just grabs all workspaces... and then
37+
// later filters them to match the current project.
38+
const { data: workspaces, error: workspacesError } = useSWR<Workspace[], Error>(() => `/api/v2/users/me/workspaces`)
39+
40+
if (organizationError) {
41+
return <ErrorSummary error={organizationError} />
42+
}
43+
3344
if (projectError) {
3445
return <ErrorSummary error={projectError} />
3546
}
@@ -43,7 +54,7 @@ const ProjectPage: React.FC = () => {
4354
}
4455

4556
const createWorkspace = () => {
46-
void router.push(`/projects/${organization}/${project}/create`)
57+
void router.push(`/projects/${organizationName}/${projectName}/create`)
4758
}
4859

4960
const emptyState = (
@@ -61,26 +72,30 @@ const ProjectPage: React.FC = () => {
6172
{
6273
key: "name",
6374
name: "Name",
64-
renderer: (nameField: string) => {
65-
return <Link href={`/workspaces/me/${nameField}`}>{nameField}</Link>
75+
renderer: (nameField: string, workspace: Workspace) => {
76+
return <Link href={`/workspaces/${workspace.id}`}>{nameField}</Link>
6677
},
6778
},
6879
]
6980

81+
const perProjectWorkspaces = workspaces.filter((workspace) => {
82+
return workspace.project_id === projectInfo.id
83+
})
84+
7085
const tableProps = {
7186
title: "Workspaces",
7287
columns,
73-
data: workspaces,
88+
data: perProjectWorkspaces,
7489
emptyState: emptyState,
7590
}
7691

7792
return (
7893
<div className={styles.root}>
7994
<Navbar user={me} onSignOut={signOut} />
8095
<Header
81-
title={firstOrItem(project, "")}
82-
description={firstOrItem(organization, "")}
83-
subTitle={`${workspaces.length} workspaces`}
96+
title={firstOrItem(projectName, "")}
97+
description={firstOrItem(organizationName, "")}
98+
subTitle={`${perProjectWorkspaces.length} workspaces`}
8499
action={{
85100
text: "Create Workspace",
86101
onClick: createWorkspace,

site/pages/workspaces/[user]/[workspace].tsx

Lines changed: 0 additions & 71 deletions
This file was deleted.

site/pages/workspaces/[workspace].tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from "react"
2+
import useSWR from "swr"
3+
import { makeStyles } from "@material-ui/core/styles"
4+
import { useRouter } from "next/router"
5+
import { Navbar } from "../../components/Navbar"
6+
import { Footer } from "../../components/Page"
7+
import { useUser } from "../../contexts/UserContext"
8+
import { firstOrItem } from "../../util/array"
9+
import { ErrorSummary } from "../../components/ErrorSummary"
10+
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
11+
import { Workspace } from "../../components/Workspace"
12+
import { unsafeSWRArgument } from "../../util"
13+
import * as API from "../../api"
14+
15+
const WorkspacesPage: React.FC = () => {
16+
const styles = useStyles()
17+
const router = useRouter()
18+
const { me, signOut } = useUser(true)
19+
20+
const { workspace: workspaceQueryParam } = router.query
21+
22+
const { data: workspace, error: workspaceError } = useSWR<API.Workspace, Error>(() => {
23+
const workspaceParam = firstOrItem(workspaceQueryParam, null)
24+
25+
return `/api/v2/workspaces/${workspaceParam}`
26+
})
27+
28+
// Fetch parent project
29+
const { data: project, error: projectError } = useSWR<API.Project, Error>(() => {
30+
return `/api/v2/projects/${unsafeSWRArgument(workspace).project_id}`
31+
})
32+
33+
const { data: organization, error: organizationError } = useSWR<API.Project, Error>(() => {
34+
return `/api/v2/organizations/${unsafeSWRArgument(project).organization_id}`
35+
})
36+
37+
if (workspaceError) {
38+
return <ErrorSummary error={workspaceError} />
39+
}
40+
41+
if (projectError) {
42+
return <ErrorSummary error={projectError} />
43+
}
44+
45+
if (organizationError) {
46+
return <ErrorSummary error={organizationError} />
47+
}
48+
49+
if (!me || !workspace || !project || !organization) {
50+
return <FullScreenLoader />
51+
}
52+
53+
return (
54+
<div className={styles.root}>
55+
<Navbar user={me} onSignOut={signOut} />
56+
57+
<div className={styles.inner}>
58+
<Workspace organization={organization} project={project} workspace={workspace} />
59+
</div>
60+
61+
<Footer />
62+
</div>
63+
)
64+
}
65+
66+
const useStyles = makeStyles(() => ({
67+
root: {
68+
display: "flex",
69+
flexDirection: "column",
70+
},
71+
inner: {
72+
maxWidth: "1380px",
73+
margin: "1em auto",
74+
width: "100%",
75+
},
76+
}))
77+
78+
export default WorkspacesPage

site/util/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./array"
2+
export * from "./swr"

site/util/swr.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* unsafeSWRArgument
3+
*
4+
* Helper function for working with SWR / useSWR in the TypeScript world.
5+
* TypeScript is helpful in enforcing type-safety, but SWR is designed to
6+
* with the expectation that, if the argument is not available, an exception
7+
* will be thrown.
8+
*
9+
* This just helps in abiding by those rules, explicitly, and lets us suppress
10+
* the lint warning in a single place.
11+
*/
12+
export const unsafeSWRArgument = <T>(arg: T | null | undefined): T => {
13+
if (typeof arg === "undefined" || arg === null) {
14+
throw "SWR: Expected exception because the argument is not available"
15+
}
16+
return arg
17+
}

0 commit comments

Comments
 (0)