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.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 new file mode 100644 index 0000000000000..1603e28e7e8e1 --- /dev/null +++ b/site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx @@ -0,0 +1,233 @@ +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" +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", + dayTuesdayLabel: "Tuesday", + dayWednesdayLabel: "Wednesday", + dayThursdayLabel: "Thursday", + 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.", +} + +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 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() + + const form = useFormik({ + initialValues: { + sunday: false, + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: false, + + startTime: "09:30", + ttl: 120, + }, + onSubmit, + validationSchema, + }) + 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} + /> + + {form.errors.monday && {Language.errorNoDayOfWeek}} + + + + + + +
+
+ ) +} + +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, + }, +}) diff --git a/site/src/util/formUtils.ts b/site/src/util/formUtils.ts index da5d1b6b87a78..eec487f70ca15 100644 --- a/site/src/util/formUtils.ts +++ b/site/src/util/formUtils.ts @@ -28,7 +28,7 @@ export const getFormHelpers = ...form.getFieldProps(name), id: name, error: touched && Boolean(error), - helperText: touched ? error : helperText, + helperText: touched ? error || helperText : helperText, } }