Skip to content

Commit

Permalink
fix: Update routes for project page, workspace creation page, and wor…
Browse files Browse the repository at this point in the history
…kspace page (#415)

Some API routes were updated in #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 #380 as part of this change.

Fixes #380
  • Loading branch information
bryphe-coder committed Mar 9, 2022
1 parent ac387a1 commit 9f19041
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 97 deletions.
2 changes: 1 addition & 1 deletion site/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export interface Workspace {

export namespace Workspace {
export const create = async (request: CreateWorkspaceRequest): Promise<Workspace> => {
const response = await fetch(`/api/v2/workspaces/me`, {
const response = await fetch(`/api/v2/users/me/workspaces`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down
4 changes: 2 additions & 2 deletions site/components/Workspace/Workspace.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { render, screen } from "@testing-library/react"
import React from "react"
import { Workspace } from "./Workspace"
import { MockWorkspace } from "../../test_helpers"
import { MockOrganization, MockProject, MockWorkspace } from "../../test_helpers"

describe("Workspace", () => {
it("renders", async () => {
// When
render(<Workspace workspace={MockWorkspace} />)
render(<Workspace organization={MockOrganization} project={MockProject} workspace={MockWorkspace} />)

// Then
const element = await screen.findByText(MockWorkspace.name)
Expand Down
12 changes: 8 additions & 4 deletions site/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@ import * as API from "../../api"
import { WorkspaceSection } from "./WorkspaceSection"

export interface WorkspaceProps {
organization: API.Organization
workspace: API.Workspace
project: API.Project
}

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

return (
<div className={styles.root}>
<div className={styles.vertical}>
<WorkspaceHeader workspace={workspace} />
<WorkspaceHeader organization={organization} project={project} workspace={workspace} />
<div className={styles.horizontal}>
<div className={styles.sidebarContainer}>
<WorkspaceSection title="Applications">
Expand Down Expand Up @@ -54,17 +56,19 @@ export const Workspace: React.FC<WorkspaceProps> = ({ workspace }) => {
/**
* Component for the header at the top of the workspace page
*/
export const WorkspaceHeader: React.FC<WorkspaceProps> = ({ workspace }) => {
export const WorkspaceHeader: React.FC<WorkspaceProps> = ({ organization, project, workspace }) => {
const styles = useStyles()

const projectLink = `/projects/${organization.name}/${project.name}`

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>
<Link href={projectLink}>{project.name}</Link>
</Typography>
</div>
</div>
Expand Down
22 changes: 16 additions & 6 deletions site/pages/projects/[organization]/[project]/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,36 @@ import { useUser } from "../../../../contexts/UserContext"
import { ErrorSummary } from "../../../../components/ErrorSummary"
import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader"
import { CreateWorkspaceForm } from "../../../../forms/CreateWorkspaceForm"
import { unsafeSWRArgument } from "../../../../util"

const CreateWorkspacePage: React.FC = () => {
const { push, query } = useRouter()
const styles = useStyles()
const { me } = useUser(/* redirectOnError */ true)
const { organization, project: projectName } = query
const { data: project, error: projectError } = useSWR<API.Project, Error>(
`/api/v2/projects/${organization}/${projectName}`,
const { organization: organizationName, project: projectName } = query

const { data: organizationInfo, error: organizationError } = useSWR<API.Organization, Error>(
() => `/api/v2/users/me/organizations/${organizationName}`,
)

const { data: project, error: projectError } = useSWR<API.Project, Error>(() => {
return `/api/v2/organizations/${unsafeSWRArgument(organizationInfo).id}/projects/${projectName}`
})

const onCancel = useCallback(async () => {
await push(`/projects/${organization}/${projectName}`)
}, [push, organization, projectName])
await push(`/projects/${organizationName}/${projectName}`)
}, [push, organizationName, projectName])

const onSubmit = async (req: API.CreateWorkspaceRequest) => {
const workspace = await API.Workspace.create(req)
await push(`/workspaces/me/${workspace.name}`)
await push(`/workspaces/${workspace.id}`)
return workspace
}

if (organizationError) {
return <ErrorSummary error={organizationError} />
}

if (projectError) {
return <ErrorSummary error={projectError} />
}
Expand Down
41 changes: 28 additions & 13 deletions site/pages/projects/[organization]/[project]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Link from "next/link"
import { useRouter } from "next/router"
import useSWR from "swr"

import { Project, Workspace } from "../../../../api"
import { Organization, Project, Workspace } from "../../../../api"
import { Header } from "../../../../components/Header"
import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader"
import { Navbar } from "../../../../components/Navbar"
Expand All @@ -15,21 +15,32 @@ import { useUser } from "../../../../contexts/UserContext"
import { ErrorSummary } from "../../../../components/ErrorSummary"
import { firstOrItem } from "../../../../util/array"
import { EmptyState } from "../../../../components/EmptyState"
import { unsafeSWRArgument } from "../../../../util"

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

const router = useRouter()
const { project, organization } = router.query
const { project: projectName, organization: organizationName } = router.query

const { data: projectInfo, error: projectError } = useSWR<Project, Error>(
() => `/api/v2/projects/${organization}/${project}`,
const { data: organizationInfo, error: organizationError } = useSWR<Organization, Error>(
() => `/api/v2/users/me/organizations/${organizationName}`,
)
const { data: workspaces, error: workspacesError } = useSWR<Workspace[], Error>(
() => `/api/v2/projects/${organization}/${project}/workspaces`,

const { data: projectInfo, error: projectError } = useSWR<Project, Error>(
() => `/api/v2/organizations/${unsafeSWRArgument(organizationInfo).id}/projects/${projectName}`,
)

// TODO: The workspaces endpoint was recently changed, so that we can't get
// workspaces per-project. This just grabs all workspaces... and then
// later filters them to match the current project.
const { data: workspaces, error: workspacesError } = useSWR<Workspace[], Error>(() => `/api/v2/users/me/workspaces`)

if (organizationError) {
return <ErrorSummary error={organizationError} />
}

if (projectError) {
return <ErrorSummary error={projectError} />
}
Expand All @@ -43,7 +54,7 @@ const ProjectPage: React.FC = () => {
}

const createWorkspace = () => {
void router.push(`/projects/${organization}/${project}/create`)
void router.push(`/projects/${organizationName}/${projectName}/create`)
}

const emptyState = (
Expand All @@ -61,26 +72,30 @@ const ProjectPage: React.FC = () => {
{
key: "name",
name: "Name",
renderer: (nameField: string) => {
return <Link href={`/workspaces/me/${nameField}`}>{nameField}</Link>
renderer: (nameField: string, workspace: Workspace) => {
return <Link href={`/workspaces/${workspace.id}`}>{nameField}</Link>
},
},
]

const perProjectWorkspaces = workspaces.filter((workspace) => {
return workspace.project_id === projectInfo.id
})

const tableProps = {
title: "Workspaces",
columns,
data: workspaces,
data: perProjectWorkspaces,
emptyState: emptyState,
}

return (
<div className={styles.root}>
<Navbar user={me} onSignOut={signOut} />
<Header
title={firstOrItem(project, "")}
description={firstOrItem(organization, "")}
subTitle={`${workspaces.length} workspaces`}
title={firstOrItem(projectName, "")}
description={firstOrItem(organizationName, "")}
subTitle={`${perProjectWorkspaces.length} workspaces`}
action={{
text: "Create Workspace",
onClick: createWorkspace,
Expand Down
71 changes: 0 additions & 71 deletions site/pages/workspaces/[user]/[workspace].tsx

This file was deleted.

78 changes: 78 additions & 0 deletions site/pages/workspaces/[workspace].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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 { unsafeSWRArgument } from "../../util"
import * as API from "../../api"

const WorkspacesPage: React.FC = () => {
const styles = useStyles()
const router = useRouter()
const { me, signOut } = useUser(true)

const { workspace: workspaceQueryParam } = router.query

const { data: workspace, error: workspaceError } = useSWR<API.Workspace, Error>(() => {
const workspaceParam = firstOrItem(workspaceQueryParam, null)

return `/api/v2/workspaces/${workspaceParam}`
})

// Fetch parent project
const { data: project, error: projectError } = useSWR<API.Project, Error>(() => {
return `/api/v2/projects/${unsafeSWRArgument(workspace).project_id}`
})

const { data: organization, error: organizationError } = useSWR<API.Project, Error>(() => {
return `/api/v2/organizations/${unsafeSWRArgument(project).organization_id}`
})

if (workspaceError) {
return <ErrorSummary error={workspaceError} />
}

if (projectError) {
return <ErrorSummary error={projectError} />
}

if (organizationError) {
return <ErrorSummary error={organizationError} />
}

if (!me || !workspace || !project || !organization) {
return <FullScreenLoader />
}

return (
<div className={styles.root}>
<Navbar user={me} onSignOut={signOut} />

<div className={styles.inner}>
<Workspace organization={organization} project={project} workspace={workspace} />
</div>

<Footer />
</div>
)
}

const useStyles = makeStyles(() => ({
root: {
display: "flex",
flexDirection: "column",
},
inner: {
maxWidth: "1380px",
margin: "1em auto",
width: "100%",
},
}))

export default WorkspacesPage
2 changes: 2 additions & 0 deletions site/util/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./array"
export * from "./swr"
17 changes: 17 additions & 0 deletions site/util/swr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* unsafeSWRArgument
*
* Helper function for working with SWR / useSWR in the TypeScript world.
* TypeScript is helpful in enforcing type-safety, but SWR is designed to
* with the expectation that, if the argument is not available, an exception
* will be thrown.
*
* This just helps in abiding by those rules, explicitly, and lets us suppress
* the lint warning in a single place.
*/
export const unsafeSWRArgument = <T>(arg: T | null | undefined): T => {
if (typeof arg === "undefined" || arg === null) {
throw "SWR: Expected exception because the argument is not available"
}
return arg
}

0 comments on commit 9f19041

Please sign in to comment.