From 5d40b1f0f45359b8d77ae6a936629cb947ca2f55 Mon Sep 17 00:00:00 2001
From: Presley Pizzo <1290996+presleyp@users.noreply.github.com>
Date: Wed, 10 Aug 2022 18:03:15 -0400
Subject: [PATCH] feat: Add switches for auto-start and auto-stop (#3358)
* Add elements
* Add Loading story
* Make form show empty values when manual
* Make form depend on switches
* Fix style
* Format
* Update unit tests
* Tweaks
* Update storybook
* Move util files
* Pull out more util functions
* Pull out strings
* Add border to section
* Make min ttl 1
* Format
* Fix import
* Fix validation for falsey values
* Format and fix tests
* Put switches in form, persist form state
* Fix bug
* Remove helper text when disabled
* Fix storybook
* Revert "Remove helper text when disabled"
This reverts commit a6271ca6c4f83d149c283021fd6831d4ea771a3c.
* Format
* Use nicer function to set values
* Format
---
site/src/components/Section/Section.tsx | 5 +-
.../WorkspaceScheduleForm.stories.tsx | 57 +++--
.../WorkspaceScheduleForm.test.ts | 14 +-
.../WorkspaceScheduleForm.tsx | 230 ++++++++++--------
.../WorkspaceSchedulePage.test.tsx | 107 ++++----
.../WorkspaceSchedulePage.tsx | 160 ++----------
.../WorkspaceSchedulePage/formToRequest.ts | 74 ++++++
.../pages/WorkspaceSchedulePage/schedule.ts | 91 +++++++
site/src/pages/WorkspaceSchedulePage/ttl.ts | 13 +
9 files changed, 424 insertions(+), 327 deletions(-)
create mode 100644 site/src/pages/WorkspaceSchedulePage/formToRequest.ts
create mode 100644 site/src/pages/WorkspaceSchedulePage/schedule.ts
create mode 100644 site/src/pages/WorkspaceSchedulePage/ttl.ts
diff --git a/site/src/components/Section/Section.tsx b/site/src/components/Section/Section.tsx
index 9e2b993ed3..40e68161b7 100644
--- a/site/src/components/Section/Section.tsx
+++ b/site/src/components/Section/Section.tsx
@@ -30,7 +30,7 @@ export const Section: SectionFC = ({
}) => {
const styles = useStyles({ layout })
return (
-
+
{(title || description) && (
@@ -49,7 +49,7 @@ export const Section: SectionFC = ({
{alert &&
{alert}
}
{children}
-
+
)
}
@@ -63,6 +63,7 @@ const useStyles = makeStyles((theme) => ({
marginBottom: theme.spacing(1),
padding: theme.spacing(6),
borderRadius: theme.shape.borderRadius,
+ border: `1px solid ${theme.palette.divider}`,
[theme.breakpoints.down("sm")]: {
padding: theme.spacing(4, 3, 4, 3),
diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx
index cb24e1316d..7d1957bcdf 100644
--- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx
+++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx
@@ -3,12 +3,10 @@ import dayjs from "dayjs"
import advancedFormat from "dayjs/plugin/advancedFormat"
import timezone from "dayjs/plugin/timezone"
import utc from "dayjs/plugin/utc"
+import { defaultSchedule, emptySchedule } from "pages/WorkspaceSchedulePage/schedule"
+import { defaultTTL, emptyTTL } from "pages/WorkspaceSchedulePage/ttl"
import { makeMockApiError } from "testHelpers/entities"
-import {
- defaultWorkspaceSchedule,
- WorkspaceScheduleForm,
- WorkspaceScheduleFormProps,
-} from "./WorkspaceScheduleForm"
+import { WorkspaceScheduleForm, WorkspaceScheduleFormProps } from "./WorkspaceScheduleForm"
dayjs.extend(advancedFormat)
dayjs.extend(utc)
@@ -29,51 +27,60 @@ export default {
const Template: Story
= (args) =>
-export const WorkspaceWillNotShutDown = Template.bind({})
-WorkspaceWillNotShutDown.args = {
+const defaultInitialValues = {
+ autoStartEnabled: true,
+ ...defaultSchedule(),
+ autoStopEnabled: true,
+ ttl: defaultTTL,
+}
+
+export const AllDisabled = Template.bind({})
+AllDisabled.args = {
initialValues: {
- ...defaultWorkspaceSchedule(5),
- ttl: 0,
+ autoStartEnabled: false,
+ ...emptySchedule,
+ autoStopEnabled: false,
+ ttl: emptyTTL,
},
}
-export const WorkspaceWillShutdownInAnHour = Template.bind({})
-WorkspaceWillShutdownInAnHour.args = {
+export const AutoStart = Template.bind({})
+AutoStart.args = {
initialValues: {
- ...defaultWorkspaceSchedule(5),
- ttl: 1,
+ autoStartEnabled: true,
+ ...defaultSchedule(),
+ autoStopEnabled: false,
+ ttl: emptyTTL,
},
}
export const WorkspaceWillShutdownInTwoHours = Template.bind({})
WorkspaceWillShutdownInTwoHours.args = {
- initialValues: {
- ...defaultWorkspaceSchedule(2),
- ttl: 2,
- },
+ initialValues: { ...defaultInitialValues, ttl: 2 },
}
export const WorkspaceWillShutdownInADay = Template.bind({})
WorkspaceWillShutdownInADay.args = {
- initialValues: {
- ...defaultWorkspaceSchedule(2),
- ttl: 24,
- },
+ initialValues: { ...defaultInitialValues, ttl: 24 },
}
export const WorkspaceWillShutdownInTwoDays = Template.bind({})
WorkspaceWillShutdownInTwoDays.args = {
- initialValues: {
- ...defaultWorkspaceSchedule(2),
- ttl: 48,
- },
+ initialValues: { ...defaultInitialValues, ttl: 48 },
}
export const WithError = Template.bind({})
WithError.args = {
+ initialValues: { ...defaultInitialValues, ttl: 100 },
initialTouched: { ttl: true },
submitScheduleError: makeMockApiError({
message: "Something went wrong.",
validations: [{ field: "ttl_ms", detail: "Invalid time until shutdown." }],
}),
}
+
+export const Loading = Template.bind({})
+Loading.args = {
+ initialValues: defaultInitialValues,
+ isLoading: true,
+}
diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts
index 0b08446f0f..101635a13c 100644
--- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts
+++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts
@@ -7,6 +7,7 @@ import {
import { zones } from "./zones"
const valid: WorkspaceScheduleFormValues = {
+ autoStartEnabled: true,
sunday: false,
monday: true,
tuesday: true,
@@ -14,15 +15,17 @@ const valid: WorkspaceScheduleFormValues = {
thursday: true,
friday: true,
saturday: false,
-
startTime: "09:30",
timezone: "Canada/Eastern",
+
+ autoStopEnabled: true,
ttl: 120,
}
describe("validationSchema", () => {
- it("allows everything to be falsy", () => {
+ it("allows everything to be falsy when switches are off", () => {
const values: WorkspaceScheduleFormValues = {
+ autoStartEnabled: false,
sunday: false,
monday: false,
tuesday: false,
@@ -30,9 +33,10 @@ describe("validationSchema", () => {
thursday: false,
friday: false,
saturday: false,
-
startTime: "",
timezone: "",
+
+ autoStopEnabled: false,
ttl: 0,
}
const validate = () => validationSchema.validateSync(values)
@@ -48,7 +52,7 @@ describe("validationSchema", () => {
expect(validate).toThrow()
})
- it("disallows all days-of-week to be false when startTime is set", () => {
+ it("disallows all days-of-week to be false when auto-start is enabled", () => {
const values: WorkspaceScheduleFormValues = {
...valid,
sunday: false,
@@ -63,7 +67,7 @@ describe("validationSchema", () => {
expect(validate).toThrowError(Language.errorNoDayOfWeek)
})
- it("disallows empty startTime when at least one day is set", () => {
+ it("disallows empty startTime when auto-start is enabled", () => {
const values: WorkspaceScheduleFormValues = {
...valid,
sunday: false,
diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx
index 6eb500550f..94c378c7f7 100644
--- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx
+++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx
@@ -6,8 +6,10 @@ import FormHelperText from "@material-ui/core/FormHelperText"
import FormLabel from "@material-ui/core/FormLabel"
import MenuItem from "@material-ui/core/MenuItem"
import makeStyles from "@material-ui/core/styles/makeStyles"
+import Switch from "@material-ui/core/Switch"
import TextField from "@material-ui/core/TextField"
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
+import { Section } from "components/Section/Section"
import dayjs from "dayjs"
import advancedFormat from "dayjs/plugin/advancedFormat"
import duration from "dayjs/plugin/duration"
@@ -15,7 +17,9 @@ import relativeTime from "dayjs/plugin/relativeTime"
import timezone from "dayjs/plugin/timezone"
import utc from "dayjs/plugin/utc"
import { FormikTouched, useFormik } from "formik"
-import { FC } from "react"
+import { defaultSchedule } from "pages/WorkspaceSchedulePage/schedule"
+import { defaultTTL } from "pages/WorkspaceSchedulePage/ttl"
+import { ChangeEvent, FC } from "react"
import * as Yup from "yup"
import { getFormHelpersWithError } from "../../util/formUtils"
import { FormFooter } from "../FormFooter/FormFooter"
@@ -32,10 +36,11 @@ dayjs.extend(relativeTime)
dayjs.extend(timezone)
export const Language = {
- errorNoDayOfWeek: "Must set at least one day of week if start time is set",
- errorNoTime: "Start time is required when days of the week are selected",
+ errorNoDayOfWeek: "Must set at least one day of week if auto-start is enabled",
+ errorNoTime: "Start time is required when auto-start is enabled",
errorTime: "Time must be in HH:mm format (24 hours)",
errorTimezone: "Invalid timezone",
+ errorNoStop: "Time until shutdown must be greater than zero when auto-stop is enabled",
daysOfWeekLabel: "Days of Week",
daySundayLabel: "Sunday",
dayMondayLabel: "Monday",
@@ -51,11 +56,16 @@ export const Language = {
ttlCausesShutdownHelperText: "Your workspace will shut down",
ttlCausesShutdownAfterStart: "after start",
ttlCausesNoShutdownHelperText: "Your workspace will not automatically shut down.",
+ formTitle: "Workspace schedule",
+ startSection: "Start",
+ startSwitch: "Auto-start",
+ stopSection: "Stop",
+ stopSwitch: "Auto-stop",
}
export interface WorkspaceScheduleFormProps {
submitScheduleError?: Error | unknown
- initialValues?: WorkspaceScheduleFormValues
+ initialValues: WorkspaceScheduleFormValues
isLoading: boolean
onCancel: () => void
onSubmit: (values: WorkspaceScheduleFormValues) => void
@@ -64,6 +74,7 @@ export interface WorkspaceScheduleFormProps {
}
export interface WorkspaceScheduleFormValues {
+ autoStartEnabled: boolean
sunday: boolean
monday: boolean
tuesday: boolean
@@ -71,18 +82,20 @@ export interface WorkspaceScheduleFormValues {
thursday: boolean
friday: boolean
saturday: boolean
-
startTime: string
timezone: string
+
+ autoStopEnabled: boolean
ttl: number
}
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
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) {
+ if (!parent.autoStartEnabled) {
return true
} else {
return ![
@@ -104,20 +117,9 @@ export const validationSchema = Yup.object({
startTime: Yup.string()
.ensure()
- .test("required-if-day-selected", Language.errorNoTime, function (value) {
+ .test("required-if-auto-start", Language.errorNoTime, function (value) {
const parent = this.parent as WorkspaceScheduleFormValues
-
- const isDaySelected = [
- parent.sunday,
- parent.monday,
- parent.tuesday,
- parent.wednesday,
- parent.thursday,
- parent.friday,
- parent.saturday,
- ].some((day) => day)
-
- if (isDaySelected) {
+ if (parent.autoStartEnabled) {
return value !== ""
} else {
return true
@@ -157,31 +159,20 @@ export const validationSchema = Yup.object({
ttl: Yup.number()
.integer()
.min(0)
- .max(24 * 7 /* 7 days */),
-})
-
-export const defaultWorkspaceScheduleTTL = 8
-
-export const defaultWorkspaceSchedule = (
- ttl = defaultWorkspaceScheduleTTL,
- timezone = dayjs.tz.guess(),
-): WorkspaceScheduleFormValues => ({
- sunday: false,
- monday: true,
- tuesday: true,
- wednesday: true,
- thursday: true,
- friday: true,
- saturday: false,
-
- startTime: "09:30",
- timezone,
- ttl,
+ .max(24 * 7 /* 7 days */)
+ .test("positive-if-auto-stop", Language.errorNoStop, function (value) {
+ const parent = this.parent as WorkspaceScheduleFormValues
+ if (parent.autoStopEnabled) {
+ return !!value
+ } else {
+ return true
+ }
+ }),
})
export const WorkspaceScheduleForm: FC = ({
submitScheduleError,
- initialValues = defaultWorkspaceSchedule(),
+ initialValues,
isLoading,
onCancel,
onSubmit,
@@ -210,72 +201,115 @@ export const WorkspaceScheduleForm: FC = ({
{ value: form.values.saturday, name: "saturday", label: Language.daySaturdayLabel },
]
+ const handleToggleAutoStart = async (e: ChangeEvent) => {
+ form.handleChange(e)
+ // if enabling from empty values, fill with defaults
+ if (!form.values.autoStartEnabled && !form.values.startTime) {
+ await form.setValues({ ...form.values, autoStartEnabled: true, ...defaultSchedule() })
+ }
+ }
+
+ const handleToggleAutoStop = async (e: ChangeEvent) => {
+ form.handleChange(e)
+ // if enabling from empty values, fill with defaults
+ if (!form.values.autoStopEnabled && !form.values.ttl) {
+ await form.setFieldValue("ttl", defaultTTL)
+ }
+ }
+
return (
-
+