diff --git a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx index 1a5d35a499f11..5d5d1db960c5c 100644 --- a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx +++ b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx @@ -17,8 +17,10 @@ import { stripTimezone } from "../../util/schedule" import { isWorkspaceOn } from "../../util/workspace" import { Stack } from "../Stack/Stack" -dayjs.extend(advancedFormat) +// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're +// sorted alphabetically. dayjs.extend(utc) +dayjs.extend(advancedFormat) dayjs.extend(duration) dayjs.extend(relativeTime) dayjs.extend(timezone) @@ -50,7 +52,7 @@ export const Language = { if (now.isAfter(deadline)) { return "Workspace is shutting down" } else { - return deadline.tz(dayjs.tz.guess()).format("hh:mm A") + return deadline.tz(dayjs.tz.guess()).format("MMM D, YYYY h:mm A") } } else if (!ttl || ttl < 1) { // If the workspace is not on, and the ttl is 0 or undefined, then the diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx index c9ea6eafa8b9d..5c3126fc74248 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx @@ -22,7 +22,7 @@ export const WorkspaceNotRunning = Template.bind({}) WorkspaceNotRunning.args = { now: dayjs("2022-05-17T17:40:00Z"), initialValues: { - ...defaultWorkspaceSchedule(5, "asdfasdf"), + ...defaultWorkspaceSchedule(5), timezone: "UTC", }, workspace: { @@ -41,7 +41,7 @@ export const WorkspaceWillNotShutDown = Template.bind({}) WorkspaceWillNotShutDown.args = { now: dayjs("2022-05-17T17:40:00Z"), initialValues: { - ...defaultWorkspaceSchedule(5, "asdfasdf"), + ...defaultWorkspaceSchedule(5), timezone: "UTC", ttl: 0, }, @@ -60,7 +60,7 @@ export const WorkspaceWillShutdown = Template.bind({}) WorkspaceWillShutdown.args = { now: dayjs("2022-05-17T17:40:00Z"), initialValues: { - ...defaultWorkspaceSchedule(5, "asdfasdf"), + ...defaultWorkspaceSchedule(5), timezone: "UTC", }, workspace: { @@ -76,9 +76,9 @@ WorkspaceWillShutdown.args = { export const WorkspaceWillShutdownSoon = Template.bind({}) WorkspaceWillShutdownSoon.args = { - now: dayjs("2022-05-17T18:10:00Z"), + now: dayjs("2022-05-17T16:39:00Z"), initialValues: { - ...defaultWorkspaceSchedule(5, "asdfasdf"), + ...defaultWorkspaceSchedule(2), timezone: "UTC", ttl: 1, }, @@ -86,8 +86,9 @@ WorkspaceWillShutdownSoon.args = { ...Mocks.MockWorkspace, latest_build: { ...Mocks.MockWorkspaceBuild, - updated_at: "2022-05-17T17:39:00Z", + deadline: "2022-05-17T18:09:00Z", }, + ttl_ms: 2 * 60 * 60 * 1000, // 2 hours = shuts off at 18:09 }, onCancel: () => action("onCancel"), onSubmit: () => action("onSubmit"), @@ -95,9 +96,9 @@ WorkspaceWillShutdownSoon.args = { export const WorkspaceWillShutdownImmediately = Template.bind({}) WorkspaceWillShutdownImmediately.args = { - now: dayjs("2022-05-17T18:40:00Z"), + now: dayjs("2022-05-17T17:09:00Z"), initialValues: { - ...defaultWorkspaceSchedule(5, "asdfasdf"), + ...defaultWorkspaceSchedule(1), timezone: "UTC", ttl: 1, }, @@ -105,8 +106,9 @@ WorkspaceWillShutdownImmediately.args = { ...Mocks.MockWorkspace, latest_build: { ...Mocks.MockWorkspaceBuild, - updated_at: "2022-05-17T17:39:00Z", + deadline: "2022-05-17T18:09:00Z", }, + ttl_ms: 2 * 60 * 60 * 1000, // 2 hours = shuts off at 18:09 }, onCancel: () => action("onCancel"), onSubmit: () => action("onSubmit"), diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts index 8fa25cc66abd3..3e37484b9c09b 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.test.ts @@ -160,31 +160,99 @@ describe("validationSchema", () => { }) describe("ttlShutdownAt", () => { - it.each<[dayjs.Dayjs, Workspace, string, number, string]>([ - [dayjs("2022-05-17T18:09:00Z"), Mocks.MockStoppedWorkspace, "America/Chicago", 1, Language.ttlHelperText], - [dayjs("2022-05-17T18:09:00Z"), Mocks.MockWorkspace, "America/Chicago", 0, Language.ttlCausesNoShutdownHelperText], + it.each<[string, dayjs.Dayjs, Workspace, string, number, string]>([ [ + "Workspace is stopped --> helper text", dayjs("2022-05-17T18:09:00Z"), - Mocks.MockWorkspace, + Mocks.MockStoppedWorkspace, "America/Chicago", 1, - `${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownAt} 01:39 PM CDT.`, + Language.ttlHelperText, ], [ - dayjs("2022-05-17T18:10:00Z"), + "TTL is not modified --> helper text", + dayjs("2022-05-17T16:09:00Z"), + { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspaceBuild, + deadline: "2022-05-17T18:09:00Z", + }, + ttl_ms: 2 * 60 * 60 * 1000, // 2 hours = shuts off at 18:09 + }, + "America/Chicago", + 2, + Language.ttlHelperText, + ], + [ + "TTL becomes 0 --> manual helper text", + dayjs("2022-05-17T18:09:00Z"), Mocks.MockWorkspace, "America/Chicago", + 0, + Language.ttlCausesNoShutdownHelperText, + ], + [ + "Deadline of 18:09 becomes 17:09 at 17:09 --> immediate shutdown", + dayjs("2022-05-17T17:09:00Z"), + { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspaceBuild, + deadline: "2022-05-17T18:09:00Z", + }, + ttl_ms: 2 * 60 * 60 * 1000, // 2 hours = shuts off at 18:09 + }, + "America/Chicago", + 1, + `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownImmediately} ⚠️`, + ], + [ + "Deadline of 18:09 becomes 17:09 at 16:39 --> display shutdown soon", + dayjs("2022-05-17T16:39:00Z"), + { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspaceBuild, + deadline: "2022-05-17T18:09:00Z", + }, + ttl_ms: 2 * 60 * 60 * 1000, // 2 hours = shuts off at 18:09 + }, + "America/Chicago", 1, `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownSoon} ⚠️`, ], [ - dayjs("2022-05-17T18:40:00Z"), - Mocks.MockWorkspace, + "Deadline of 18:09 becomes 17:09 at 16:09 --> display 12:09 CDT", + dayjs("2022-05-17T16:09:00Z"), + { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspaceBuild, + deadline: "2022-05-17T18:09:00Z", + }, + ttl_ms: 2 * 60 * 60 * 1000, // 2 hours = shuts off at 18:09 + }, "America/Chicago", 1, - `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownImmediately} ⚠️`, + `${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownAt} May 17, 2022 12:09 PM.`, + ], + [ + "Manual workspace gets new deadline of 18:09 at 17:09 --> display 1:09 CDT", + dayjs("2022-05-17T17:09:00Z"), + { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspaceBuild, + deadline: "0001-01-01T00:00:00Z", + }, + ttl_ms: 0, + }, + "America/Chicago", + 1, + `${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownAt} May 17, 2022 1:09 PM.`, ], - ])("ttlShutdownAt(%p, %p, %p, %p) returns %p", (now, workspace, timezone, ttlHours, expected) => { + ])("%p", (_, now, workspace, timezone, ttlHours, expected) => { expect(ttlShutdownAt(now, workspace, timezone, ttlHours)).toEqual(expected) }) }) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx index e5d5376596c94..efd2c78199e34 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx @@ -9,6 +9,7 @@ import makeStyles from "@material-ui/core/styles/makeStyles" import TextField from "@material-ui/core/TextField" import dayjs from "dayjs" import advancedFormat from "dayjs/plugin/advancedFormat" +import isSameOrBefore from "dayjs/plugin/isSameOrBefore" import timezone from "dayjs/plugin/timezone" import utc from "dayjs/plugin/utc" import { useFormik } from "formik" @@ -23,11 +24,11 @@ import { FullPageForm } from "../FullPageForm/FullPageForm" import { Stack } from "../Stack/Stack" import { zones } from "./zones" -// REMARK: timezone plugin depends on UTC -// -// SEE: https://day.js.org/docs/en/timezone/timezone -dayjs.extend(advancedFormat) +// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're +// sorted alphabetically. dayjs.extend(utc) +dayjs.extend(advancedFormat) +dayjs.extend(isSameOrBefore) dayjs.extend(timezone) export const Language = { @@ -282,19 +283,29 @@ export const WorkspaceScheduleForm: FC = ({ ) } -export const ttlShutdownAt = (now: dayjs.Dayjs, workspace: Workspace, tz: string, newTTL: number): string => { - const newDeadline = dayjs(workspace.latest_build.updated_at).add(newTTL, "hour") - if (!isWorkspaceOn(workspace)) { +export const ttlShutdownAt = (now: dayjs.Dayjs, workspace: Workspace, tz: string, formTTL: number): string => { + // a manual shutdown has a deadline of '"0001-01-01T00:00:00Z"' + // SEE: #1834 + const deadline = dayjs(workspace.latest_build.deadline).utc() + const hasDeadline = deadline.year() > 1 + const ttl = workspace.ttl_ms ? workspace.ttl_ms / (1000 * 60 * 60) : 0 + const delta = formTTL - ttl + + if (delta === 0 || !isWorkspaceOn(workspace)) { return Language.ttlHelperText - } else if (newTTL === 0) { + } else if (formTTL === 0) { return Language.ttlCausesNoShutdownHelperText - } else if (newDeadline.isBefore(now)) { - return `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownImmediately} ⚠️` - } else if (newDeadline.isBefore(now.add(30, "minute"))) { - return `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownSoon} ⚠️` } else { - const newDeadlineString = newDeadline.tz(tz).format("hh:mm A z") - return `${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownAt} ${newDeadlineString}.` + const newDeadline = dayjs(hasDeadline ? deadline : now).add(delta, "hours") + if (newDeadline.isSameOrBefore(now)) { + return `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownImmediately} ⚠️` + } else if (newDeadline.isSameOrBefore(now.add(30, "minutes"))) { + return `⚠️ ${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownSoon} ⚠️` + } else { + return `${Language.ttlCausesShutdownHelperText} ${Language.ttlCausesShutdownAt} ${newDeadline + .tz(tz) + .format("MMM D, YYYY h:mm A")}.` + } } }