mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: Add Create Workspace Form (#73)
Fixes #38 This adds a create-workspace form (with only 1 field, probably the simplest form ever 😄 )  It currently redirects to a path `/workspaces/<unique id>`, but that isn't implemented yet - but you can see the workspace show up on the projects page.
This commit is contained in:
30
site/api.ts
30
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
|
// Must be kept in sync with backend Workspace struct
|
||||||
export interface Workspace {
|
export interface Workspace {
|
||||||
id: string
|
id: string
|
||||||
@ -77,6 +82,31 @@ export interface Workspace {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export namespace Workspace {
|
||||||
|
export const create = async (request: CreateWorkspaceRequest): Promise<Workspace> => {
|
||||||
|
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<LoginResponse> => {
|
export const login = async (email: string, password: string): Promise<LoginResponse> => {
|
||||||
const response = await fetch("/api/v2/login", {
|
const response = await fetch("/api/v2/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
20
site/forms/CreateWorkspaceForm.test.tsx
Normal file
20
site/forms/CreateWorkspaceForm.test.tsx
Normal file
@ -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(<CreateWorkspaceForm project={MockProject} onSubmit={onSubmit} onCancel={onCancel} />)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
// Simple smoke test to verify form renders
|
||||||
|
const element = await screen.findByText("Create Workspace")
|
||||||
|
expect(element).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
97
site/forms/CreateWorkspaceForm.tsx
Normal file
97
site/forms/CreateWorkspaceForm.tsx
Normal file
@ -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<Workspace>
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationSchema = Yup.object({
|
||||||
|
name: Yup.string().required("Name is required"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CreateWorkspaceForm: React.FC<CreateWorkspaceForm> = ({ 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 (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<FormTitle
|
||||||
|
title="Create Workspace"
|
||||||
|
detail={
|
||||||
|
<span>
|
||||||
|
for project <strong>{project.name}</strong>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormSection title="Name">
|
||||||
|
<FormTextField
|
||||||
|
form={form}
|
||||||
|
formFieldName="name"
|
||||||
|
fullWidth
|
||||||
|
helperText="A unique name describing your workspace."
|
||||||
|
label="Workspace Name"
|
||||||
|
placeholder="my-workspace"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<Button className={styles.button} onClick={onCancel} variant="outlined">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<LoadingButton
|
||||||
|
loading={form.isSubmitting}
|
||||||
|
className={styles.button}
|
||||||
|
onClick={form.submitForm}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</LoadingButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
}))
|
56
site/pages/projects/[organization]/[project]/create.tsx
Normal file
56
site/pages/projects/[organization]/[project]/create.tsx
Normal file
@ -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.Project, Error>(
|
||||||
|
`/api/v2/projects/${organization}/${projectName}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (projectError) {
|
||||||
|
return <ErrorSummary error={projectError} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!me || !project) {
|
||||||
|
return <FullScreenLoader />
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<CreateWorkspaceForm onCancel={onCancel} onSubmit={onSubmit} project={project} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "100vh",
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default CreateWorkspacePage
|
Reference in New Issue
Block a user