From adeb0ffb5b3142f7be80bfade8f8b75e8b201f42 Mon Sep 17 00:00:00 2001 From: G r e y Date: Fri, 20 May 2022 17:42:49 +0000 Subject: [PATCH 1/4] feat: ui for editing ws schedule Summary: This presents a form component and storybook. The UI will be a routed page and added into the dashboard in a separate PR. It is likely a XService will be used at the page level to supply errors and actions to this form. Impact of Change: Further progress on #1455 Squashed Commits: * refactor: add className prop to Stack combine classes with internal classes and an optional external className to better control the Stack. --- site/src/components/Stack/Stack.tsx | 11 +- .../WorkspaceScheduleForm.stories.tsx | 20 +++ .../WorkspaceStats/WorkspaceScheduleForm.tsx | 149 ++++++++++++++++++ 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 site/src/components/WorkspaceStats/WorkspaceScheduleForm.stories.tsx create mode 100644 site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx diff --git a/site/src/components/Stack/Stack.tsx b/site/src/components/Stack/Stack.tsx index de9bacede3f73..97bb40538617c 100644 --- a/site/src/components/Stack/Stack.tsx +++ b/site/src/components/Stack/Stack.tsx @@ -1,11 +1,12 @@ import { makeStyles } from "@material-ui/core/styles" import React from "react" +import { combineClasses } from "../../util/combineClasses" type Direction = "column" | "row" interface StyleProps { - spacing: number direction: Direction + spacing: number } const useStyles = makeStyles((theme) => ({ @@ -17,11 +18,13 @@ const useStyles = makeStyles((theme) => ({ })) export interface StackProps { - spacing?: number + className?: string direction?: Direction + spacing?: number } -export const Stack: React.FC = ({ children, spacing = 2, direction = "column" }) => { +export const Stack: React.FC = ({ children, className, direction = "column", spacing = 2 }) => { const styles = useStyles({ spacing, direction }) - return
{children}
+ + return
{children}
} diff --git a/site/src/components/WorkspaceStats/WorkspaceScheduleForm.stories.tsx b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.stories.tsx new file mode 100644 index 0000000000000..74fc7b921ce75 --- /dev/null +++ b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.stories.tsx @@ -0,0 +1,20 @@ +import { action } from "@storybook/addon-actions" +import { Story } from "@storybook/react" +import React from "react" +import { WorkspaceScheduleForm, WorkspaceScheduleFormProps } from "./WorkspaceScheduleForm" + +export default { + title: "components/WorkspaceScheduleForm", + component: WorkspaceScheduleForm, +} + +const Template: Story = (args) => + +export const Example = Template.bind({}) +Example.args = { + onCancel: () => action("onCancel"), + onSubmit: () => { + action("onSubmit") + return Promise.resolve() + }, +} diff --git a/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx new file mode 100644 index 0000000000000..14ff1a89c80f1 --- /dev/null +++ b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx @@ -0,0 +1,149 @@ +import Checkbox from "@material-ui/core/Checkbox" +import FormControl from "@material-ui/core/FormControl" +import FormControlLabel from "@material-ui/core/FormControlLabel" +import FormGroup from "@material-ui/core/FormGroup" +import FormLabel from "@material-ui/core/FormLabel" +import makeStyles from "@material-ui/core/styles/makeStyles" +import TextField from "@material-ui/core/TextField" +import { useFormik } from "formik" +import React from "react" +import { getFormHelpers } from "../../util/formUtils" +import { FormFooter } from "../FormFooter/FormFooter" +import { FullPageForm } from "../FullPageForm/FullPageForm" +import { Stack } from "../Stack/Stack" + +export const Language = { + daysOfWeekLabel: "Days of Week", + daySundayLabel: "Sunday", + dayMondayLabel: "Monday", + dayTuesdayLabel: "Tuesday", + dayWednesdayLabel: "Wednesday", + dayThursdayLabel: "Thursday", + dayFridayLabel: "Friday", + daySaturdayLabel: "Saturday", + startTimeLabel: "Start time", + ttlLabel: "Runtime (minutes)", + ttlHelperText: "Your workspace will automatically shutdown after the runtime.", +} + +export interface WorkspaceScheduleFormProps { + onCancel: () => void + onSubmit: (values: WorkspaceScheduleFormValues) => Promise +} + +export interface WorkspaceScheduleFormValues { + sunday: boolean + monday: boolean + tuesday: boolean + wednesday: boolean + thursday: boolean + friday: boolean + saturday: boolean + + startTime: string + ttl: number +} + +export const WorkspaceScheduleForm: React.FC = ({ onCancel, onSubmit }) => { + const styles = useStyles() + + const form = useFormik({ + initialValues: { + sunday: false, + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: false, + + startTime: "", + ttl: 0, + }, + onSubmit, + }) + const formHelpers = getFormHelpers(form) + + return ( + +
+ + + + + + {Language.daysOfWeekLabel} + + + + } + label={Language.daySundayLabel} + /> + } + label={Language.dayMondayLabel} + /> + } + label={Language.dayTuesdayLabel} + /> + } + label={Language.dayWednesdayLabel} + /> + } + label={Language.dayThursdayLabel} + /> + } + label={Language.dayFridayLabel} + /> + } + label={Language.daySaturdayLabel} + /> + + + + + + + +
+
+ ) +} + +const useStyles = makeStyles({ + form: { + display: "flex", + justifyContent: "center", + }, + stack: { + // REMARK: 360 is 'arbitrary' in that it gives the helper text enough room + // to render on one line. If we change the text, we might want to + // adjust these. Without constraining the width, the date picker + // and number inputs aren't visually appealing or maximally usable. + maxWidth: 360, + minWidth: 360, + }, + daysOfWeekLabel: { + fontSize: 12, + }, +}) From 2ee8d87e682c618fd9ebe568f57a45ed7e589caa Mon Sep 17 00:00:00 2001 From: G r e y Date: Fri, 20 May 2022 19:25:47 +0000 Subject: [PATCH 2/4] validation --- .../WorkspaceScheduleForm.test.ts | 57 +++++++++++ .../WorkspaceStats/WorkspaceScheduleForm.tsx | 95 +++++++++++++++++-- 2 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 site/src/components/WorkspaceStats/WorkspaceScheduleForm.test.ts diff --git a/site/src/components/WorkspaceStats/WorkspaceScheduleForm.test.ts b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.test.ts new file mode 100644 index 0000000000000..cdd133dfc7932 --- /dev/null +++ b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.test.ts @@ -0,0 +1,57 @@ +import { Language, validationSchema, WorkspaceScheduleFormValues } from "./WorkspaceScheduleForm" + +const valid: WorkspaceScheduleFormValues = { + sunday: false, + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: false, + + startTime: "09:30", + ttl: 120, +} + +describe("validationSchema", () => { + it("allows everything to be falsy", () => { + const values: WorkspaceScheduleFormValues = { + sunday: false, + monday: false, + tuesday: false, + wednesday: false, + thursday: false, + friday: false, + saturday: false, + + startTime: "", + ttl: 0, + } + const validate = () => validationSchema.validateSync(values) + expect(validate).not.toThrow() + }) + + it("disallows ttl to be negative", () => { + const values = { + ...valid, + ttl: -1, + } + const validate = () => validationSchema.validateSync(values) + expect(validate).toThrow() + }) + + it("disallows all days-of-week to be false when startTime is set", () => { + const values = { + ...valid, + sunday: false, + monday: false, + tuesday: false, + wednesday: false, + thursday: false, + friday: false, + saturday: false, + } + const validate = () => validationSchema.validateSync(values) + expect(validate).toThrowError(Language.errorNoDayOfWeek) + }) +}) diff --git a/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx index 14ff1a89c80f1..f0eb594ed8cd7 100644 --- a/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx +++ b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx @@ -7,12 +7,14 @@ import makeStyles from "@material-ui/core/styles/makeStyles" import TextField from "@material-ui/core/TextField" import { useFormik } from "formik" import React from "react" +import * as Yup from "yup" import { getFormHelpers } from "../../util/formUtils" import { FormFooter } from "../FormFooter/FormFooter" import { FullPageForm } from "../FullPageForm/FullPageForm" import { Stack } from "../Stack/Stack" export const Language = { + errorNoDayOfWeek: "Must set at least one day of week", daysOfWeekLabel: "Days of Week", daySundayLabel: "Sunday", dayMondayLabel: "Monday", @@ -44,6 +46,35 @@ export interface WorkspaceScheduleFormValues { ttl: number } +export const validationSchema = Yup.object({ + sunday: Yup.boolean(), + monday: Yup.boolean().test("at-least-one-day", Language.errorNoDayOfWeek, function (value) { + const parent = this.parent as WorkspaceScheduleFormValues + + if (!parent.startTime) { + return true + } else { + return ![ + parent.sunday, + value, + parent.tuesday, + parent.wednesday, + parent.thursday, + parent.friday, + parent.saturday, + ].every((day) => day === false) + } + }), + tuesday: Yup.boolean(), + wednesday: Yup.boolean(), + thursday: Yup.boolean(), + friday: Yup.boolean(), + saturday: Yup.boolean(), + + startTime: Yup.string(), + ttl: Yup.number().min(0).integer(), +}) + export const WorkspaceScheduleForm: React.FC = ({ onCancel, onSubmit }) => { const styles = useStyles() @@ -61,6 +92,7 @@ export const WorkspaceScheduleForm: React.FC = ({ on ttl: 0, }, onSubmit, + validationSchema, }) const formHelpers = getFormHelpers(form) @@ -85,31 +117,80 @@ export const WorkspaceScheduleForm: React.FC = ({ on } + control={ + + } label={Language.daySundayLabel} /> } + control={ + + } label={Language.dayMondayLabel} /> } + control={ + + } label={Language.dayTuesdayLabel} /> } + control={ + + } label={Language.dayWednesdayLabel} /> } + control={ + + } label={Language.dayThursdayLabel} /> } + control={ + + } label={Language.dayFridayLabel} /> } + control={ + + } label={Language.daySaturdayLabel} /> From a651f0e774f08676e8f9c2cdbb9defe87a08b09c Mon Sep 17 00:00:00 2001 From: G r e y Date: Fri, 20 May 2022 19:31:26 +0000 Subject: [PATCH 3/4] fixup! validation --- .../components/WorkspaceStats/WorkspaceScheduleForm.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx index f0eb594ed8cd7..7c1e9589ea9f1 100644 --- a/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx +++ b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx @@ -2,6 +2,7 @@ import Checkbox from "@material-ui/core/Checkbox" import FormControl from "@material-ui/core/FormControl" import FormControlLabel from "@material-ui/core/FormControlLabel" import FormGroup from "@material-ui/core/FormGroup" +import FormHelperText from "@material-ui/core/FormHelperText" import FormLabel from "@material-ui/core/FormLabel" import makeStyles from "@material-ui/core/styles/makeStyles" import TextField from "@material-ui/core/TextField" @@ -88,8 +89,8 @@ export const WorkspaceScheduleForm: React.FC = ({ on friday: true, saturday: false, - startTime: "", - ttl: 0, + startTime: "09:30", + ttl: 120, }, onSubmit, validationSchema, @@ -110,7 +111,7 @@ export const WorkspaceScheduleForm: React.FC = ({ on variant="standard" /> - + {Language.daysOfWeekLabel} @@ -194,6 +195,7 @@ export const WorkspaceScheduleForm: React.FC = ({ on label={Language.daySaturdayLabel} /> + {form.errors.monday && {Language.errorNoDayOfWeek}} Date: Fri, 20 May 2022 19:37:41 +0000 Subject: [PATCH 4/4] Fix helperText --- site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx | 3 ++- site/src/util/formUtils.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx index 7c1e9589ea9f1..1603e28e7e8e1 100644 --- a/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx +++ b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx @@ -25,6 +25,7 @@ export const Language = { dayFridayLabel: "Friday", daySaturdayLabel: "Saturday", startTimeLabel: "Start time", + startTimeHelperText: "Your workspace will automatically start at this time.", ttlLabel: "Runtime (minutes)", ttlHelperText: "Your workspace will automatically shutdown after the runtime.", } @@ -102,7 +103,7 @@ export const WorkspaceScheduleForm: React.FC = ({ on