Skip to content

Commit c83255e

Browse files
committed
🧹
1 parent f3c7c9b commit c83255e

File tree

4 files changed

+66
-91
lines changed

4 files changed

+66
-91
lines changed

site/src/pages/UserSettingsPage/SchedulePage/ScheduleForm.tsx

Lines changed: 11 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,11 @@ import {
1010
UpdateUserQuietHoursScheduleRequest,
1111
UserQuietHoursScheduleResponse,
1212
} from "api/typesGenerated";
13-
import cronParser from "cron-parser";
1413
import MenuItem from "@mui/material/MenuItem";
1514
import { Stack } from "components/Stack/Stack";
16-
import dayjs from "dayjs";
17-
import relativeTime from "dayjs/plugin/relativeTime";
18-
import timezone from "dayjs/plugin/timezone";
19-
import utc from "dayjs/plugin/utc";
20-
import { timeZones } from "utils/timeZones";
15+
import { timeZones, getPreferredTimezone } from "utils/timeZones";
2116
import { Alert } from "components/Alert/Alert";
22-
import { useQueryClient } from "@tanstack/react-query";
23-
24-
dayjs.extend(utc);
25-
import advancedFormat from "dayjs/plugin/advancedFormat";
26-
import duration from "dayjs/plugin/duration";
27-
import { userQuietHoursScheduleKey } from "api/queries/settings";
28-
dayjs.extend(advancedFormat);
29-
dayjs.extend(duration);
30-
dayjs.extend(timezone);
31-
dayjs.extend(relativeTime);
17+
import { timeToCron, quietHoursDisplay } from "utils/schedule";
3218

