diff --git a/site/api.ts b/site/api.ts index e749e70c3d..0a11659ed2 100644 --- a/site/api.ts +++ b/site/api.ts @@ -67,6 +67,11 @@ export namespace Project { } } +export interface CreateWorkspaceRequest { + name: string + project_id: string +} + // Must be kept in sync with backend Workspace struct export interface Workspace { id: string @@ -77,6 +82,31 @@ export interface Workspace { name: string } +export namespace Workspace { + export const create = async (request: CreateWorkspaceRequest): Promise => { + const response = await fetch(`/api/v2/workspaces/me`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + }) + + const body = await response.json() + if (!response.ok) { + throw new Error(body.message) + } + + // Let SWR know that both the /api/v2/workspaces/* and /api/v2/projects/* + // endpoints will need to fetch new data. + const mutateWorkspacesPromise = mutate("/api/v2/workspaces") + const mutateProjectsPromise = mutate("/api/v2/projects") + await Promise.all([mutateWorkspacesPromise, mutateProjectsPromise]) + + return body + } +} + export const login = async (email: string, password: string): Promise => { const response = await fetch("/api/v2/login", { method: "POST", diff --git a/site/forms/CreateWorkspaceForm.test.tsx b/site/forms/CreateWorkspaceForm.test.tsx new file mode 100644 index 0000000000..09df65215c --- /dev/null +++ b/site/forms/CreateWorkspaceForm.test.tsx @@ -0,0 +1,20 @@ +import { render, screen } from "@testing-library/react" +import React from "react" +import { CreateWorkspaceForm } from "./CreateWorkspaceForm" +import { MockProject, MockWorkspace } from "./../test_helpers" + +describe("CreateWorkspaceForm", () => { + it("renders", async () => { + // Given + const onSubmit = () => Promise.resolve(MockWorkspace) + const onCancel = () => Promise.resolve() + + // When + render() + + // Then + // Simple smoke test to verify form renders + const element = await screen.findByText("Create Workspace") + expect(element).toBeDefined() + }) +}) diff --git a/site/forms/CreateWorkspaceForm.tsx b/site/forms/CreateWorkspaceForm.tsx new file mode 100644 index 0000000000..d8f70d03e0 --- /dev/null +++ b/site/forms/CreateWorkspaceForm.tsx @@ -0,0 +1,97 @@ +import Button from "@material-ui/core/Button" +import { makeStyles } from "@material-ui/core/styles" +import { FormikContextType, useFormik } from "formik" +import React from "react" +import * as Yup from "yup" + +import { FormTextField, FormTitle, FormSection } from "../components/Form" +import { LoadingButton } from "../components/Button" +import { Project, Workspace, CreateWorkspaceRequest } from "../api" + +export interface CreateWorkspaceForm { + project: Project + onSubmit: (request: CreateWorkspaceRequest) => Promise + onCancel: () => void +} + +const validationSchema = Yup.object({ + name: Yup.string().required("Name is required"), +}) + +export const CreateWorkspaceForm: React.FC = ({ project, onSubmit, onCancel }) => { + const styles = useStyles() + + const form: FormikContextType<{ name: string }> = useFormik<{ name: string }>({ + initialValues: { + name: "", + }, + enableReinitialize: true, + validationSchema: validationSchema, + onSubmit: ({ name }) => { + return onSubmit({ + project_id: project.id, + name: name, + }) + }, + }) + + return ( +
+ + for project {project.name} + + } + /> + + + + +
+ + + Submit + +
+
+ ) +} + +const useStyles = makeStyles(() => ({ + root: { + maxWidth: "1380px", + width: "100%", + display: "flex", + flexDirection: "column", + alignItems: "center", + }, + footer: { + display: "flex", + flex: "0", + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + }, + button: { + margin: "1em", + }, +})) diff --git a/site/pages/projects/[organization]/[project]/create.tsx b/site/pages/projects/[organization]/[project]/create.tsx new file mode 100644 index 0000000000..ce2c66508a --- /dev/null +++ b/site/pages/projects/[organization]/[project]/create.tsx @@ -0,0 +1,56 @@ +import React from "react" +import { makeStyles } from "@material-ui/core/styles" +import { useRouter } from "next/router" +import useSWR from "swr" + +import * as API from "../../../../api" +import { useUser } from "../../../../contexts/UserContext" +import { ErrorSummary } from "../../../../components/ErrorSummary" +import { FullScreenLoader } from "../../../../components/Loader/FullScreenLoader" +import { CreateWorkspaceForm } from "../../../../forms/CreateWorkspaceForm" + +const CreateWorkspacePage: React.FC = () => { + const router = useRouter() + const styles = useStyles() + const { me } = useUser(/* redirectOnError */ true) + const { organization, project: projectName } = router.query + const { data: project, error: projectError } = useSWR( + `/api/v2/projects/${organization}/${projectName}`, + ) + + if (projectError) { + return + } + + if (!me || !project) { + return + } + + const onCancel = async () => { + await router.push(`/projects/${organization}/${project}`) + } + + const onSubmit = async (req: API.CreateWorkspaceRequest) => { + const workspace = await API.Workspace.create(req) + await router.push(`/workspaces/${workspace.id}`) + return workspace + } + + return ( +
+ +
+ ) +} + +const useStyles = makeStyles((theme) => ({ + root: { + display: "flex", + flexDirection: "column", + alignItems: "center", + height: "100vh", + backgroundColor: theme.palette.background.paper, + }, +})) + +export default CreateWorkspacePage