Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c7fb16e

Browse files
authored
feat: Initial Project Create Form ('/projects/create') (#60)
This implements a simple form for creating projects: ![2022-01-25 12 58 21](https://user-images.githubusercontent.com/88213859/151058767-be3672f6-e100-48c8-849e-cc6de94f3ebf.gif) Fixes #65
1 parent bbd8b8f commit c7fb16e

File tree

12 files changed

+451
-10
lines changed

12 files changed

+451
-10
lines changed

site/api.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,35 @@
1+
import { mutate } from "swr"
2+
13
interface LoginResponse {
24
session_token: string
35
}
46

7+
/**
8+
* `Organization` must be kept in sync with the go struct in organizations.go
9+
*/
10+
export interface Organization {
11+
id: string
12+
name: string
13+
created_at: string
14+
updated_at: string
15+
}
16+
17+
export interface Provisioner {
18+
id: string
19+
name: string
20+
}
21+
22+
export const provisioners: Provisioner[] = [
23+
{
24+
id: "terraform",
25+
name: "Terraform",
26+
},
27+
{
28+
id: "cdr-basic",
29+
name: "Basic",
30+
},
31+
]
32+
533
// This must be kept in sync with the `Project` struct in the back-end
634
export interface Project {
735
id: string
@@ -13,6 +41,32 @@ export interface Project {
1341
active_version_id: string
1442
}
1543

44+
export interface CreateProjectRequest {
45+
name: string
46+
organizationId: string
47+
provisioner: string
48+
}
49+
50+
export namespace Project {
51+
export const create = async (request: CreateProjectRequest): Promise<Project> => {
52+
const response = await fetch(`/api/v2/projects/${request.organizationId}/`, {
53+
method: "POST",
54+
headers: {
55+
"Content-Type": "application/json",
56+
},
57+
body: JSON.stringify(request),
58+
})
59+
60+
const body = await response.json()
61+
await mutate("/api/v2/projects")
62+
if (!response.ok) {
63+
throw new Error(body.message)
64+
}
65+
66+
return body
67+
}
68+
}
69+
1670
export const login = async (email: string, password: string): Promise<LoginResponse> => {
1771
const response = await fetch("/api/v2/login", {
1872
method: "POST",
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Box from "@material-ui/core/Box"
2+
import MenuItem from "@material-ui/core/MenuItem"
3+
import { makeStyles } from "@material-ui/core/styles"
4+
import Typography from "@material-ui/core/Typography"
5+
import React from "react"
6+
7+
import { FormTextField, FormTextFieldProps } from "./FormTextField"
8+
9+
export interface DropdownItem {
10+
value: string
11+
name: string
12+
description?: string
13+
}
14+
15+
export interface FormDropdownFieldProps<T> extends FormTextFieldProps<T> {
16+
items: DropdownItem[]
17+
}
18+
19+
export const FormDropdownField = <T,>({ items, ...props }: FormDropdownFieldProps<T>): React.ReactElement => {
20+
const styles = useStyles()
21+
return (
22+
<FormTextField select {...props}>
23+
{items.map((item: DropdownItem) => (
24+
<MenuItem key={item.value} value={item.value}>
25+
<Box alignItems="center" display="flex">
26+
<Box ml={1}>
27+
<Typography>{item.name}</Typography>
28+
</Box>
29+
{item.description && (
30+
<Box ml={1}>
31+
<Typography className={styles.hintText} variant="caption">
32+
{item.description}
33+
</Typography>
34+
</Box>
35+
)}
36+
</Box>
37+
</MenuItem>
38+
))}
39+
</FormTextField>
40+
)
41+
}
42+
43+
const useStyles = makeStyles({
44+
hintText: {
45+
opacity: 0.75,
46+
},
47+
})

site/components/Form/FormSection.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { makeStyles } from "@material-ui/core/styles"
2+
import Typography from "@material-ui/core/Typography"
3+
import React from "react"
4+
5+
export interface FormSectionProps {
6+
title: string
7+
description?: string
8+
}
9+
10+
export const useStyles = makeStyles((theme) => ({
11+
root: {
12+
display: "flex",
13+
flexDirection: "row",
14+
// Borrowed from PaperForm styles
15+
maxWidth: "852px",
16+
width: "100%",
17+
borderBottom: `1px solid ${theme.palette.divider}`,
18+
},
19+
descriptionContainer: {
20+
maxWidth: "200px",
21+
flex: "0 0 200px",
22+
display: "flex",
23+
flexDirection: "column",
24+
justifyContent: "flex-start",
25+
alignItems: "flex-start",
26+
marginTop: theme.spacing(5),
27+
marginBottom: theme.spacing(2),
28+
},
29+
descriptionText: {
30+
fontSize: "0.9em",
31+
lineHeight: "1em",
32+
color: theme.palette.text.secondary,
33+
marginTop: theme.spacing(1),
34+
},
35+
contents: {
36+
flex: 1,
37+
marginTop: theme.spacing(4),
38+
marginBottom: theme.spacing(4),
39+
},
40+
}))
41+
42+
export const FormSection: React.FC<FormSectionProps> = ({ title, description, children }) => {
43+
const styles = useStyles()
44+
45+
return (
46+
<div className={styles.root}>
47+
<div className={styles.descriptionContainer}>
48+
<Typography variant="h5" color="textPrimary">
49+
{title}
50+
</Typography>
51+
{description && (
52+
<Typography className={styles.descriptionText} variant="body2" color="textSecondary">
53+
{description}
54+
</Typography>
55+
)}
56+
</div>
57+
<div className={styles.contents}>{children}</div>
58+
</div>
59+
)
60+
}

site/components/Form/FormTitle.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { makeStyles } from "@material-ui/core/styles"
2+
import Typography from "@material-ui/core/Typography"
3+
import React from "react"
4+
5+
export interface FormTitleProps {
6+
title: string
7+
detail?: React.ReactNode
8+
}
9+
10+
const useStyles = makeStyles((theme) => ({
11+
title: {
12+
textAlign: "center",
13+
marginTop: theme.spacing(5),
14+
marginBottom: theme.spacing(5),
15+
16+
"& h3": {
17+
marginBottom: theme.spacing(1),
18+
},
19+
},
20+
}))
21+
22+
export const FormTitle: React.FC<FormTitleProps> = ({ title, detail }) => {
23+
const styles = useStyles()
24+
25+
return (
26+
<div className={styles.title}>
27+
<Typography variant="h3">{title}</Typography>
28+
{detail && <Typography variant="caption">{detail}</Typography>}
29+
</div>
30+
)
31+
}

site/components/Form/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from "./FormSection"
2+
export * from "./FormDropdownField"
3+
export * from "./FormTextField"
4+
export * from "./FormTitle"

site/components/Form/index.tsx

Lines changed: 0 additions & 1 deletion
This file was deleted.

site/forms/CreateProjectForm.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { render, screen } from "@testing-library/react"
2+
import React from "react"
3+
import { CreateProjectForm } from "./CreateProjectForm"
4+
import { MockProvisioner, MockOrganization, MockProject } from "./../test_helpers"
5+
6+
describe("CreateProjectForm", () => {
7+
it("renders", async () => {
8+
// Given
9+
const provisioners = [MockProvisioner]
10+
const organizations = [MockOrganization]
11+
const onSubmit = () => Promise.resolve(MockProject)
12+
const onCancel = () => Promise.resolve()
13+
14+
// When
15+
render(
16+
<CreateProjectForm
17+
provisioners={provisioners}
18+
organizations={organizations}
19+
onSubmit={onSubmit}
20+
onCancel={onCancel}
21+
/>,
22+
)
23+
24+
// Then
25+
// Simple smoke test to verify form renders
26+
const element = await screen.findByText("Create Project")
27+
expect(element).toBeDefined()
28+
})
29+
})

site/forms/CreateProjectForm.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import Button from "@material-ui/core/Button"
2+
import { makeStyles } from "@material-ui/core/styles"
3+
import { FormikContextType, useFormik } from "formik"
4+
import React from "react"
5+
import * as Yup from "yup"
6+
7+
import { DropdownItem, FormDropdownField, FormTextField, FormTitle, FormSection } from "../components/Form"
8+
import { LoadingButton } from "../components/Button"
9+
import { Organization, Project, Provisioner, CreateProjectRequest } from "./../api"
10+
11+
export interface CreateProjectFormProps {
12+
provisioners: Provisioner[]
13+
organizations: Organization[]
14+
onSubmit: (request: CreateProjectRequest) => Promise<Project>
15+
onCancel: () => void
16+
}
17+
18+
const validationSchema = Yup.object({
19+
provisioner: Yup.string().required("Provisioner is required."),
20+
organizationId: Yup.string().required("Organization is required."),
21+
name: Yup.string().required("Name is required"),
22+
})
23+
24+
export const CreateProjectForm: React.FC<CreateProjectFormProps> = ({
25+
provisioners,
26+
organizations,
27+
onSubmit,
28+
onCancel,
29+
}) => {
30+
const styles = useStyles()
31+
32+
const form: FormikContextType<CreateProjectRequest> = useFormik<CreateProjectRequest>({
33+
initialValues: {
34+
provisioner: provisioners[0].id,
35+
organizationId: organizations[0].name,
36+
name: "",
37+
},
38+
enableReinitialize: true,
39+
validationSchema: validationSchema,
40+
onSubmit: (req) => {
41+
return onSubmit(req)
42+
},
43+
})
44+
45+
const organizationDropDownItems: DropdownItem[] = organizations.map((org) => {
46+
return {
47+
value: org.name,
48+
name: org.name,
49+
}
50+
})
51+
52+
const provisionerDropDownItems: DropdownItem[] = provisioners.map((provisioner) => {
53+
return {
54+
value: provisioner.id,
55+
name: provisioner.name,
56+
}
57+
})
58+
59+
return (
60+
<div className={styles.root}>
61+
<FormTitle title="Create Project" />
62+
63+
<FormSection title="Name">
64+
<FormTextField
65+
form={form}
66+
formFieldName="name"
67+
fullWidth
68+
helperText="A unique name describing your project."
69+
label="Project Name"
70+
placeholder="my-project"
71+
required
72+
/>
73+
</FormSection>
74+
75+
<FormSection title="Organization">
76+
<FormDropdownField
77+
form={form}
78+
formFieldName="organizationId"
79+
helperText="The organization owning this project."
80+
items={organizationDropDownItems}
81+
fullWidth
82+
select
83+
required
84+
/>
85+
</FormSection>
86+
87+
<FormSection title="Provider">
88+
<FormDropdownField
89+
form={form}
90+
formFieldName="provisioner"
91+
helperText="The backing provisioner for this project."
92+
items={provisionerDropDownItems}
93+
fullWidth
94+
select
95+
required
96+
/>
97+
</FormSection>
98+
99+
<div className={styles.footer}>
100+
<Button className={styles.button} onClick={onCancel} variant="outlined">
101+
Cancel
102+
</Button>
103+
<LoadingButton
104+
loading={form.isSubmitting}
105+
className={styles.button}
106+
onClick={form.submitForm}
107+
variant="contained"
108+
color="primary"
109+
type="submit"
110+
>
111+
Submit
112+
</LoadingButton>
113+
</div>
114+
</div>
115+
)
116+
}
117+
118+
const useStyles = makeStyles(() => ({
119+
root: {
120+
maxWidth: "1380px",
121+
width: "100%",
122+
display: "flex",
123+
flexDirection: "column",
124+
alignItems: "center",
125+
},
126+
footer: {
127+
display: "flex",
128+
flex: "0",
129+
flexDirection: "row",
130+
justifyContent: "center",
131+
alignItems: "center",
132+
},
133+
button: {
134+
margin: "1em",
135+
},
136+
}))

0 commit comments

Comments
 (0)