Put switches in form, persist form state

This commit is contained in:
Presley Pizzo
2022-08-10 19:44:32 +00:00
parent bfbabe1213
commit d1420de03f
7 changed files with 197 additions and 213 deletions
@@ -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")
})
})
@@ -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<WorkspaceScheduleFormProps> = ({
submitScheduleError,
autoStart,
toggleAutoStart,
autoStop,
toggleAutoStop,
initialValues,
isLoading,
onCancel,
onSubmit,
initialTouched,
}) => {
const styles = useStyles()
const initialValues = { ...autoStart.schedule, ttl: autoStop.ttl }
const form = useFormik<WorkspaceScheduleFormValues>({
initialValues,
enableReinitialize: true,
onSubmit,
validationSchema: () => getValidationSchema(autoStart.enabled, autoStop.enabled),
validationSchema,
initialTouched,
})
const formHelpers = getFormHelpersWithError<WorkspaceScheduleFormValues>(
@@ -206,6 +201,27 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
{ 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 (
<FullPageForm onCancel={onCancel} title={Language.formTitle}>
<form onSubmit={form.handleSubmit} className={styles.form}>
@@ -213,12 +229,18 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
{submitScheduleError && <ErrorSummary error={submitScheduleError} />}
<Section title={Language.startSection}>
<FormControlLabel
control={<Switch checked={autoStart.enabled} onChange={toggleAutoStart} />}
control={
<Switch
name="autoStartEnabled"
checked={form.values.autoStartEnabled}
onChange={handleToggleAutoStart}
/>
}
label={Language.startSwitch}
/>
<TextField
{...formHelpers("startTime", Language.startTimeHelperText)}
disabled={isLoading || !autoStart.enabled}
disabled={isLoading || !form.values.autoStartEnabled}
InputLabelProps={{
shrink: true,
}}
@@ -229,7 +251,7 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
<TextField
{...formHelpers("timezone")}
disabled={isLoading || !autoStart.enabled}
disabled={isLoading || !form.values.autoStartEnabled}
InputLabelProps={{
shrink: true,
}}
@@ -255,7 +277,7 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
control={
<Checkbox
checked={checkbox.value}
disabled={isLoading || !autoStart.enabled}
disabled={isLoading || !form.values.autoStartEnabled}
onChange={form.handleChange}
name={checkbox.name}
color="primary"
@@ -275,12 +297,18 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
<Section title={Language.stopSection}>
<FormControlLabel
control={<Switch checked={autoStop.enabled} onChange={toggleAutoStop} />}
control={
<Switch
name="autoStopEnabled"
checked={form.values.autoStopEnabled}
onChange={handleToggleAutoStop}
/>
}
label={Language.stopSwitch}
/>
<TextField
{...formHelpers("ttl", ttlShutdownAt(form.values.ttl), "ttl_ms")}
disabled={isLoading || !autoStop.enabled}
disabled={isLoading || !form.values.autoStopEnabled}
inputProps={{ min: 0, step: 1 }}
label={Language.ttlLabel}
type="number"
@@ -8,6 +8,7 @@ import * as TypesGen from "../../api/typesGenerated"
import { WorkspaceScheduleFormValues } from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm"
const validValues: WorkspaceScheduleFormValues = {
autoStartEnabled: true,
sunday: false,
monday: true,
tuesday: true,
@@ -17,6 +18,7 @@ const validValues: WorkspaceScheduleFormValues = {
saturday: false,
startTime: "09:30",
timezone: "Canada/Eastern",
autoStopEnabled: true,
ttl: 120,
}
@@ -26,6 +28,7 @@ describe("WorkspaceSchedulePage", () => {
[
// 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)
})
@@ -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 <Navigate to="/workspaces" />
}
@@ -137,10 +87,7 @@ export const WorkspaceSchedulePage: React.FC = () => {
return (
<WorkspaceScheduleForm
submitScheduleError={submitScheduleError}
autoStart={autoStart}
toggleAutoStart={onToggleAutoStart}
autoStop={autoStop}
toggleAutoStop={onToggleAutoStop}
initialValues={{ ...autoStart, ...autoStop }}
isLoading={scheduleState.tags.has("loading")}
onCancel={() => {
navigate(`/@${username}/${workspaceName}`)
@@ -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,
}
}
@@ -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 }
}
}
+2 -2
View File
@@ -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 }