diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts index 49d97a3c7c..101635a13c 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts @@ -1,12 +1,13 @@ import { - getValidationSchema, Language, ttlShutdownAt, + validationSchema, WorkspaceScheduleFormValues, } from "./WorkspaceScheduleForm" 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 when switches are off", () => { const values: WorkspaceScheduleFormValues = { + autoStartEnabled: false, sunday: false, monday: false, tuesday: false, @@ -30,12 +33,13 @@ describe("validationSchema", () => { thursday: false, friday: false, saturday: false, - startTime: "", timezone: "", + + autoStopEnabled: false, ttl: 0, } - const validate = () => getValidationSchema(false, false).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).not.toThrow() }) @@ -44,7 +48,7 @@ describe("validationSchema", () => { ...valid, ttl: -1, } - const validate = () => getValidationSchema(true, true).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).toThrow() }) @@ -59,7 +63,7 @@ describe("validationSchema", () => { friday: false, saturday: false, } - const validate = () => getValidationSchema(true, false).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).toThrowError(Language.errorNoDayOfWeek) }) @@ -75,7 +79,7 @@ describe("validationSchema", () => { saturday: false, startTime: "", } - const validate = () => getValidationSchema(true, false).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).toThrowError(Language.errorNoTime) }) @@ -84,7 +88,7 @@ describe("validationSchema", () => { ...valid, startTime: "16:20", } - const validate = () => getValidationSchema(true, true).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).not.toThrow() }) @@ -93,7 +97,7 @@ describe("validationSchema", () => { ...valid, startTime: "9:30", } - const validate = () => getValidationSchema(true, true).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).toThrowError(Language.errorTime) }) @@ -102,7 +106,7 @@ describe("validationSchema", () => { ...valid, startTime: "09:5", } - const validate = () => getValidationSchema(true, true).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).toThrowError(Language.errorTime) }) @@ -111,7 +115,7 @@ describe("validationSchema", () => { ...valid, startTime: "24:01", } - const validate = () => getValidationSchema(true, true).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).toThrowError(Language.errorTime) }) @@ -120,7 +124,7 @@ describe("validationSchema", () => { ...valid, startTime: "09:60", } - const validate = () => getValidationSchema(true, true).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).toThrowError(Language.errorTime) }) @@ -129,7 +133,7 @@ describe("validationSchema", () => { ...valid, timezone: "Canada/North", } - const validate = () => getValidationSchema(true, true).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).toThrowError(Language.errorTimezone) }) @@ -138,7 +142,7 @@ describe("validationSchema", () => { ...valid, timezone: zone, } - const validate = () => getValidationSchema(true, true).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).not.toThrow() }) @@ -147,7 +151,7 @@ describe("validationSchema", () => { ...valid, ttl: 24 * 7, } - const validate = () => getValidationSchema(true, true).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).not.toThrowError() }) @@ -156,7 +160,7 @@ describe("validationSchema", () => { ...valid, ttl: 24 * 7 + 1, } - const validate = () => getValidationSchema(true, true).validateSync(values) + const validate = () => validationSchema.validateSync(values) expect(validate).toThrowError("ttl must be less than or equal to 168") }) }) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx index 3fda342cad..e80cfad890 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx @@ -17,9 +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 { AutoStart } from "pages/WorkspaceSchedulePage/schedule" -import { AutoStop } from "pages/WorkspaceSchedulePage/ttl" -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" @@ -65,10 +65,7 @@ export const Language = { export interface WorkspaceScheduleFormProps { submitScheduleError?: Error | unknown - autoStart: AutoStart - toggleAutoStart: () => void - autoStop: AutoStop - toggleAutoStop: () => void + initialValues: WorkspaceScheduleFormValues isLoading: boolean onCancel: () => void onSubmit: (values: WorkspaceScheduleFormValues) => void @@ -77,6 +74,7 @@ export interface WorkspaceScheduleFormProps { } export interface WorkspaceScheduleFormValues { + autoStartEnabled: boolean sunday: boolean monday: boolean tuesday: boolean @@ -84,111 +82,108 @@ 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 getValidationSchema = (autoStartEnabled: boolean, autoStopEnabled: boolean) => - Yup.object({ - sunday: Yup.boolean(), - monday: Yup.boolean().test("at-least-one-day", Language.errorNoDayOfWeek, function (value) { - const parent = this.parent as WorkspaceScheduleFormValues +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 (!autoStartEnabled) { - return true + if (parent.autoStartEnabled) { + 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() + .ensure() + .test("required-if-auto-start", Language.errorNoTime, function (value) { + const parent = this.parent as WorkspaceScheduleFormValues + if (parent.autoStartEnabled) { + return value !== "" } else { - return ![ - parent.sunday, - value, - parent.tuesday, - parent.wednesday, - parent.thursday, - parent.friday, - parent.saturday, - ].every((day) => day === false) + return true + } + }) + .test("is-time-string", Language.errorTime, (value) => { + if (value === "") { + return true + } else if (!/^[0-9][0-9]:[0-9][0-9]$/.test(value)) { + return false + } else { + const parts = value.split(":") + const HH = Number(parts[0]) + const mm = Number(parts[1]) + return HH >= 0 && HH <= 23 && mm >= 0 && mm <= 59 } }), - tuesday: Yup.boolean(), - wednesday: Yup.boolean(), - thursday: Yup.boolean(), - friday: Yup.boolean(), - saturday: Yup.boolean(), + timezone: Yup.string() + .ensure() + .test("is-timezone", Language.errorTimezone, function (value) { + const parent = this.parent as WorkspaceScheduleFormValues - startTime: Yup.string() - .ensure() - .test("required-if-auto-start", Language.errorNoTime, function (value) { - if (autoStartEnabled) { - return value !== "" - } else { + if (!parent.startTime) { + return true + } else { + // Unfortunately, there's not a good API on dayjs at this time for + // evaluating a timezone. Attempt to parse today in the supplied timezone + // and return as valid if the function doesn't throw. + try { + dayjs.tz(dayjs(), value) return true - } - }) - .test("is-time-string", Language.errorTime, (value) => { - if (value === "") { - return true - } else if (!/^[0-9][0-9]:[0-9][0-9]$/.test(value)) { + } catch (e) { return false - } else { - const parts = value.split(":") - const HH = Number(parts[0]) - const mm = Number(parts[1]) - return HH >= 0 && HH <= 23 && mm >= 0 && mm <= 59 } - }), - timezone: Yup.string() - .ensure() - .test("is-timezone", Language.errorTimezone, function (value) { - const parent = this.parent as WorkspaceScheduleFormValues - - if (!parent.startTime) { - return true - } else { - // Unfortunately, there's not a good API on dayjs at this time for - // evaluating a timezone. Attempt to parse today in the supplied timezone - // and return as valid if the function doesn't throw. - try { - dayjs.tz(dayjs(), value) - return true - } catch (e) { - return false - } - } - }), - ttl: Yup.number() - .integer() - .min(0) - .max(24 * 7 /* 7 days */) - .test("positive-if-auto-stop", Language.errorNoStop, (value) => { - if (autoStopEnabled) { - return !!value - } else { - return true - } - }), - }) + } + }), + ttl: Yup.number() + .integer() + .min(0) + .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, - autoStart, - toggleAutoStart, - autoStop, - toggleAutoStop, + initialValues, isLoading, onCancel, onSubmit, initialTouched, }) => { const styles = useStyles() - const initialValues = { ...autoStart.schedule, ttl: autoStop.ttl } const form = useFormik({ initialValues, - enableReinitialize: true, onSubmit, - validationSchema: () => getValidationSchema(autoStart.enabled, autoStop.enabled), + validationSchema, initialTouched, }) const formHelpers = getFormHelpersWithError( @@ -206,6 +201,27 @@ 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) { + const defaults = defaultSchedule() + checkboxes.forEach(async ({ name }) => { + await form.setFieldValue(name, defaults[name]) + }) + await form.setFieldValue("startTime", defaults.startTime) + await form.setFieldValue("timezone", defaults.timezone) + } + } + + 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 (
@@ -213,12 +229,18 @@ export const WorkspaceScheduleForm: FC = ({ {submitScheduleError && }
} + control={ + + } label={Language.startSwitch} /> = ({ = ({ control={ = ({
} + control={ + + } label={Language.stopSwitch} /> { [ // Empty case { + autoStartEnabled: false, sunday: false, monday: false, tuesday: false, @@ -35,6 +38,7 @@ describe("WorkspaceSchedulePage", () => { saturday: false, startTime: "", timezone: "", + autoStopEnabled: false, ttl: 0, }, { @@ -44,6 +48,7 @@ describe("WorkspaceSchedulePage", () => { [ // Single day { + autoStartEnabled: true, sunday: true, monday: false, tuesday: false, @@ -53,6 +58,7 @@ describe("WorkspaceSchedulePage", () => { saturday: false, startTime: "16:20", timezone: "Canada/Eastern", + autoStopEnabled: true, ttl: 120, }, { @@ -62,6 +68,7 @@ describe("WorkspaceSchedulePage", () => { [ // Standard 1-5 case { + autoStartEnabled: true, sunday: false, monday: true, tuesday: true, @@ -71,6 +78,7 @@ describe("WorkspaceSchedulePage", () => { saturday: false, startTime: "09:30", timezone: "America/Central", + autoStopEnabled: true, ttl: 120, }, { @@ -80,6 +88,7 @@ describe("WorkspaceSchedulePage", () => { [ // Everyday { + autoStartEnabled: true, sunday: true, monday: true, tuesday: true, @@ -89,6 +98,7 @@ describe("WorkspaceSchedulePage", () => { saturday: true, startTime: "09:00", timezone: "", + autoStopEnabled: true, ttl: 60 * 8, }, { @@ -98,6 +108,7 @@ describe("WorkspaceSchedulePage", () => { [ // Mon, Wed, Fri Evenings { + autoStartEnabled: true, sunday: false, monday: true, tuesday: false, @@ -107,6 +118,7 @@ describe("WorkspaceSchedulePage", () => { saturday: false, startTime: "16:20", timezone: "", + autoStopEnabled: true, ttl: 60 * 3, }, { @@ -161,18 +173,16 @@ describe("WorkspaceSchedulePage", () => { [ undefined, { - enabled: false, - schedule: { - sunday: false, - monday: false, - tuesday: false, - wednesday: false, - thursday: false, - friday: false, - saturday: false, - startTime: "", - timezone: "", - }, + autoStartEnabled: false, + sunday: false, + monday: false, + tuesday: false, + wednesday: false, + thursday: false, + friday: false, + saturday: false, + startTime: "", + timezone: "", }, ], @@ -180,18 +190,16 @@ describe("WorkspaceSchedulePage", () => { [ "CRON_TZ=UTC 30 9 * * 1-5", { - enabled: true, - schedule: { - sunday: false, - monday: true, - tuesday: true, - wednesday: true, - thursday: true, - friday: true, - saturday: false, - startTime: "09:30", - timezone: "UTC", - }, + autoStartEnabled: true, + sunday: false, + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: false, + startTime: "09:30", + timezone: "UTC", }, ], @@ -199,18 +207,16 @@ describe("WorkspaceSchedulePage", () => { [ "CRON_TZ=Canada/Eastern 20 16 * * 1,3-4,6", { - enabled: true, - schedule: { - sunday: false, - monday: true, - tuesday: false, - wednesday: true, - thursday: true, - friday: false, - saturday: true, - startTime: "16:20", - timezone: "Canada/Eastern", - }, + autoStartEnabled: true, + sunday: false, + monday: true, + tuesday: false, + wednesday: true, + thursday: true, + friday: false, + saturday: true, + startTime: "16:20", + timezone: "Canada/Eastern", }, ], ])(`scheduleToAutoStart(%p) returns %p`, (schedule, autoStart) => { @@ -221,11 +227,11 @@ describe("WorkspaceSchedulePage", () => { describe("ttlMsToAutoStop", () => { it.each<[number | undefined, AutoStop]>([ // empty case - [undefined, { enabled: false, ttl: 0 }], + [undefined, { autoStopEnabled: false, ttl: 0 }], // zero - [0, { enabled: false, ttl: 0 }], + [0, { autoStopEnabled: false, ttl: 0 }], // basic case - [28_800_000, { enabled: true, ttl: 8 }], + [28_800_000, { autoStopEnabled: true, ttl: 8 }], ])(`ttlMsToAutoStop(%p) returns %p`, (ttlMs, autoStop) => { expect(ttlMsToAutoStop(ttlMs)).toEqual(autoStop) }) diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index a59b4b0804..b20be6b5de 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -1,10 +1,6 @@ import { useMachine, useSelector } from "@xstate/react" -import { - defaultSchedule, - emptySchedule, - scheduleToAutoStart, -} from "pages/WorkspaceSchedulePage/schedule" -import { defaultTTL, emptyTTL, ttlMsToAutoStop } from "pages/WorkspaceSchedulePage/ttl" +import { scheduleToAutoStart } from "pages/WorkspaceSchedulePage/schedule" +import { ttlMsToAutoStop } from "pages/WorkspaceSchedulePage/ttl" import React, { useContext, useEffect, useState } from "react" import { Navigate, useNavigate, useParams } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" @@ -58,52 +54,6 @@ export const WorkspaceSchedulePage: React.FC = () => { setAutoStop(getAutoStop(workspace)) }, [workspace]) - const onToggleAutoStart = () => { - if (autoStart.enabled) { - setAutoStart({ - enabled: false, - schedule: emptySchedule, - }) - } else { - if (workspace?.autostart_schedule) { - // repopulate saved schedule - setAutoStart({ - enabled: true, - schedule: getAutoStart(workspace).schedule, - }) - } else { - // populate with defaults - setAutoStart({ - enabled: true, - schedule: defaultSchedule(), - }) - } - } - } - - const onToggleAutoStop = () => { - if (autoStop.enabled) { - setAutoStop({ - enabled: false, - ttl: emptyTTL, - }) - } else { - if (workspace?.ttl_ms) { - // repopulate saved ttl - setAutoStop({ - enabled: true, - ttl: getAutoStop(workspace).ttl, - }) - } else { - // set default - setAutoStop({ - enabled: true, - ttl: defaultTTL, - }) - } - } - } - if (!username || !workspaceName) { return } @@ -137,10 +87,7 @@ export const WorkspaceSchedulePage: React.FC = () => { return ( { navigate(`/@${username}/${workspaceName}`) diff --git a/site/src/pages/WorkspaceSchedulePage/formToRequest.ts b/site/src/pages/WorkspaceSchedulePage/formToRequest.ts index 88dafe245d..8802139c76 100644 --- a/site/src/pages/WorkspaceSchedulePage/formToRequest.ts +++ b/site/src/pages/WorkspaceSchedulePage/formToRequest.ts @@ -4,7 +4,7 @@ import { WorkspaceScheduleFormValues } from "components/WorkspaceScheduleForm/Wo export const formValuesToAutoStartRequest = ( values: WorkspaceScheduleFormValues, ): TypesGen.UpdateWorkspaceAutostartRequest => { - if (!values.startTime) { + if (!values.autoStartEnabled || !values.startTime) { return { schedule: "", } @@ -69,6 +69,6 @@ export const formValuesToTTLRequest = ( ): TypesGen.UpdateWorkspaceTTLRequest => { return { // minutes to nanoseconds - ttl_ms: values.ttl ? values.ttl * 60 * 60 * 1000 : undefined, + ttl_ms: values.autoStopEnabled && values.ttl ? values.ttl * 60 * 60 * 1000 : undefined, } } diff --git a/site/src/pages/WorkspaceSchedulePage/schedule.ts b/site/src/pages/WorkspaceSchedulePage/schedule.ts index 6518872cb6..ef08da81e9 100644 --- a/site/src/pages/WorkspaceSchedulePage/schedule.ts +++ b/site/src/pages/WorkspaceSchedulePage/schedule.ts @@ -22,10 +22,9 @@ export interface AutoStartSchedule { timezone: string } -export interface AutoStart { - enabled: boolean - schedule: AutoStartSchedule -} +export type AutoStart = { + autoStartEnabled: boolean +} & AutoStartSchedule export const emptySchedule = { sunday: false, @@ -83,10 +82,10 @@ const transformSchedule = (schedule: string) => { export const scheduleToAutoStart = (schedule?: string): AutoStart => { if (schedule) { return { - enabled: true, - schedule: transformSchedule(schedule), + autoStartEnabled: true, + ...transformSchedule(schedule), } } else { - return { enabled: false, schedule: emptySchedule } + return { autoStartEnabled: false, ...emptySchedule } } } diff --git a/site/src/pages/WorkspaceSchedulePage/ttl.ts b/site/src/pages/WorkspaceSchedulePage/ttl.ts index 4035ab09ed..0d82563b64 100644 --- a/site/src/pages/WorkspaceSchedulePage/ttl.ts +++ b/site/src/pages/WorkspaceSchedulePage/ttl.ts @@ -1,5 +1,5 @@ export interface AutoStop { - enabled: boolean + autoStopEnabled: boolean ttl: number } @@ -10,4 +10,4 @@ export const defaultTTL = 8 const msToHours = (ms: number) => Math.round(ms / (1000 * 60 * 60)) export const ttlMsToAutoStop = (ttl_ms?: number): AutoStop => - ttl_ms ? { enabled: true, ttl: msToHours(ttl_ms) } : { enabled: false, ttl: 0 } + ttl_ms ? { autoStopEnabled: true, ttl: msToHours(ttl_ms) } : { autoStopEnabled: false, ttl: 0 }