mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: Update routes for project page, workspace creation page, and workspace page (#415)
Some API routes were updated in https://github.com/coder/coder/pull/401, which impacted the UX - the flow is currently broken when trying to navigate to a project:  This fixes all the routes so that the complete project -> create workspace -> workspace page flow works:  Because this had to touch a bunch of UI routes, I also opportunistically fixed #380 as part of this change. Fixes #380
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
@@ -54,9 +56,11 @@ 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}>
|
||||
@@ -64,7 +68,7 @@ export const WorkspaceHeader: React.FC<WorkspaceProps> = ({ workspace }) => {
|
||||
<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>
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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: organizationInfo, error: organizationError } = useSWR<Organization, Error>(
|
||||
() => `/api/v2/users/me/organizations/${organizationName}`,
|
||||
)
|
||||
|
||||
const { data: projectInfo, error: projectError } = useSWR<Project, Error>(
|
||||
() => `/api/v2/projects/${organization}/${project}`,
|
||||
)
|
||||
const { data: workspaces, error: workspacesError } = useSWR<Workspace[], Error>(
|
||||
() => `/api/v2/projects/${organization}/${project}/workspaces`,
|
||||
() => `/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} />
|
||||
}
|
||||
@@ -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 = (
|
||||
@@ -61,16 +72,20 @@ 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,
|
||||
}
|
||||
|
||||
@@ -78,9 +93,9 @@ const ProjectPage: React.FC = () => {
|
||||
<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,
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
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:
|
||||
if (normalizedUserParam === null || workspaceParam === null) {
|
||||
throw "Data not yet available to make API call"
|
||||
}
|
||||
|
||||
return `/api/v2/workspaces/${normalizedUserParam}/${workspaceParam}`
|
||||
})
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./array"
|
||||
export * from "./swr"
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user