From 42de7e5caa95300af658f4f52673290b3dbe4eba Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 13 Dec 2022 18:55:59 +0000 Subject: [PATCH 01/11] Check template default ttl while setting max --- site/src/util/schedule.ts | 14 ++++++++++---- .../workspaceScheduleBannerXService.ts | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/site/src/util/schedule.ts b/site/src/util/schedule.ts index cd325f2006f4c..06ef9adbcf1c9 100644 --- a/site/src/util/schedule.ts +++ b/site/src/util/schedule.ts @@ -143,11 +143,17 @@ export function getMaxDeadline( throw Error("Cannot calculate max deadline because workspace is undefined") } const startedAt = dayjs(ws.latest_build.updated_at) - const maxTemplateDeadline = startedAt.add( - dayjs.duration(tpl.default_ttl_ms, "milliseconds"), - ) const maxGlobalDeadline = startedAt.add(deadlineExtensionMax) - return dayjs.min(maxTemplateDeadline, maxGlobalDeadline) + // only consider template default if it is defined and > 0, because undefined or 0 + // means stop never, not stop immediately + if (tpl.default_ttl_ms) { + const maxTemplateDeadline = startedAt.add( + dayjs.duration(tpl.default_ttl_ms, "milliseconds"), + ) + return dayjs.min(maxTemplateDeadline, maxGlobalDeadline) + } else { + return maxGlobalDeadline + } } /** diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts index 5db341240f22b..57a5681c2d5e8 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts @@ -171,6 +171,7 @@ export const workspaceScheduleBannerMachine = createMachine( proposedDeadline, getMaxDeadline(context.workspace, context.template), ) + console.log(getMaxDeadline(context.workspace, context.template), proposedDeadline, newDeadline) await API.putWorkspaceExtension(context.workspace.id, newDeadline) }, decreaseDeadline: async (context, event) => { From 5e8f6d6c5b8294e57a98a5d45a8035813f5c45b6 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 13 Dec 2022 20:01:42 +0000 Subject: [PATCH 02/11] Lint --- .../workspaceSchedule/workspaceScheduleBannerXService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts index 57a5681c2d5e8..5db341240f22b 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts @@ -171,7 +171,6 @@ export const workspaceScheduleBannerMachine = createMachine( proposedDeadline, getMaxDeadline(context.workspace, context.template), ) - console.log(getMaxDeadline(context.workspace, context.template), proposedDeadline, newDeadline) await API.putWorkspaceExtension(context.workspace.id, newDeadline) }, decreaseDeadline: async (context, event) => { From 7c8d7fbfbcfaca0dddac56d150e6f60c35580a25 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 15 Dec 2022 20:17:08 +0000 Subject: [PATCH 03/11] Remove template default from max ttl consideration --- .../WorkspacePage/WorkspaceReadyPage.tsx | 5 ++-- site/src/util/schedule.test.ts | 22 +++++------------- site/src/util/schedule.ts | 23 ++++--------------- .../xServices/workspace/workspaceXService.ts | 1 - .../workspaceScheduleBannerXService.ts | 6 ++--- 5 files changed, 14 insertions(+), 43 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index ab293db819533..579605205a8fb 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -106,10 +106,9 @@ export const WorkspaceReadyPage = ({ maxDeadlineDecrease: deadline ? getMaxDeadlineChange(deadline, getMinDeadline()) : 0, - maxDeadlineIncrease: - deadline && template + maxDeadlineIncrease: deadline ? getMaxDeadlineChange( - getMaxDeadline(workspace, template), + getMaxDeadline(workspace), deadline, ) : 0, diff --git a/site/src/util/schedule.test.ts b/site/src/util/schedule.test.ts index 43b0306203490..34828399e732f 100644 --- a/site/src/util/schedule.test.ts +++ b/site/src/util/schedule.test.ts @@ -2,7 +2,7 @@ import dayjs from "dayjs" import duration from "dayjs/plugin/duration" import { emptySchedule } from "pages/WorkspaceSchedulePage/schedule" import { emptyTTL } from "pages/WorkspaceSchedulePage/ttl" -import { Template, Workspace } from "../api/typesGenerated" +import { Workspace } from "../api/typesGenerated" import * as Mocks from "../testHelpers/entities" import { canExtendDeadline, @@ -53,29 +53,20 @@ describe("maxDeadline", () => { } describe("given a template with 25 hour max ttl", () => { it("should be never be greater than global max deadline", () => { - const template: Template = { - ...Mocks.MockTemplate, - default_ttl_ms: 25 * 60 * 60 * 1000, - } - // Then: deadlineMinusDisabled should be falsy - const delta = getMaxDeadline(workspace, template).diff(now) + const delta = getMaxDeadline(workspace).diff(now) expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) }) }) describe("given a template with 4 hour max ttl", () => { it("should be never be greater than global max deadline", () => { - const template: Template = { - ...Mocks.MockTemplate, - default_ttl_ms: 4 * 60 * 60 * 1000, - } - // Then: deadlineMinusDisabled should be falsy - const delta = getMaxDeadline(workspace, template).diff(now) + const delta = getMaxDeadline(workspace).diff(now) expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) }) }) + }) describe("minDeadline", () => { @@ -91,7 +82,6 @@ describe("canExtendDeadline", () => { canExtendDeadline( dayjs().add(25, "hours"), Mocks.MockWorkspace, - Mocks.MockTemplate, ), ).toBeFalsy() }) @@ -101,7 +91,7 @@ describe("canExtendDeadline", () => { dayjs.duration(Mocks.MockTemplate.default_ttl_ms, "milliseconds"), ) expect( - canExtendDeadline(tooFarAhead, Mocks.MockWorkspace, Mocks.MockTemplate), + canExtendDeadline(tooFarAhead, Mocks.MockWorkspace), ).toBeFalsy() }) @@ -110,7 +100,7 @@ describe("canExtendDeadline", () => { dayjs.duration(Mocks.MockTemplate.default_ttl_ms / 2, "milliseconds"), ) expect( - canExtendDeadline(okDeadline, Mocks.MockWorkspace, Mocks.MockTemplate), + canExtendDeadline(okDeadline, Mocks.MockWorkspace), ).toBeFalsy() }) }) diff --git a/site/src/util/schedule.ts b/site/src/util/schedule.ts index 06ef9adbcf1c9..247cd88319d70 100644 --- a/site/src/util/schedule.ts +++ b/site/src/util/schedule.ts @@ -7,7 +7,7 @@ import duration from "dayjs/plugin/duration" import relativeTime from "dayjs/plugin/relativeTime" import timezone from "dayjs/plugin/timezone" import utc from "dayjs/plugin/utc" -import { Template, Workspace } from "../api/typesGenerated" +import { Workspace } from "../api/typesGenerated" import { isWorkspaceOn } from "./workspace" import { WorkspaceScheduleFormValues } from "components/WorkspaceScheduleForm/WorkspaceScheduleForm" import { AutoStop } from "pages/WorkspaceSchedulePage/ttl" @@ -127,15 +127,12 @@ export const deadlineExtensionMin = dayjs.duration(30, "minutes") export const deadlineExtensionMax = dayjs.duration(24, "hours") /** - * Depends on the time the workspace was last updated, the template config, - * and a global constant. + * Depends on the time the workspace was last updated and a global constant. * @param ws workspace - * @param tpl template * @returns the latest datetime at which the workspace can be automatically shut down. */ export function getMaxDeadline( ws: Workspace | undefined, - tpl: Template, ): dayjs.Dayjs { // note: we count runtime from updated_at as started_at counts from the start of // the workspace build process, which can take a while. @@ -143,17 +140,7 @@ export function getMaxDeadline( throw Error("Cannot calculate max deadline because workspace is undefined") } const startedAt = dayjs(ws.latest_build.updated_at) - const maxGlobalDeadline = startedAt.add(deadlineExtensionMax) - // only consider template default if it is defined and > 0, because undefined or 0 - // means stop never, not stop immediately - if (tpl.default_ttl_ms) { - const maxTemplateDeadline = startedAt.add( - dayjs.duration(tpl.default_ttl_ms, "milliseconds"), - ) - return dayjs.min(maxTemplateDeadline, maxGlobalDeadline) - } else { - return maxGlobalDeadline - } + return startedAt.add(deadlineExtensionMax) } /** @@ -167,9 +154,8 @@ export function getMinDeadline(): dayjs.Dayjs { export function canExtendDeadline( deadline: dayjs.Dayjs, workspace: Workspace, - template: Template, ): boolean { - return deadline < getMaxDeadline(workspace, template) + return deadline < getMaxDeadline(workspace) } export function canReduceDeadline(deadline: dayjs.Dayjs): boolean { @@ -183,7 +169,6 @@ export const getDeadline = (workspace: Workspace): dayjs.Dayjs => * Get number of hours you can add or subtract to the current deadline before hitting the max or min deadline. * @param deadline * @param workspace - * @param template * @returns number, in hours */ export const getMaxDeadlineChange = ( diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 18480de9ccacb..ec6cad2bae0b7 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -470,7 +470,6 @@ export const workspaceMachine = createMachine( src: workspaceScheduleBannerMachine, data: { workspace: (context: WorkspaceContext) => context.workspace, - template: (context: WorkspaceContext) => context.template, }, }, }, diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts index 5db341240f22b..935e073c4a627 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts @@ -3,7 +3,7 @@ * presented as an Alert/banner, for reactively updating a workspace schedule. */ import { getErrorMessage } from "api/errors" -import { Template, Workspace } from "api/typesGenerated" +import { Workspace } from "api/typesGenerated" import dayjs from "dayjs" import minMax from "dayjs/plugin/minMax" import { @@ -29,7 +29,6 @@ export const Language = { export interface WorkspaceScheduleBannerContext { workspace: Workspace - template: Template deadline?: dayjs.Dayjs } @@ -141,7 +140,6 @@ export const workspaceScheduleBannerMachine = createMachine( ? !canExtendDeadline( context.deadline, context.workspace, - context.template, ) : false, isAtMinDeadline: (context) => @@ -169,7 +167,7 @@ export const workspaceScheduleBannerMachine = createMachine( const proposedDeadline = context.deadline.add(event.hours, "hours") const newDeadline = dayjs.min( proposedDeadline, - getMaxDeadline(context.workspace, context.template), + getMaxDeadline(context.workspace), ) await API.putWorkspaceExtension(context.workspace.id, newDeadline) }, From 4634118d692a5ff2ccf1d1dde9ca9d925850d67d Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 15 Dec 2022 20:27:21 +0000 Subject: [PATCH 04/11] Finish removing template --- site/src/util/schedule.test.ts | 42 +++++----------------------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/site/src/util/schedule.test.ts b/site/src/util/schedule.test.ts index 34828399e732f..59015834c862f 100644 --- a/site/src/util/schedule.test.ts +++ b/site/src/util/schedule.test.ts @@ -19,6 +19,7 @@ import { dayjs.extend(duration) const now = dayjs() +const startTime = dayjs(Mocks.MockWorkspaceBuild.updated_at) describe("util/schedule", () => { describe("stripTimezone", () => { @@ -43,30 +44,17 @@ describe("util/schedule", () => { }) describe("maxDeadline", () => { - // Given: a workspace built from a template with a max deadline equal to 25 hours which isn't really possible const workspace: Workspace = { ...Mocks.MockWorkspace, latest_build: { ...Mocks.MockWorkspaceBuild, - deadline: now.add(8, "hours").utc().format(), + deadline: startTime.add(8, "hours").utc().format(), }, } - describe("given a template with 25 hour max ttl", () => { - it("should be never be greater than global max deadline", () => { - // Then: deadlineMinusDisabled should be falsy - const delta = getMaxDeadline(workspace).diff(now) - expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) - }) - }) - - describe("given a template with 4 hour max ttl", () => { - it("should be never be greater than global max deadline", () => { - // Then: deadlineMinusDisabled should be falsy - const delta = getMaxDeadline(workspace).diff(now) - expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) - }) + it("should be 24 hours from the workspace start time", () => { + const delta = getMaxDeadline(workspace).diff(startTime) + expect(delta).toEqual(deadlineExtensionMax.asMilliseconds()) }) - }) describe("minDeadline", () => { @@ -80,29 +68,11 @@ describe("canExtendDeadline", () => { it("should be falsy if the deadline is more than 24 hours in the future", () => { expect( canExtendDeadline( - dayjs().add(25, "hours"), + startTime.add(25, "hours"), Mocks.MockWorkspace, ), ).toBeFalsy() }) - - it("should be falsy if the deadline is more than the template max_ttl", () => { - const tooFarAhead = dayjs().add( - dayjs.duration(Mocks.MockTemplate.default_ttl_ms, "milliseconds"), - ) - expect( - canExtendDeadline(tooFarAhead, Mocks.MockWorkspace), - ).toBeFalsy() - }) - - it("should be truth if the deadline is within the template max_ttl", () => { - const okDeadline = dayjs().add( - dayjs.duration(Mocks.MockTemplate.default_ttl_ms / 2, "milliseconds"), - ) - expect( - canExtendDeadline(okDeadline, Mocks.MockWorkspace), - ).toBeFalsy() - }) }) describe("canReduceDeadline", () => { From 35a77fb122bb08cabb4b2e797cdb19def6b0146c Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Fri, 16 Dec 2022 23:07:04 +0000 Subject: [PATCH 05/11] Fix disabling buttons --- .../Workspace/Workspace.stories.tsx | 1 - site/src/components/Workspace/Workspace.tsx | 4 --- .../WorkspaceScheduleButton.tsx | 10 +++--- .../WorkspacePage/WorkspaceReadyPage.tsx | 21 +++++-------- site/src/util/schedule.test.ts | 31 +++++++++++++++---- site/src/util/schedule.ts | 18 +++++++++-- .../workspaceScheduleBannerXService.ts | 24 +++++++------- 7 files changed, 65 insertions(+), 44 deletions(-) diff --git a/site/src/components/Workspace/Workspace.stories.tsx b/site/src/components/Workspace/Workspace.stories.tsx index 0c9dda55344f4..ec7fbae6831ec 100644 --- a/site/src/components/Workspace/Workspace.stories.tsx +++ b/site/src/components/Workspace/Workspace.stories.tsx @@ -29,7 +29,6 @@ Running.args = { return canExtendDeadline( dayjs(Mocks.MockWorkspace.latest_build.deadline), Mocks.MockWorkspace, - Mocks.MockTemplate, ) }, maxDeadlineDecrease: 1000, diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 0634f10a07a4e..d9f3728976ab4 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -35,8 +35,6 @@ export interface WorkspaceProps { scheduleProps: { onDeadlinePlus: (hours: number) => void onDeadlineMinus: (hours: number) => void - deadlinePlusEnabled: () => boolean - deadlineMinusEnabled: () => boolean maxDeadlineIncrease: number maxDeadlineDecrease: number } @@ -131,8 +129,6 @@ export const Workspace: FC> = ({ workspace={workspace} onDeadlineMinus={scheduleProps.onDeadlineMinus} onDeadlinePlus={scheduleProps.onDeadlinePlus} - deadlineMinusEnabled={scheduleProps.deadlineMinusEnabled} - deadlinePlusEnabled={scheduleProps.deadlinePlusEnabled} maxDeadlineDecrease={scheduleProps.maxDeadlineDecrease} maxDeadlineIncrease={scheduleProps.maxDeadlineIncrease} canUpdateWorkspace={canUpdateWorkspace} diff --git a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx index b613786910ca6..de002d530129b 100644 --- a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx +++ b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx @@ -48,8 +48,6 @@ export interface WorkspaceScheduleButtonProps { workspace: Workspace onDeadlinePlus: (hours: number) => void onDeadlineMinus: (hours: number) => void - deadlineMinusEnabled: () => boolean - deadlinePlusEnabled: () => boolean maxDeadlineIncrease: number maxDeadlineDecrease: number canUpdateWorkspace: boolean @@ -63,8 +61,6 @@ export const WorkspaceScheduleButton: React.FC< workspace, onDeadlinePlus, onDeadlineMinus, - deadlinePlusEnabled, - deadlineMinusEnabled, maxDeadlineDecrease, maxDeadlineIncrease, canUpdateWorkspace, @@ -75,6 +71,8 @@ export const WorkspaceScheduleButton: React.FC< const [editMode, setEditMode] = useState("off") const id = isOpen ? "schedule-popover" : undefined const styles = useStyles({ editMode }) + const deadlinePlusEnabled = maxDeadlineIncrease >= 1 + const deadlineMinusEnabled = maxDeadlineDecrease >= 1 const onClose = () => { setIsOpen(false) @@ -108,7 +106,7 @@ export const WorkspaceScheduleButton: React.FC< { setEditMode("subtract") }} @@ -122,7 +120,7 @@ export const WorkspaceScheduleButton: React.FC< { setEditMode("add") }} diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 579605205a8fb..bbe64ac10212f 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -6,6 +6,7 @@ import { Helmet } from "react-helmet-async" import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" import { + getDeadline, getMaxDeadline, getMaxDeadlineChange, getMinDeadline, @@ -37,10 +38,9 @@ export const WorkspaceReadyPage = ({ quotaState, workspaceSend, }: WorkspaceReadyPageProps): JSX.Element => { - const [bannerState, bannerSend] = useActor( + const [_, bannerSend] = useActor( workspaceState.children["scheduleBannerMachine"], ) - const deadline = bannerState.context.deadline const xServices = useContext(XServiceContext) const featureVisibility = useSelector( xServices.entitlementsXService, @@ -61,6 +61,7 @@ export const WorkspaceReadyPage = ({ if (workspace === undefined) { throw Error("Workspace is undefined") } + const deadline = getDeadline(workspace) const canUpdateWorkspace = Boolean(permissions?.updateWorkspace) const { t } = useTranslation("workspacePage") const favicon = getFaviconByStatus(workspace.latest_build) @@ -101,17 +102,11 @@ export const WorkspaceReadyPage = ({ hours, }) }, - deadlineMinusEnabled: () => !bannerState.matches("atMinDeadline"), - deadlinePlusEnabled: () => !bannerState.matches("atMaxDeadline"), - maxDeadlineDecrease: deadline - ? getMaxDeadlineChange(deadline, getMinDeadline()) - : 0, - maxDeadlineIncrease: deadline - ? getMaxDeadlineChange( - getMaxDeadline(workspace), - deadline, - ) - : 0, + maxDeadlineDecrease: getMaxDeadlineChange(deadline, getMinDeadline()), + maxDeadlineIncrease: getMaxDeadlineChange( + getMaxDeadline(workspace), + deadline, + ) }} isUpdating={workspaceState.hasTag("updating")} workspace={workspace} diff --git a/site/src/util/schedule.test.ts b/site/src/util/schedule.test.ts index 59015834c862f..d9ead6930551d 100644 --- a/site/src/util/schedule.test.ts +++ b/site/src/util/schedule.test.ts @@ -65,7 +65,7 @@ describe("minDeadline", () => { }) describe("canExtendDeadline", () => { - it("should be falsy if the deadline is more than 24 hours in the future", () => { + it("should be falsy if the deadline is more than 24 hours from the start time", () => { expect( canExtendDeadline( startTime.add(25, "hours"), @@ -73,18 +73,37 @@ describe("canExtendDeadline", () => { ), ).toBeFalsy() }) + it("should be falsy if the deadline is less than an hour below the max deadline", () => { + expect( + canExtendDeadline( + startTime.add(23.5, "hours"), + Mocks.MockWorkspace + ) + ).toBeFalsy() + }) + it("should be true if the deadline is one hour below the max deadline", () => { + expect( + canExtendDeadline( + startTime.add(23, "hours"), + Mocks.MockWorkspace + ) + ).toBeTruthy() + }) }) describe("canReduceDeadline", () => { - it("should be falsy if the deadline is 30 minutes or less in the future", () => { + // the minimum ttl is 30 minutes from the current time + // ttl can be reduced by one hour at a time + // so current deadline must be >=90 minutes from current time to be reducible + it("should be falsy if the deadline is 90 minutes or less in the future", () => { expect(canReduceDeadline(dayjs())).toBeFalsy() expect(canReduceDeadline(dayjs().add(1, "minutes"))).toBeFalsy() - expect(canReduceDeadline(dayjs().add(29, "minutes"))).toBeFalsy() - expect(canReduceDeadline(dayjs().add(30, "minutes"))).toBeFalsy() + expect(canReduceDeadline(dayjs().add(89, "minutes"))).toBeFalsy() + expect(canReduceDeadline(dayjs().add(90, "minutes"))).toBeTruthy() }) - it("should be truthy if the deadline is 30 minutes or more in the future", () => { - expect(canReduceDeadline(dayjs().add(31, "minutes"))).toBeTruthy() + it("should be truthy if the deadline is 90 minutes or more in the future", () => { + expect(canReduceDeadline(dayjs().add(91, "minutes"))).toBeTruthy() expect(canReduceDeadline(dayjs().add(100, "years"))).toBeTruthy() }) }) diff --git a/site/src/util/schedule.ts b/site/src/util/schedule.ts index 247cd88319d70..02130b0071412 100644 --- a/site/src/util/schedule.ts +++ b/site/src/util/schedule.ts @@ -151,15 +151,29 @@ export function getMinDeadline(): dayjs.Dayjs { return dayjs().add(deadlineExtensionMin) } +/** + * Determines if ScheduleBanner can increase ttl by one or more hours + * without hitting the global max deadline. + * @param deadline + * @param workspace + */ export function canExtendDeadline( deadline: dayjs.Dayjs, workspace: Workspace, ): boolean { - return deadline < getMaxDeadline(workspace) + const diff = (getMaxDeadline(workspace)).diff(deadline, 'hours') + return diff >= 1 } +/** + * Determines if ScheduleBanner can reduce ttl by one or more hours + * without hitting the global min remaining time to live. + * Depends on workspace deadline, current time, and a global constant. + * @param deadline current workspace deadline + */ export function canReduceDeadline(deadline: dayjs.Dayjs): boolean { - return deadline > getMinDeadline() + const diff = deadline.diff(getMinDeadline(), 'hours') + return diff >= 1 } export const getDeadline = (workspace: Workspace): dayjs.Dayjs => diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts index 935e073c4a627..14bbb1b64a913 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts @@ -29,7 +29,6 @@ export const Language = { export interface WorkspaceScheduleBannerContext { workspace: Workspace - deadline?: dayjs.Dayjs } export type WorkspaceScheduleBannerEvent = @@ -135,15 +134,17 @@ export const workspaceScheduleBannerMachine = createMachine( }, { guards: { - isAtMaxDeadline: (context) => - context.deadline + isAtMaxDeadline: (context) => { + return context.workspace.latest_build.deadline ? !canExtendDeadline( - context.deadline, + getDeadline(context.workspace), context.workspace, ) - : false, - isAtMinDeadline: (context) => - context.deadline ? !canReduceDeadline(context.deadline) : false, + : false + }, + isAtMinDeadline: (context) => { + return context.workspace.latest_build.deadline ? !canReduceDeadline(getDeadline(context.workspace)) : false + } }, actions: { // This error does not have a detail, so using the snackbar is okay @@ -155,16 +156,15 @@ export const workspaceScheduleBannerMachine = createMachine( }, assignWorkspace: assign((_, event) => ({ workspace: event.workspace, - deadline: getDeadline(event.workspace), })), }, services: { increaseDeadline: async (context, event) => { - if (!context.deadline) { + if (!context.workspace.latest_build.deadline) { throw Error("Deadline is undefined.") } - const proposedDeadline = context.deadline.add(event.hours, "hours") + const proposedDeadline = getDeadline(context.workspace).add(event.hours, "hours") const newDeadline = dayjs.min( proposedDeadline, getMaxDeadline(context.workspace), @@ -172,10 +172,10 @@ export const workspaceScheduleBannerMachine = createMachine( await API.putWorkspaceExtension(context.workspace.id, newDeadline) }, decreaseDeadline: async (context, event) => { - if (!context.deadline) { + if (!context.workspace.latest_build.deadline) { throw Error("Deadline is undefined.") } - const proposedDeadline = context.deadline.subtract(event.hours, "hours") + const proposedDeadline = getDeadline(context.workspace).subtract(event.hours, "hours") const newDeadline = dayjs.max(proposedDeadline, getMinDeadline()) await API.putWorkspaceExtension(context.workspace.id, newDeadline) }, From 56c2a793340a0c769539499b5178f2343dc9d45c Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 20 Dec 2022 16:31:37 +0000 Subject: [PATCH 06/11] Simplify, wip --- .../Workspace/Workspace.stories.tsx | 11 --- site/src/util/schedule.test.ts | 46 ------------ site/src/util/schedule.ts | 25 ------- .../workspaceScheduleBannerXService.ts | 70 ++++--------------- 4 files changed, 12 insertions(+), 140 deletions(-) diff --git a/site/src/components/Workspace/Workspace.stories.tsx b/site/src/components/Workspace/Workspace.stories.tsx index ec7fbae6831ec..a1679e698061b 100644 --- a/site/src/components/Workspace/Workspace.stories.tsx +++ b/site/src/components/Workspace/Workspace.stories.tsx @@ -1,7 +1,5 @@ import { action } from "@storybook/addon-actions" import { Story } from "@storybook/react" -import dayjs from "dayjs" -import { canExtendDeadline, canReduceDeadline } from "util/schedule" import * as Mocks from "../../testHelpers/entities" import { Workspace, WorkspaceErrors, WorkspaceProps } from "./Workspace" @@ -22,15 +20,6 @@ Running.args = { onDeadlinePlus: () => { // do nothing, this is just for storybook }, - deadlineMinusEnabled: () => { - return canReduceDeadline(dayjs(Mocks.MockWorkspace.latest_build.deadline)) - }, - deadlinePlusEnabled: () => { - return canExtendDeadline( - dayjs(Mocks.MockWorkspace.latest_build.deadline), - Mocks.MockWorkspace, - ) - }, maxDeadlineDecrease: 1000, maxDeadlineIncrease: 1000, }, diff --git a/site/src/util/schedule.test.ts b/site/src/util/schedule.test.ts index d9ead6930551d..d8ad59c3e3ce2 100644 --- a/site/src/util/schedule.test.ts +++ b/site/src/util/schedule.test.ts @@ -5,8 +5,6 @@ import { emptyTTL } from "pages/WorkspaceSchedulePage/ttl" import { Workspace } from "../api/typesGenerated" import * as Mocks from "../testHelpers/entities" import { - canExtendDeadline, - canReduceDeadline, deadlineExtensionMax, deadlineExtensionMin, extractTimezone, @@ -64,50 +62,6 @@ describe("minDeadline", () => { }) }) -describe("canExtendDeadline", () => { - it("should be falsy if the deadline is more than 24 hours from the start time", () => { - expect( - canExtendDeadline( - startTime.add(25, "hours"), - Mocks.MockWorkspace, - ), - ).toBeFalsy() - }) - it("should be falsy if the deadline is less than an hour below the max deadline", () => { - expect( - canExtendDeadline( - startTime.add(23.5, "hours"), - Mocks.MockWorkspace - ) - ).toBeFalsy() - }) - it("should be true if the deadline is one hour below the max deadline", () => { - expect( - canExtendDeadline( - startTime.add(23, "hours"), - Mocks.MockWorkspace - ) - ).toBeTruthy() - }) -}) - -describe("canReduceDeadline", () => { - // the minimum ttl is 30 minutes from the current time - // ttl can be reduced by one hour at a time - // so current deadline must be >=90 minutes from current time to be reducible - it("should be falsy if the deadline is 90 minutes or less in the future", () => { - expect(canReduceDeadline(dayjs())).toBeFalsy() - expect(canReduceDeadline(dayjs().add(1, "minutes"))).toBeFalsy() - expect(canReduceDeadline(dayjs().add(89, "minutes"))).toBeFalsy() - expect(canReduceDeadline(dayjs().add(90, "minutes"))).toBeTruthy() - }) - - it("should be truthy if the deadline is 90 minutes or more in the future", () => { - expect(canReduceDeadline(dayjs().add(91, "minutes"))).toBeTruthy() - expect(canReduceDeadline(dayjs().add(100, "years"))).toBeTruthy() - }) -}) - describe("getMaxDeadlineChange", () => { it("should return the number of hours you can add before hitting the max deadline", () => { const deadline = dayjs() diff --git a/site/src/util/schedule.ts b/site/src/util/schedule.ts index 02130b0071412..cd7960e088991 100644 --- a/site/src/util/schedule.ts +++ b/site/src/util/schedule.ts @@ -151,31 +151,6 @@ export function getMinDeadline(): dayjs.Dayjs { return dayjs().add(deadlineExtensionMin) } -/** - * Determines if ScheduleBanner can increase ttl by one or more hours - * without hitting the global max deadline. - * @param deadline - * @param workspace - */ -export function canExtendDeadline( - deadline: dayjs.Dayjs, - workspace: Workspace, -): boolean { - const diff = (getMaxDeadline(workspace)).diff(deadline, 'hours') - return diff >= 1 -} - -/** - * Determines if ScheduleBanner can reduce ttl by one or more hours - * without hitting the global min remaining time to live. - * Depends on workspace deadline, current time, and a global constant. - * @param deadline current workspace deadline - */ -export function canReduceDeadline(deadline: dayjs.Dayjs): boolean { - const diff = deadline.diff(getMinDeadline(), 'hours') - return diff >= 1 -} - export const getDeadline = (workspace: Workspace): dayjs.Dayjs => dayjs(workspace.latest_build.deadline).utc() diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts index 14bbb1b64a913..26af55e9a5ba3 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts @@ -7,8 +7,6 @@ import { Workspace } from "api/typesGenerated" import dayjs from "dayjs" import minMax from "dayjs/plugin/minMax" import { - canExtendDeadline, - canReduceDeadline, getDeadline, getMaxDeadline, getMinDeadline, @@ -58,51 +56,27 @@ export const workspaceScheduleBannerMachine = createMachine( events: {} as WorkspaceScheduleBannerEvent, context: {} as WorkspaceScheduleBannerContext, }, - initial: "initialize", + initial: "idle", on: { REFRESH_WORKSPACE: { actions: "assignWorkspace" }, }, states: { - initialize: { - always: [ - { cond: "isAtMaxDeadline", target: "atMaxDeadline" }, - { cond: "isAtMinDeadline", target: "atMinDeadline" }, - { target: "midRange" }, - ], - }, - midRange: { + idle: { on: { INCREASE_DEADLINE: "increasingDeadline", DECREASE_DEADLINE: "decreasingDeadline", }, }, - atMaxDeadline: { - on: { - DECREASE_DEADLINE: "decreasingDeadline", - }, - }, - atMinDeadline: { - on: { - INCREASE_DEADLINE: "increasingDeadline", - }, - }, increasingDeadline: { invoke: { src: "increaseDeadline", id: "increaseDeadline", - onDone: [ - { - cond: "isAtMaxDeadline", - target: "atMaxDeadline", - actions: "displaySuccessMessage", - }, - { - target: "midRange", - actions: "displaySuccessMessage", - }, - ], + onDone: { + target: "idle", + actions: "displaySuccessMessage", + }, onError: { - target: "midRange", + target: "idle", actions: "displayFailureMessage", }, }, @@ -112,19 +86,12 @@ export const workspaceScheduleBannerMachine = createMachine( invoke: { src: "decreaseDeadline", id: "decreaseDeadline", - onDone: [ - { - cond: "isAtMinDeadline", - target: "atMinDeadline", - actions: "displaySuccessMessage", - }, - { - target: "midRange", - actions: "displaySuccessMessage", - }, - ], + onDone: { + target: "idle", + actions: "displaySuccessMessage", + }, onError: { - target: "midRange", + target: "idle", actions: "displayFailureMessage", }, }, @@ -133,19 +100,6 @@ export const workspaceScheduleBannerMachine = createMachine( }, }, { - guards: { - isAtMaxDeadline: (context) => { - return context.workspace.latest_build.deadline - ? !canExtendDeadline( - getDeadline(context.workspace), - context.workspace, - ) - : false - }, - isAtMinDeadline: (context) => { - return context.workspace.latest_build.deadline ? !canReduceDeadline(getDeadline(context.workspace)) : false - } - }, actions: { // This error does not have a detail, so using the snackbar is okay displayFailureMessage: (_, event) => { From e1088fff58e490966a4fe4aea47671f26de1542c Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 20 Dec 2022 16:52:25 +0000 Subject: [PATCH 07/11] Handle NaN --- site/src/components/WorkspaceScheduleButton/EditHours.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/components/WorkspaceScheduleButton/EditHours.tsx b/site/src/components/WorkspaceScheduleButton/EditHours.tsx index 5bcd34a710054..0df4fa85e2e29 100644 --- a/site/src/components/WorkspaceScheduleButton/EditHours.tsx +++ b/site/src/components/WorkspaceScheduleButton/EditHours.tsx @@ -19,13 +19,13 @@ export const EditHours = ({ const styles = useStyles() return ( -
handleSubmit(hours)}> + handleSubmit(Number.isNaN(hours) ? 0 : hours)}> setHours(parseInt(e.target.value))} type="number" /> From 359653a21e48d7fd806aa5ba2614b22180e6d870 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 20 Dec 2022 16:52:53 +0000 Subject: [PATCH 08/11] Format --- .../pages/WorkspacePage/WorkspaceReadyPage.tsx | 6 +++--- site/src/util/schedule.ts | 4 +--- .../workspaceScheduleBannerXService.ts | 16 +++++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index bbe64ac10212f..968b74cd14ebc 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -104,9 +104,9 @@ export const WorkspaceReadyPage = ({ }, maxDeadlineDecrease: getMaxDeadlineChange(deadline, getMinDeadline()), maxDeadlineIncrease: getMaxDeadlineChange( - getMaxDeadline(workspace), - deadline, - ) + getMaxDeadline(workspace), + deadline, + ), }} isUpdating={workspaceState.hasTag("updating")} workspace={workspace} diff --git a/site/src/util/schedule.ts b/site/src/util/schedule.ts index cd7960e088991..7187fce1f62b5 100644 --- a/site/src/util/schedule.ts +++ b/site/src/util/schedule.ts @@ -131,9 +131,7 @@ export const deadlineExtensionMax = dayjs.duration(24, "hours") * @param ws workspace * @returns the latest datetime at which the workspace can be automatically shut down. */ -export function getMaxDeadline( - ws: Workspace | undefined, -): dayjs.Dayjs { +export function getMaxDeadline(ws: Workspace | undefined): dayjs.Dayjs { // note: we count runtime from updated_at as started_at counts from the start of // the workspace build process, which can take a while. if (ws === undefined) { diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts index 26af55e9a5ba3..41c38f66da974 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleBannerXService.ts @@ -6,11 +6,7 @@ import { getErrorMessage } from "api/errors" import { Workspace } from "api/typesGenerated" import dayjs from "dayjs" import minMax from "dayjs/plugin/minMax" -import { - getDeadline, - getMaxDeadline, - getMinDeadline, -} from "util/schedule" +import { getDeadline, getMaxDeadline, getMinDeadline } from "util/schedule" import { ActorRefFrom, assign, createMachine } from "xstate" import * as API from "../../api/api" import { @@ -118,7 +114,10 @@ export const workspaceScheduleBannerMachine = createMachine( if (!context.workspace.latest_build.deadline) { throw Error("Deadline is undefined.") } - const proposedDeadline = getDeadline(context.workspace).add(event.hours, "hours") + const proposedDeadline = getDeadline(context.workspace).add( + event.hours, + "hours", + ) const newDeadline = dayjs.min( proposedDeadline, getMaxDeadline(context.workspace), @@ -129,7 +128,10 @@ export const workspaceScheduleBannerMachine = createMachine( if (!context.workspace.latest_build.deadline) { throw Error("Deadline is undefined.") } - const proposedDeadline = getDeadline(context.workspace).subtract(event.hours, "hours") + const proposedDeadline = getDeadline(context.workspace).subtract( + event.hours, + "hours", + ) const newDeadline = dayjs.max(proposedDeadline, getMinDeadline()) await API.putWorkspaceExtension(context.workspace.id, newDeadline) }, From 7743fa035bf149598db7d9b9718c92ba6ef470f6 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 20 Dec 2022 18:28:04 +0000 Subject: [PATCH 09/11] Add aria labels --- .../WorkspaceScheduleButton/WorkspaceScheduleButton.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx index de002d530129b..e137210df9010 100644 --- a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx +++ b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx @@ -105,6 +105,7 @@ export const WorkspaceScheduleButton: React.FC< { @@ -119,6 +120,7 @@ export const WorkspaceScheduleButton: React.FC< { From 0ddf307d617952423e21393fb3110028e68ebdf0 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 20 Dec 2022 18:42:24 +0000 Subject: [PATCH 10/11] Explain NaN handling --- site/src/components/WorkspaceScheduleButton/EditHours.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/components/WorkspaceScheduleButton/EditHours.tsx b/site/src/components/WorkspaceScheduleButton/EditHours.tsx index 0df4fa85e2e29..3e9a274d48e26 100644 --- a/site/src/components/WorkspaceScheduleButton/EditHours.tsx +++ b/site/src/components/WorkspaceScheduleButton/EditHours.tsx @@ -19,6 +19,7 @@ export const EditHours = ({ const styles = useStyles() return ( + // hours is NaN when user deletes the value, so treat it as 0 handleSubmit(Number.isNaN(hours) ? 0 : hours)}> Date: Wed, 21 Dec 2022 15:20:06 +0000 Subject: [PATCH 11/11] Use more realistic storybook args --- site/src/components/Workspace/Workspace.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/components/Workspace/Workspace.stories.tsx b/site/src/components/Workspace/Workspace.stories.tsx index a1679e698061b..56a318cdac9ca 100644 --- a/site/src/components/Workspace/Workspace.stories.tsx +++ b/site/src/components/Workspace/Workspace.stories.tsx @@ -20,8 +20,8 @@ Running.args = { onDeadlinePlus: () => { // do nothing, this is just for storybook }, - maxDeadlineDecrease: 1000, - maxDeadlineIncrease: 1000, + maxDeadlineDecrease: 0, + maxDeadlineIncrease: 24, }, workspace: Mocks.MockWorkspace, handleStart: action("start"),