Skip to content

Commit 1166924

Browse files
committed
add timezone
1 parent 7a050db commit 1166924

File tree

6 files changed

+80
-9
lines changed

6 files changed

+80
-9
lines changed

site/src/components/WorkspaceStats/WorkspaceScheduleForm.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const valid: WorkspaceScheduleFormValues = {
1010
saturday: false,
1111

1212
startTime: "09:30",
13+
timezone: "Canada/Eastern",
1314
ttl: 120,
1415
}
1516

@@ -25,6 +26,7 @@ describe("validationSchema", () => {
2526
saturday: false,
2627

2728
startTime: "",
29+
timezone: "",
2830
ttl: 0,
2931
}
3032
const validate = () => validationSchema.validateSync(values)
@@ -99,4 +101,13 @@ describe("validationSchema", () => {
99101
const validate = () => validationSchema.validateSync(values)
100102
expect(validate).toThrowError(Language.errorTime)
101103
})
104+
105+
it("disallows an invalid timezone Canada/North", () => {
106+
const values: WorkspaceScheduleFormValues = {
107+
...valid,
108+
timezone: "Canada/North",
109+
}
110+
const validate = () => validationSchema.validateSync(values)
111+
expect(validate).toThrowError(Language.errorTimezone)
112+
})
102113
})

site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ import FormControlLabel from "@material-ui/core/FormControlLabel"
44
import FormGroup from "@material-ui/core/FormGroup"
55
import FormHelperText from "@material-ui/core/FormHelperText"
66
import FormLabel from "@material-ui/core/FormLabel"
7+
import Link from "@material-ui/core/Link"
78
import makeStyles from "@material-ui/core/styles/makeStyles"
89
import TextField from "@material-ui/core/TextField"
10+
import dayjs from "dayjs"
11+
import timezone from "dayjs/plugin/timezone"
12+
import utc from "dayjs/plugin/utc"
913
import { useFormik } from "formik"
1014
import React from "react"
1115
import * as Yup from "yup"
@@ -15,9 +19,16 @@ import { FormFooter } from "../FormFooter/FormFooter"
1519
import { FullPageForm } from "../FullPageForm/FullPageForm"
1620
import { Stack } from "../Stack/Stack"
1721

22+
// REMARK: timezone plugin depends on UTC
23+
//
24+
// SEE: https://day.js.org/docs/en/timezone/timezone
25+
dayjs.extend(utc)
26+
dayjs.extend(timezone)
27+
1828
export const Language = {
1929
errorNoDayOfWeek: "Must set at least one day of week",
2030
errorTime: "Invalid time",
31+
errorTimezone: "Invalid timezone",
2132
daysOfWeekLabel: "Days of Week",
2233
daySundayLabel: "Sunday",
2334
dayMondayLabel: "Monday",
@@ -28,6 +39,7 @@ export const Language = {
2839
daySaturdayLabel: "Saturday",
2940
startTimeLabel: "Start time",
3041
startTimeHelperText: "Your workspace will automatically start at this time.",
42+
timezoneLabel: "Timezone",
3143
ttlLabel: "Runtime (minutes)",
3244
ttlHelperText: "Your workspace will automatically shutdown after the runtime.",
3345
}
@@ -49,6 +61,7 @@ export interface WorkspaceScheduleFormValues {
4961
saturday: boolean
5062

5163
startTime: string
64+
timezone: string
5265
ttl: number
5366
}
5467

@@ -91,6 +104,25 @@ export const validationSchema = Yup.object({
91104
return HH >= 0 && HH <= 23 && mm >= 0 && mm <= 59
92105
}
93106
}),
107+
timezone: Yup.string()
108+
.ensure()
109+
.test("is-timezone", Language.errorTimezone, function (value) {
110+
const parent = this.parent as WorkspaceScheduleFormValues
111+
112+
if (!parent.startTime) {
113+
return true
114+
} else {
115+
// Unfortunately, there's not a good API on dayjs at this time for
116+
// evaluating a timezone. Attempt to parse today in the supplied timezone
117+
// and return as valid if the function doesn't throw.
118+
try {
119+
dayjs.tz(dayjs(), value)
120+
return true
121+
} catch (e) {
122+
return false
123+
}
124+
}
125+
}),
94126
ttl: Yup.number().min(0).integer(),
95127
})
96128

