diff --git a/package.json b/package.json index 6ae8df91d9cde..1db677026856b 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "@material-ui/core": "4.9.4", "@material-ui/icons": "4.5.1", "@material-ui/lab": "4.0.0-alpha.42", + "@testing-library/jest-dom": "5.16.1", "@testing-library/react": "12.1.2", + "@testing-library/user-event": "13.5.0", "@types/express": "4.17.13", "@types/jest": "27.4.0", "@types/node": "14.18.4", diff --git a/site/api.ts b/site/api.ts index 82d3303d45988..bd3411a9be501 100644 --- a/site/api.ts +++ b/site/api.ts @@ -1,3 +1,108 @@ +import { wait } from "./util" + +// TEMPORARY +// This is all placeholder / stub code until we have a real API to work with! +// +// The implementations below that are hard-coded will switch to using `fetch` +// once the routes are available. +// TEMPORARY + +export type ProjectParameterType = "string" | "number" + +export interface ProjectParameter { + id: string + name: string + description: string + defaultValue?: string + type: ProjectParameterType +} + +export interface Project { + id: string + icon?: string + name: string + description: string + parameters: ProjectParameter[] +} + +export namespace Project { + const testProject1: Project = { + id: "test-terraform-1", + icon: "https://www.datocms-assets.com/2885/1620155117-brandhcterraformverticalcolorwhite.svg", + name: "Terraform Project 1", + description: "Simple terraform project that deploys a kubernetes provider", + parameters: [ + { + id: "namespace", + name: "Namespace", + description: "The kubernetes namespace that will own the workspace pod.", + defaultValue: "default", + type: "string", + }, + { + id: "cpu_cores", + name: "CPU Cores", + description: "Number of CPU cores to allocate for pod.", + type: "number", + }, + ], + } + + const testProject2: Project = { + id: "test-echo-1", + name: "Echo Project", + description: "Project built on echo provisioner", + parameters: [ + { + id: "echo_string", + name: "Echo string", + description: "String that will be echoed out during build.", + type: "string", + }, + ], + } + + const allProjects = [testProject1, testProject2] + + export const getAllProjectsInOrg = async (_org: string): Promise => { + await wait(250) + return allProjects + } + + export const getProject = async (_org: string, projectId: string): Promise => { + await wait(250) + + const matchingProjects = allProjects.filter((p) => p.id === projectId) + + if (matchingProjects.length === 0) { + throw new Error(`No project matching ${projectId} found`) + } + + return matchingProjects[0] + } + + export const createWorkspace = async (name: string): Promise => { + await wait(250) + return name + } +} + +export namespace Workspace { + export type WorkspaceId = string + + export const createWorkspace = ( + name: string, + projectTemplate: string, + parameters: Record, + ): Promise => { + alert( + `Creating workspace named ${name} for project ${projectTemplate} with parameters: ${JSON.stringify(parameters)}`, + ) + + return Promise.resolve("test-workspace") + } +} + interface LoginResponse { session_token: string } @@ -20,4 +125,4 @@ export const login = async (email: string, password: string): Promise ({ + row: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + }, +})) + +export const FormRow: React.FC = ({ children }) => { + const styles = useStyles() + return
{children}
+} diff --git a/site/components/Form/FormSection.tsx b/site/components/Form/FormSection.tsx new file mode 100644 index 0000000000000..ff82819163974 --- /dev/null +++ b/site/components/Form/FormSection.tsx @@ -0,0 +1,60 @@ +import { makeStyles } from "@material-ui/core/styles" +import Typography from "@material-ui/core/Typography" +import React from "react" + +export interface FormSectionProps { + title: string + description?: string +} + +export const useStyles = makeStyles((theme) => ({ + root: { + display: "flex", + flexDirection: "row", + // Borrowed from PaperForm styles + maxWidth: "852px", + width: "100%", + borderBottom: `1px solid ${theme.palette.divider}`, + }, + descriptionContainer: { + maxWidth: "200px", + flex: "0 0 200px", + display: "flex", + flexDirection: "column", + justifyContent: "flex-start", + alignItems: "flex-start", + marginTop: theme.spacing(5), + marginBottom: theme.spacing(2), + }, + descriptionText: { + fontSize: "0.9em", + lineHeight: "1em", + color: theme.palette.text.secondary, + marginTop: theme.spacing(1), + }, + contents: { + flex: 1, + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + }, +})) + +export const FormSection: React.FC = ({ title, description, children }) => { + const styles = useStyles() + + return ( +
+
+ + {title} + + {description && ( + + {description} + + )} +
+
{children}
+
+ ) +} diff --git a/site/components/Form/FormTextField.tsx b/site/components/Form/FormTextField.tsx index db76173846a67..f80308f0b001c 100644 --- a/site/components/Form/FormTextField.tsx +++ b/site/components/Form/FormTextField.tsx @@ -1,7 +1,7 @@ import TextField, { TextFieldProps } from "@material-ui/core/TextField" +import { FormikLike } from "../../util/formik" import React from "react" import { PasswordField } from "./PasswordField" -import { FormikContextType } from "formik" /** * FormFieldProps are required props for creating form fields using a factory. @@ -11,7 +11,7 @@ export interface FormFieldProps { * form is a reference to a form or subform and is used to compute common * states such as error and helper text */ - form: FormikContextType + form: FormikLike /** * formFieldName is a field name associated with the form schema. */ @@ -26,31 +26,31 @@ export interface FormFieldProps { */ export interface FormTextFieldProps extends Pick< - TextFieldProps, - | "autoComplete" - | "autoFocus" - | "children" - | "className" - | "disabled" - | "fullWidth" - | "helperText" - | "id" - | "InputLabelProps" - | "InputProps" - | "inputProps" - | "label" - | "margin" - | "multiline" - | "onChange" - | "placeholder" - | "required" - | "rows" - | "select" - | "SelectProps" - | "style" - | "type" - >, - FormFieldProps { + TextFieldProps, + | "autoComplete" + | "autoFocus" + | "children" + | "className" + | "disabled" + | "fullWidth" + | "helperText" + | "id" + | "InputLabelProps" + | "InputProps" + | "inputProps" + | "label" + | "margin" + | "multiline" + | "onChange" + | "placeholder" + | "required" + | "rows" + | "select" + | "SelectProps" + | "style" + | "type" + >, + FormFieldProps { /** * eventTransform is an optional transformer on the event data before it is * processed by formik. @@ -124,7 +124,7 @@ export const formTextFieldFactory = (): React.FC> => { // Conversion to a string primitive is necessary as formFieldName is an in // indexable type such as a string, number or enum. - const fieldId = String(formFieldName) + const fieldId = FormikLike.getFieldId(form, String(formFieldName)) const Component = isPassword ? PasswordField : TextField const inputType = isPassword ? undefined : type @@ -167,4 +167,4 @@ export const formTextFieldFactory = (): React.FC> => { // Required when using an anonymous factory function component.displayName = "FormTextField" return component -} +} \ No newline at end of file diff --git a/site/components/Form/FormTitle.tsx b/site/components/Form/FormTitle.tsx new file mode 100644 index 0000000000000..8e6275853719c --- /dev/null +++ b/site/components/Form/FormTitle.tsx @@ -0,0 +1,31 @@ +import { makeStyles } from "@material-ui/core/styles" +import Typography from "@material-ui/core/Typography" +import React from "react" + +export interface TitleProps { + title: string + detail: React.ReactNode +} + +const useStyles = makeStyles((theme) => ({ + title: { + textAlign: "center", + marginTop: theme.spacing(5), + marginBottom: theme.spacing(5), + + "& h3": { + marginBottom: theme.spacing(1), + }, + }, +})) + +export const Title: React.FC = ({ title, detail }) => { + const styles = useStyles() + + return ( +
+ {title} + {detail} +
+ ) +} diff --git a/site/components/Form/index.ts b/site/components/Form/index.ts new file mode 100644 index 0000000000000..f9b0464de03ce --- /dev/null +++ b/site/components/Form/index.ts @@ -0,0 +1,4 @@ +export * from "./FormRow" +export * from "./FormSection" +export * from "./FormTextField" +export * from "./FormTitle" \ No newline at end of file diff --git a/site/components/Form/types.ts b/site/components/Form/types.ts new file mode 100644 index 0000000000000..bc30b42d424e7 --- /dev/null +++ b/site/components/Form/types.ts @@ -0,0 +1,16 @@ +import { FormikLike } from "../../util/formik" + +/** + * FormFieldProps are required props for creating form fields using a factory. + */ +export interface FormFieldProps { + /** + * form is a reference to a form or subform and is used to compute common + * states such as error and helper text + */ + form: FormikLike + /** + * formFieldName is a field name associated with the form schema. + */ + formFieldName: keyof T +} diff --git a/site/components/Navbar/index.tsx b/site/components/Navbar/index.tsx index b9fd1e3d95324..2a27693e775b2 100644 --- a/site/components/Navbar/index.tsx +++ b/site/components/Navbar/index.tsx @@ -1,7 +1,5 @@ import React from "react" import Button from "@material-ui/core/Button" -import List from "@material-ui/core/List" -import ListSubheader from "@material-ui/core/ListSubheader" import { makeStyles } from "@material-ui/core/styles" import Link from "next/link" @@ -23,14 +21,8 @@ export const Navbar: React.FC = () => { -
-
Coder v2
-
-
- - Manage - -
+
+
) } diff --git a/site/components/Page/Footer.test.tsx b/site/components/Page/Footer.test.tsx deleted file mode 100644 index 32d710d1ad564..0000000000000 --- a/site/components/Page/Footer.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react" -import { screen } from "@testing-library/react" - -import { render } from "../../test_helpers" -import { Footer } from "./Footer" - -describe("Footer", () => { - it("renders content", async () => { - // When - render(