diff --git a/site/api.ts b/site/api.ts index febeee6c96dd1..179bc1ce9abf9 100644 --- a/site/api.ts +++ b/site/api.ts @@ -84,7 +84,7 @@ export interface Workspace { export namespace Workspace { export const create = async (request: CreateWorkspaceRequest): Promise => { - const response = await fetch(`/api/v2/workspaces/me`, { + const response = await fetch(`/api/v2/users/me/workspaces`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/site/components/Workspace/Workspace.test.tsx b/site/components/Workspace/Workspace.test.tsx index 245455e729da1..d0a2279c5801b 100644 --- a/site/components/Workspace/Workspace.test.tsx +++ b/site/components/Workspace/Workspace.test.tsx @@ -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() + render() // Then const element = await screen.findByText(MockWorkspace.name) diff --git a/site/components/Workspace/Workspace.tsx b/site/components/Workspace/Workspace.tsx index f7e22fbef7b49..68c64a467a387 100644 --- a/site/components/Workspace/Workspace.tsx +++ b/site/components/Workspace/Workspace.tsx @@ -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 = ({ workspace }) => { +export const Workspace: React.FC = ({ organization, project, workspace }) => { const styles = useStyles() return (
- +
@@ -54,9 +56,11 @@ export const Workspace: React.FC = ({ workspace }) => { /** * Component for the header at the top of the workspace page */ -export const WorkspaceHeader: React.FC = ({ workspace }) => { +export const WorkspaceHeader: React.FC = ({ organization, project, workspace }) => { const styles = useStyles() + const projectLink = `/projects/${organization.name}/${project.name}` + return (
@@ -64,7 +68,7 @@ export const WorkspaceHeader: React.FC = ({ workspace }) => {
{workspace.name} - {workspace.project_id} + {project.name}
diff --git a/site/pages/projects/[organization]/[project]/create.tsx b/site/pages/projects/[organization]/[project]/create.tsx index 8f78206801055..8baa5b0c295da 100644 --- a/site/pages/projects/[organization]/[project]/create.tsx +++ b/site/pages/projects/[organization]/[project]/create.tsx @@ -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/v2/projects/${organization}/${projectName}`, + const { organization: organizationName, project: projectName } = query + + const { data: organizationInfo, error: organizationError } = useSWR( + () => `/api/v2/users/me/organizations/${organizationName}`, ) + const { data: project, error: projectError } = useSWR(() => { + 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 + } + if (projectError) { return } diff --git a/site/pages/projects/[organization]/[project]/index.tsx b/site/pages/projects/[organization]/[project]/index.tsx index 3edd8a1cf68d5..23c36dedf3d54 100644 --- a/site/pages/projects/[organization]/[project]/index.tsx +++ b/site/pages/projects/[organization]/[project]/index.tsx @@ -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: projectInfo, error: projectError } = useSWR( - () => `/api/v2/projects/${organization}/${project}`, + const { data: organizationInfo, error: organizationError } = useSWR( + () => `/api/v2/users/me/organizations/${organizationName}`, ) - const { data: workspaces, error: workspacesError } = useSWR( - () => `/api/v2/projects/${organization}/${project}/workspaces`, + + const { data: projectInfo, error: projectError } = useSWR( + () => `/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(() => `/api/v2/users/me/workspaces`) + + if (organizationError) { + return + } + if (projectError) { return } @@ -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 {nameField} + renderer: (nameField: string, workspace: Workspace) => { + return {nameField} }, }, ] + 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 = () => {
{ - const styles = useStyles() - const router = useRouter() - const { me, signOut } = useUser(true) - - const { user: userQueryParam, workspace: workspaceQueryParam } = router.query - - const { data: workspace, error: workspaceError } = useSWR(() => { - 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 - } - - if (!me || !workspace) { - return - } - - return ( -
- - -
- -
- -
-
- ) -} - -const useStyles = makeStyles(() => ({ - root: { - display: "flex", - flexDirection: "column", - }, - inner: { - maxWidth: "1380px", - margin: "1em auto", - width: "100%", - }, -})) - -export default WorkspacesPage diff --git a/site/pages/workspaces/[workspace].tsx b/site/pages/workspaces/[workspace].tsx new file mode 100644 index 0000000000000..a4c6f48197c1b --- /dev/null +++ b/site/pages/workspaces/[workspace].tsx @@ -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(() => { + const workspaceParam = firstOrItem(workspaceQueryParam, null) + + return `/api/v2/workspaces/${workspaceParam}` + }) + + // Fetch parent project + const { data: project, error: projectError } = useSWR(() => { + return `/api/v2/projects/${unsafeSWRArgument(workspace).project_id}` + }) + + const { data: organization, error: organizationError } = useSWR(() => { + return `/api/v2/organizations/${unsafeSWRArgument(project).organization_id}` + }) + + if (workspaceError) { + return + } + + if (projectError) { + return + } + + if (organizationError) { + return + } + + if (!me || !workspace || !project || !organization) { + return + } + + return ( +
+ + +
+ +
+ +
+
+ ) +} + +const useStyles = makeStyles(() => ({ + root: { + display: "flex", + flexDirection: "column", + }, + inner: { + maxWidth: "1380px", + margin: "1em auto", + width: "100%", + }, +})) + +export default WorkspacesPage diff --git a/site/util/index.ts b/site/util/index.ts new file mode 100644 index 0000000000000..b492d0c2d17e1 --- /dev/null +++ b/site/util/index.ts @@ -0,0 +1,2 @@ +export * from "./array" +export * from "./swr" diff --git a/site/util/swr.ts b/site/util/swr.ts new file mode 100644 index 0000000000000..7936b270753f7 --- /dev/null +++ b/site/util/swr.ts @@ -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 = (arg: T | null | undefined): T => { + if (typeof arg === "undefined" || arg === null) { + throw "SWR: Expected exception because the argument is not available" + } + return arg +}