@@ -113,6 +145,7 @@ export const WorkspaceScheduleForm: React.FC<WorkspaceScheduleFormProps> = ({
113145
saturday: false,
114146

115147
startTime: "09:30",
148+
timezone: "",
116149
ttl: 120,
117150
},
118151
onSubmit,
@@ -145,6 +178,24 @@ export const WorkspaceScheduleForm: React.FC<WorkspaceScheduleFormProps> = ({
145178
variant="standard"
146179
/>
147180

181+
<TextField
182+
{...formHelpers(
183+
"timezone",
184+
<>
185+
Timezone must be a valid{" "}
186+
<Link href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List" target="_blank">
187+
tz database name
188+
</Link>
189+
</>,
190+
)}
191+
disabled={form.isSubmitting || isLoading || !form.values.startTime}
192+
InputLabelProps={{
193+
shrink: true,
194+
}}
195+
label={Language.timezoneLabel}
196+
variant="standard"
197+
/>
198+
148199
<FormControl component="fieldset" error={Boolean(form.errors.monday)}>
149200
<FormLabel className={styles.daysOfWeekLabel} component="legend">
150201
{Language.daysOfWeekLabel}

site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const validValues: WorkspaceScheduleFormValues = {
1111
friday: true,
1212
saturday: false,
1313
startTime: "09:30",
14+
timezone: "Canada/Eastern",
1415
ttl: 120,
1516
}
1617

@@ -28,6 +29,7 @@ describe("WorkspaceSchedulePage", () => {
2829
friday: false,
2930
saturday: false,
3031
startTime: "",
32+
timezone: "",
3133
ttl: 0,
3234
},
3335
{
@@ -45,10 +47,11 @@ describe("WorkspaceSchedulePage", () => {
4547
friday: false,
4648
saturday: false,
4749
startTime: "16:20",
50+
timezone: "Canada/Eastern",
4851
ttl: 120,
4952
},
5053
{
51-
schedule: "20 16 * * 0",
54+
schedule: "CRON_TZ=Canada/Eastern 20 16 * * 0",
5255
},
5356
],
5457
[
@@ -62,10 +65,11 @@ describe("WorkspaceSchedulePage", () => {
6265
friday: true,
6366
saturday: false,
6467
startTime: "09:30",
68+
timezone: "America/Central",
6569
ttl: 120,
6670
},
6771
{
68-
schedule: "30 09 * * 1-5",
72+
schedule: "CRON_TZ=America/Central 30 09 * * 1-5",
6973
},
7074
],
7175
[
@@ -79,6 +83,7 @@ describe("WorkspaceSchedulePage", () => {
7983
friday: true,
8084
saturday: true,
8185
startTime: "09:00",
86+
timezone: "",
8287
ttl: 60 * 8,
8388
},
8489
{
@@ -96,6 +101,7 @@ describe("WorkspaceSchedulePage", () => {
96101
friday: true,
97102
saturday: false,
98103
startTime: "16:20",
104+
timezone: "",
99105
ttl: 60 * 3,
100106
},
101107
{

site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ export const formValuesToAutoStartRequest = (
2222

2323
const [HH, mm] = values.startTime.split(":")
2424

25-
const makeCronString = (dow: string) => `${mm} ${HH} * * ${dow}`
25+
// Note: Space after CRON_TZ if timezone is defined
26+
const preparedTZ = values.timezone ? `CRON_TZ=${values.timezone} ` : ""
27+
28+
const makeCronString = (dow: string) => `${preparedTZ}${mm} ${HH} * * ${dow}`
2629

2730
const days = [
2831
values.sunday,
File renamed without changes.

site/src/util/formUtils.ts renamed to site/src/util/formUtils.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FormikContextType, FormikErrors, getIn } from "formik"
2-
import { ChangeEvent, ChangeEventHandler, FocusEventHandler } from "react"
2+
import React, { ChangeEvent, ChangeEventHandler, FocusEventHandler } from "react"
33

44
interface FormHelpers {
55
name: string
@@ -8,12 +8,12 @@ interface FormHelpers {
88
id: string
99
value?: string | number
1010
error: boolean
11-
helperText?: string
11+
helperText?: React.ReactNode
1212
}
1313

1414
export const getFormHelpers =
15-
<T>(form: FormikContextType<T>, formErrors?: FormikErrors<T>) =>
16-
(name: keyof T, helperText = ""): FormHelpers => {
15+
<T,>(form: FormikContextType<T>, formErrors?: FormikErrors<T>) =>
16+
(name: keyof T, HelperText: React.ReactNode = ""): FormHelpers => {
1717
if (typeof name !== "string") {
1818
throw new Error(`name must be type of string, instead received '${typeof name}'`)
1919
}
@@ -28,12 +28,12 @@ export const getFormHelpers =
2828
...form.getFieldProps(name),
2929
id: name,
3030
error: touched && Boolean(error),
31-
helperText: touched ? error || helperText : helperText,
31+
helperText: touched ? error || HelperText : HelperText,
3232
}
3333
}
3434

3535
export const onChangeTrimmed =
36-
<T>(form: FormikContextType<T>) =>
36+
<T,>(form: FormikContextType<T>) =>
3737
(event: ChangeEvent<HTMLInputElement>): void => {
3838
event.target.value = event.target.value.trim()
3939
form.handleChange(event)

0 commit comments

Comments
 (0)