From 2e875aae9b5a53e21a9cb316476607e4d6bb368c Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Wed, 26 Jan 2022 01:07:05 +0000 Subject: [PATCH 1/5] Implement simple projects page --- site/api.ts | 10 ++ .../projects/[organization]/[project].tsx | 119 ++++++++++++++++++ site/test_helpers/mocks.ts | 10 +- site/util/array.ts | 7 ++ 4 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 site/pages/projects/[organization]/[project].tsx create mode 100644 site/util/array.ts diff --git a/site/api.ts b/site/api.ts index f8c3ae330ebe8..e749e70c3dd7b 100644 --- a/site/api.ts +++ b/site/api.ts @@ -67,6 +67,16 @@ export namespace Project { } } +// Must be kept in sync with backend Workspace struct +export interface Workspace { + id: string + created_at: string + updated_at: string + owner_id: string + project_id: string + name: string +} + export const login = async (email: string, password: string): Promise => { const response = await fetch("/api/v2/login", { method: "POST", diff --git a/site/pages/projects/[organization]/[project].tsx b/site/pages/projects/[organization]/[project].tsx new file mode 100644 index 0000000000000..730ae3b08a00a --- /dev/null +++ b/site/pages/projects/[organization]/[project].tsx @@ -0,0 +1,119 @@ +import React from "react" +import Box from "@material-ui/core/Box" +import { makeStyles } from "@material-ui/core/styles" +import Paper from "@material-ui/core/Paper" +import Link from "next/link" +import { useRouter } from "next/router" +import useSWR from "swr" + +import { Project, Workspace } from "../../../api" +import { Header } from "../../../components/Header" +import { FullScreenLoader } from "../../../components/Loader/FullScreenLoader" +import { Navbar } from "../../../components/Navbar" +import { Footer } from "../../../components/Page" +import { Column, Table } from "../../../components/Table" +import { useUser } from "../../../contexts/UserContext" +import { ErrorSummary } from "../../../components/ErrorSummary" +import { firstOrItem } from "../../../util/array" +import { EmptyState } from "../../../components/EmptyState" + +import { MockWorkspace } from "../../../test_helpers" + +const ProjectPage: React.FC = () => { + const styles = useStyles() + const { me, signOut } = useUser(true) + + const router = useRouter() + const { project, organization } = router.query + + const { data: projectInfo, error: projectError } = useSWR( + () => `/api/v2/projects/${organization}/${project}`, + ) + let { data: workspaces, error: workspacesError } = useSWR( + () => `/api/v2/projects/${organization}/${project}/workspaces`, + ) + + workspaces = [MockWorkspace] + + if (projectError) { + return + } + + if (workspacesError) { + return + } + + if (!me || !projectInfo || !workspaces) { + return + } + + const createWorkspace = () => { + void router.push(`/projects/${organization}/${project}/create`) + } + + const emptyState = ( + + ) + + const columns: Column[] = [ + { + key: "name", + name: "Name", + renderer: (nameField: string, data: Workspace) => { + return {nameField} + }, + }, + ] + + const tableProps = { + title: "Workspaces", + columns, + data: workspaces, + emptyState: emptyState, + } + + return ( +
+ +
+ + + + +
+ + ) +} + +const useStyles = makeStyles((theme) => ({ + root: { + display: "flex", + flexDirection: "column", + }, + header: { + display: "flex", + flexDirection: "row-reverse", + justifyContent: "space-between", + margin: "1em auto", + maxWidth: "1380px", + padding: theme.spacing(2, 6.25, 0), + width: "100%", + }, +})) + +export default ProjectPage diff --git a/site/test_helpers/mocks.ts b/site/test_helpers/mocks.ts index f939fd021ef01..3af2a22bc75a8 100644 --- a/site/test_helpers/mocks.ts +++ b/site/test_helpers/mocks.ts @@ -1,5 +1,5 @@ import { User } from "../contexts/UserContext" -import { Provisioner, Organization, Project } from "../api" +import { Provisioner, Organization, Project, Workspace } from "../api" export const MockUser: User = { id: "test-user-id", @@ -29,3 +29,11 @@ export const MockOrganization: Organization = { created_at: "", updated_at: "", } + +export const MockWorkspace: Workspace = { + id: "test-workspace", + name: "Test-Workspace", + created_at: "", + updated_at: "", + project_id: "project-id", +} diff --git a/site/util/array.ts b/site/util/array.ts new file mode 100644 index 0000000000000..7f2e55f8e40ba --- /dev/null +++ b/site/util/array.ts @@ -0,0 +1,7 @@ +export const firstOrItem = (itemOrItems: T | T[]): T | null => { + if (Array.isArray(itemOrItems)) { + return itemOrItems.length > 0 ? itemOrItems[0] : null + } + + return itemOrItems +} From c158fcbe5e5eb03083c7627ada3c99ae2eced233 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Wed, 26 Jan 2022 01:11:16 +0000 Subject: [PATCH 2/5] Redirect to project page when created --- site/pages/projects/[organization]/[project].tsx | 4 +--- site/pages/projects/create.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/site/pages/projects/[organization]/[project].tsx b/site/pages/projects/[organization]/[project].tsx index 730ae3b08a00a..15ca5b2d271c5 100644 --- a/site/pages/projects/[organization]/[project].tsx +++ b/site/pages/projects/[organization]/[project].tsx @@ -29,12 +29,10 @@ const ProjectPage: React.FC = () => { const { data: projectInfo, error: projectError } = useSWR( () => `/api/v2/projects/${organization}/${project}`, ) - let { data: workspaces, error: workspacesError } = useSWR( + const { data: workspaces, error: workspacesError } = useSWR( () => `/api/v2/projects/${organization}/${project}/workspaces`, ) - workspaces = [MockWorkspace] - if (projectError) { return } diff --git a/site/pages/projects/create.tsx b/site/pages/projects/create.tsx index 66adad4542ee1..ac49d29b6cecc 100644 --- a/site/pages/projects/create.tsx +++ b/site/pages/projects/create.tsx @@ -29,7 +29,7 @@ const CreateProjectPage: React.FC = () => { const onSubmit = async (req: API.CreateProjectRequest) => { const project = await API.Project.create(req) - await router.push("/projects") + await router.push(`/projects/${req.organizationId}/${project.name}`) return project } From 87b8af0626c46e346473cdccded861ce675df32c Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Wed, 26 Jan 2022 01:19:36 +0000 Subject: [PATCH 3/5] Fix up navigation --- site/pages/projects/index.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/site/pages/projects/index.tsx b/site/pages/projects/index.tsx index 354e3530ad448..42df98f666ddf 100644 --- a/site/pages/projects/index.tsx +++ b/site/pages/projects/index.tsx @@ -12,7 +12,7 @@ import { Column, Table } from "../../components/Table" import { useUser } from "../../contexts/UserContext" import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" -import { Project } from "./../../api" +import { Organization, Project } from "./../../api" import useSWR from "swr" const ProjectsPage: React.FC = () => { @@ -20,6 +20,7 @@ const ProjectsPage: React.FC = () => { const router = useRouter() const { me, signOut } = useUser(true) const { data, error } = useSWR("/api/v2/projects") + const { data: orgs, error: orgsError } = useSWR("/api/v2/users/me/organizations") // TODO: The API call is currently returning `null`, which isn't ideal // - it breaks checking for data presence with SWR. @@ -29,7 +30,11 @@ const ProjectsPage: React.FC = () => { return } - if (!me || !projects) { + if (orgsError) { + return + } + + if (!me || !projects || !orgs) { return } @@ -42,12 +47,21 @@ const ProjectsPage: React.FC = () => { onClick: createProject, } + // Create a dictionary of organization ID -> organization Name + // Needed to properly construct links to dive into a project + const orgDictionary = orgs.reduce((acc: Record, curr: Organization) => { + return { + ...acc, + [curr.id]: curr.name, + } + }, {}) + const columns: Column[] = [ { key: "name", name: "Name", renderer: (nameField: string, data: Project) => { - return {nameField} + return {nameField} }, }, ] From 554ade334470515d2b6847b7d774192777d6d17a Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Wed, 26 Jan 2022 01:24:38 +0000 Subject: [PATCH 4/5] Move to subfolder, so we can add create form --- .../{[project].tsx => [project]/index.tsx} | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) rename site/pages/projects/[organization]/{[project].tsx => [project]/index.tsx} (78%) diff --git a/site/pages/projects/[organization]/[project].tsx b/site/pages/projects/[organization]/[project]/index.tsx similarity index 78% rename from site/pages/projects/[organization]/[project].tsx rename to site/pages/projects/[organization]/[project]/index.tsx index 15ca5b2d271c5..c011798f50ffd 100644 --- a/site/pages/projects/[organization]/[project].tsx +++ b/site/pages/projects/[organization]/[project]/index.tsx @@ -1,23 +1,20 @@ import React from "react" -import Box from "@material-ui/core/Box" import { makeStyles } from "@material-ui/core/styles" import Paper from "@material-ui/core/Paper" import Link from "next/link" import { useRouter } from "next/router" import useSWR from "swr" -import { Project, Workspace } from "../../../api" -import { Header } from "../../../components/Header" -import { FullScreenLoader } from "../../../components/Loader/FullScreenLoader" -import { Navbar } from "../../../components/Navbar" -import { Footer } from "../../../components/Page" -import { Column, Table } from "../../../components/Table" -import { useUser } from "../../../contexts/UserContext" -import { ErrorSummary } from "../../../components/ErrorSummary" -import { firstOrItem } from "../../../util/array" -import { EmptyState } from "../../../components/EmptyState" - -import { MockWorkspace } from "../../../test_helpers" +import { Project, Workspace } from "../../../../api" +import { Header } from "../../../../components/Header" +import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader" +import { Navbar } from "../../../../components/Navbar" +import { Footer } from "../../../../components/Page" +import { Column, Table } from "../../../../components/Table" +import { useUser } from "../../../../contexts/UserContext" +import { ErrorSummary } from "../../../../components/ErrorSummary" +import { firstOrItem } from "../../../../util/array" +import { EmptyState } from "../../../../components/EmptyState" const ProjectPage: React.FC = () => { const styles = useStyles() @@ -65,7 +62,7 @@ const ProjectPage: React.FC = () => { key: "name", name: "Name", renderer: (nameField: string, data: Workspace) => { - return {nameField} + return {nameField} }, }, ] From 3f556ee3f2332c144149dbfcd5d23aae70645208 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Wed, 26 Jan 2022 01:29:35 +0000 Subject: [PATCH 5/5] Add simple test cases --- site/test_helpers/mocks.ts | 1 + site/util/array.test.ts | 17 +++++++++++++++++ site/util/array.ts | 6 ++++++ 3 files changed, 24 insertions(+) create mode 100644 site/util/array.test.ts diff --git a/site/test_helpers/mocks.ts b/site/test_helpers/mocks.ts index 3af2a22bc75a8..081292b2d663e 100644 --- a/site/test_helpers/mocks.ts +++ b/site/test_helpers/mocks.ts @@ -36,4 +36,5 @@ export const MockWorkspace: Workspace = { created_at: "", updated_at: "", project_id: "project-id", + owner_id: "test-user-id", } diff --git a/site/util/array.test.ts b/site/util/array.test.ts new file mode 100644 index 0000000000000..e8e78074d1494 --- /dev/null +++ b/site/util/array.test.ts @@ -0,0 +1,17 @@ +import { firstOrItem } from "./array" + +describe("array", () => { + describe("firstOrItem", () => { + it("returns null if empty array", () => { + expect(firstOrItem([])).toBeNull() + }) + + it("returns first item if array with more one item", () => { + expect(firstOrItem(["a", "b"])).toEqual("a") + }) + + it("returns item if single item", () => { + expect(firstOrItem("c")).toEqual("c") + }) + }) +}) diff --git a/site/util/array.ts b/site/util/array.ts index 7f2e55f8e40ba..6be0442bc4ac4 100644 --- a/site/util/array.ts +++ b/site/util/array.ts @@ -1,3 +1,9 @@ +/** + * Helper function that, given an array or a single item: + * - If an array with no elements, returns null + * - If an array with 1 or more elements, returns the first element + * - If a single item, returns that item + */ export const firstOrItem = (itemOrItems: T | T[]): T | null => { if (Array.isArray(itemOrItems)) { return itemOrItems.length > 0 ? itemOrItems[0] : null