3319
export interface ScheduleFormValues {
3420
startTime: string;
@@ -41,14 +27,14 @@ const validationSchema = Yup.object({
4127
.test("is-time-string", "Time must be in HH:mm format.", (value) => {
4228
if (value === "") {
4329
return true;
44-
} else if (!/^[0-9][0-9]:[0-9][0-9]$/.test(value)) {
30+
}
31+
if (!/^[0-9][0-9]:[0-9][0-9]$/.test(value)) {
4532
return false;
46-
} else {
47-
const parts = value.split(":");
48-
const HH = Number(parts[0]);
49-
const mm = Number(parts[1]);
50-
return HH >= 0 && HH <= 23 && mm >= 0 && mm <= 59;
5133
}
34+
const parts = value.split(":");
35+
const HH = Number(parts[0]);
36+
const mm = Number(parts[1]);
37+
return HH >= 0 && HH <= 23 && mm >= 0 && mm <= 59;
5238
}),
5339
timezone: Yup.string().required(),
5440
});
@@ -71,10 +57,8 @@ export const ScheduleForm: FC<React.PropsWithChildren<ScheduleFormProps>> = ({
7157
onSubmit,
7258
now,
7359
}) => {
74-
// Force a re-render every 15 seconds to update the "Next occurrence" field.
75-
// The app re-renders by itself occasionally but this is just to be sure it
76-
// doesn't get stale.
77-
const [_, setTime] = useState<number>(Date.now());
60+
// Update every 15 seconds to update the "Next occurrence" field.
61+
const [, setTime] = useState<number>(Date.now());
7862
useEffect(() => {
7963
const interval = setInterval(() => setTime(Date.now()), 15000);
8064
return () => {
@@ -159,9 +143,8 @@ export const ScheduleForm: FC<React.PropsWithChildren<ScheduleFormProps>> = ({
159143
disabled
160144
fullWidth
161145
label="Next occurrence"
162-
value={formatNextRun(
146+
value={quietHoursDisplay(
163147
form.values.startTime,
164-
165148
form.values.timezone,
166149
now,
167150
)}
@@ -181,59 +164,3 @@ export const ScheduleForm: FC<React.PropsWithChildren<ScheduleFormProps>> = ({
181164
</Form>
182165
);
183166
};
184-
185-
const getPreferredTimezone = () => {
186-
return Intl.DateTimeFormat().resolvedOptions().timeZone;
187-
};
188-
189-
const timeToCron = (time: string, tz?: string) => {
190-
const [HH, mm] = time.split(":");
191-
let prefix = "";
192-
if (tz) {
193-
prefix = `CRON_TZ=${tz} `;
194-
}
195-
return `${prefix}${mm} ${HH} * * *`;
196-
};
197-
198-
// evaluateNextRun returns a Date object of the next cron run time.
199-
const evaluateNextRun = (
200-
time: string,
201-
tz: string,
202-
now: Date | undefined,
203-
): Date => {
204-
// The cron-parser package doesn't accept a timezone in the cron string, but
205-
// accepts it as an option.
206-
const cron = timeToCron(time);
207-
const parsed = cronParser.parseExpression(cron, {
208-
currentDate: now,
209-
iterator: false,
210-
utc: false,
211-
tz,
212-
});
213-
214-
return parsed.next().toDate();
215-
};
216-
217-
const formatNextRun = (
218-
time: string,
219-
tz: string,
220-
now: Date | undefined,
221-
): string => {
222-
const nowDjs = dayjs(now).tz(tz);
223-
const djs = dayjs(evaluateNextRun(time, tz, now)).tz(tz);
224-
let str = djs.format("h:mm A");
225-
if (djs.isSame(nowDjs, "day")) {
226-
str += " today";
227-
} else if (djs.isSame(nowDjs.add(1, "day"), "day")) {
228-
str += " tomorrow";
229-
} else {
230-
// This case will rarely ever be hit, as we're dealing with only times and
231-
// not dates, but it can be hit due to mismatched browser timezone to cron
232-
// timezone or due to daylight savings changes.
233-
str += ` on ${djs.format("dddd, MMMM D")}`;
234-
}
235-
236-
str += ` (${djs.from(now)})`;
237-
238-
return str;
239-
};

site/src/pages/UserSettingsPage/SchedulePage/SchedulePage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FC } from "react";
2-
import { Section } from "../../../components/SettingsLayout/Section";
2+
import { ErrorAlert } from "components/Alert/ErrorAlert";
3+
import { Section } from "components/SettingsLayout/Section";
34
import { ScheduleForm } from "./ScheduleForm";
45
import { useMe } from "hooks/useMe";
56
import { Loader } from "components/Loader/Loader";
@@ -9,7 +10,6 @@ import {
910
userQuietHoursSchedule,
1011
userQuietHoursScheduleKey,
1112
} from "api/queries/settings";
12-
import { ErrorAlert } from "components/Alert/ErrorAlert";
1313

1414
export const SchedulePage: FC = () => {
1515
const me = useMe();

site/src/utils/schedule.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import cronstrue from "cronstrue";
22
import dayjs, { Dayjs } from "dayjs";
3-
import advancedFormat from "dayjs/plugin/advancedFormat";
43
import duration from "dayjs/plugin/duration";
4+
import isToday from "dayjs/plugin/isToday";
5+
import isTomorrow from "dayjs/plugin/isTomorrow";
56
import relativeTime from "dayjs/plugin/relativeTime";
67
import timezone from "dayjs/plugin/timezone";
78
import utc from "dayjs/plugin/utc";
8-
import { Workspace } from "../api/typesGenerated";
9+
import { Workspace } from "api/typesGenerated";
910
import { isWorkspaceOn } from "./workspace";
11+
import cronParser from "cron-parser";
1012

1113
// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
1214
// sorted alphabetically.
1315
dayjs.extend(utc);
14-
dayjs.extend(advancedFormat);
1516
dayjs.extend(duration);
17+
dayjs.extend(isToday);
18+
dayjs.extend(isTomorrow);
1619
dayjs.extend(relativeTime);
1720
dayjs.extend(timezone);
18-
1921
/**
2022
* @fileoverview Client-side counterpart of the coderd/autostart/schedule Go
2123
* package. This package is a variation on crontab that uses minute, hour and
@@ -104,7 +106,7 @@ export const autostopDisplay = (workspace: Workspace): string => {
104106
if (isShuttingDown(workspace, deadline)) {
105107
return Language.workspaceShuttingDownLabel;
106108
} else {
107-
return deadline.tz(dayjs.tz.guess()).format("MMM D, YYYY h:mm A");
109+
return deadline.tz(dayjs.tz.guess()).format("MMMM D, YYYY h:mm A");
108110
}
109111
} else if (!ttl || ttl < 1) {
110112
// If the workspace is not on, and the ttl is 0 or undefined, then the
@@ -157,3 +159,46 @@ export const getMaxDeadlineChange = (
157159
deadline: dayjs.Dayjs,
158160
extremeDeadline: dayjs.Dayjs,
159161
): number => Math.abs(deadline.diff(extremeDeadline, "hours"));
162+
163+
export const timeToCron = (time: string, tz?: string) => {
164+
const [HH, mm] = time.split(":");
165+
let prefix = "";
166+
if (tz) {
167+
prefix = `CRON_TZ=${tz} `;
168+
}
169+
return `${prefix}${mm} ${HH} * * *`;
170+
};
171+
172+
export const quietHoursDisplay = (
173+
time: string,
174+
tz: string,
175+
now: Date | undefined,
176+
): string => {
177+
// The cron-parser package doesn't accept a timezone in the cron string, but
178+
// accepts it as an option.
179+
const cron = timeToCron(time);
180+
const parsed = cronParser.parseExpression(cron, {
181+
currentDate: now,
182+
iterator: false,
183+
utc: false,
184+
tz,
185+
});
186+
187+
const day = dayjs(parsed.next().toDate()).tz(tz);
188+
let display = day.format("h:mm A");
189+
190+
if (day.isToday()) {
191+
display += " today";
192+
} else if (day.isTomorrow()) {
193+
display += " tomorrow";
194+
} else {
195+
// This case will rarely ever be hit, as we're dealing with only times and
196+
// not dates, but it can be hit due to mismatched browser timezone to cron
197+
// timezone or due to daylight savings changes.
198+
display += ` on ${day.format("dddd, MMMM D")}`;
199+
}
200+
201+
display += ` (${day.from(now)})`;
202+
203+
return display;
204+
};

site/src/utils/timeZones.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import tzData from "tzdata";
22

33
export const timeZones = Object.keys(tzData.zones).sort();
4+
5+
export const getPreferredTimezone = () =>
6+
Intl.DateTimeFormat().resolvedOptions().timeZone;

0 commit comments

Comments
 (0)