From 5eb9317c9bfdecc09d10e0bc602aafe15b4ad9e7 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 17 Aug 2022 12:44:10 +0000 Subject: [PATCH 01/15] chore: WorkspacePage: invert workspace schedule bumper logic for readibility --- site/src/components/Workspace/Workspace.tsx | 6 ++++++ .../WorkspaceScheduleButton.tsx | 18 ++++++------------ site/src/pages/WorkspacePage/WorkspacePage.tsx | 17 ++++++++++++++++- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index e52099fac1d16..efe255ce750ed 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -2,6 +2,7 @@ import { makeStyles } from "@material-ui/core/styles" import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge" import { FC } from "react" +import dayjs from "dayjs" import { useNavigate } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" import { BuildsTable } from "../BuildsTable/BuildsTable" @@ -9,6 +10,7 @@ import { Margins } from "../Margins/Margins" import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "../PageHeader/PageHeader" import { Resources } from "../Resources/Resources" import { Stack } from "../Stack/Stack" +import { Workspace as GenWorkspace } from "../../api/typesGenerated" import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions" import { WorkspaceDeletedBanner } from "../WorkspaceDeletedBanner/WorkspaceDeletedBanner" import { WorkspaceScheduleBanner } from "../WorkspaceScheduleBanner/WorkspaceScheduleBanner" @@ -31,6 +33,8 @@ export interface WorkspaceProps { scheduleProps: { onDeadlinePlus: () => void onDeadlineMinus: () => void + deadlinePlusEnabled: (workspace: GenWorkspace, now: dayjs.Dayjs) => boolean + deadlineMinusEnabled: (workspace: GenWorkspace, now: dayjs.Dayjs) => boolean } handleStart: () => void handleStop: () => void @@ -81,6 +85,8 @@ export const Workspace: FC = ({ workspace={workspace} onDeadlineMinus={scheduleProps.onDeadlineMinus} onDeadlinePlus={scheduleProps.onDeadlinePlus} + deadlineMinusEnabled={scheduleProps.deadlineMinusEnabled} + deadlinePlusEnabled={scheduleProps.deadlinePlusEnabled} canUpdateWorkspace={canUpdateWorkspace} /> { return deadline.year() > 1 } -export const deadlineMinusDisabled = (workspace: Workspace, now: dayjs.Dayjs): boolean => { - const delta = dayjs(workspace.latest_build.deadline).diff(now) - return delta <= 30 * 60 * 1000 // 30 minutes -} - -export const deadlinePlusDisabled = (workspace: Workspace, now: dayjs.Dayjs): boolean => { - const delta = dayjs(workspace.latest_build.deadline).diff(now) - return delta >= 24 * 60 * 60 * 1000 // 24 hours -} - export interface WorkspaceScheduleButtonProps { workspace: Workspace onDeadlinePlus: () => void onDeadlineMinus: () => void + deadlineMinusEnabled: (workspace: Workspace, now: dayjs.Dayjs) => boolean + deadlinePlusEnabled: (workspace: Workspace, now: dayjs.Dayjs) => boolean canUpdateWorkspace: boolean } @@ -61,6 +53,8 @@ export const WorkspaceScheduleButton: React.FC = ( workspace, onDeadlinePlus, onDeadlineMinus, + deadlinePlusEnabled, + deadlineMinusEnabled, canUpdateWorkspace, }) => { const anchorRef = useRef(null) @@ -81,7 +75,7 @@ export const WorkspaceScheduleButton: React.FC = ( @@ -91,7 +85,7 @@ export const WorkspaceScheduleButton: React.FC = ( diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 99a25f994a813..90e7020f16543 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -8,6 +8,7 @@ import { useParams } from "react-router-dom" import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" +import { Workspace as GenWorkspace } from "../../api/typesGenerated" import { Workspace, WorkspaceErrors } from "../../components/Workspace/Workspace" import { firstOrItem } from "../../util/array" import { pageTitle } from "../../util/page" @@ -110,6 +111,8 @@ export const WorkspacePage: React.FC = () => { ), }) }, + deadlineMinusEnabled, + deadlinePlusEnabled }} workspace={workspace} handleStart={() => workspaceSend("START")} @@ -142,9 +145,21 @@ export const WorkspacePage: React.FC = () => { export const boundedDeadline = (newDeadline: dayjs.Dayjs, now: dayjs.Dayjs): dayjs.Dayjs => { const minDeadline = now.add(30, "minutes") const maxDeadline = now.add(24, "hours") - return dayjs.min(dayjs.max(minDeadline, newDeadline), maxDeadline) + const bounded = dayjs.min(dayjs.max(minDeadline, newDeadline), maxDeadline) + return bounded } +export const deadlineMinusEnabled = (workspace: GenWorkspace, now: dayjs.Dayjs): boolean => { + const delta = dayjs(workspace.latest_build.deadline).diff(now) + return delta > (30 * 60 * 1000) // 30 minutes +} + +export const deadlinePlusEnabled = (workspace: GenWorkspace, now: dayjs.Dayjs): boolean => { + const delta = dayjs(workspace.latest_build.deadline).diff(now) + return delta < (24 * 60 * 60 * 1000) // 24 hours +} + + const useStyles = makeStyles((theme) => ({ error: { margin: theme.spacing(2), From c57fed28ee8492d43b6ef0b530e3223e7047fd67 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 17 Aug 2022 15:34:13 +0000 Subject: [PATCH 02/15] fix: ui: workspace bumpers now honour template max_ttl --- site/src/components/Workspace/Workspace.tsx | 6 +- .../WorkspaceScheduleButton.test.tsx | 90 ------------------- .../WorkspaceScheduleButton.tsx | 8 +- .../src/pages/WorkspacePage/WorkspacePage.tsx | 53 +++++------ .../xServices/workspace/workspaceXService.ts | 17 +++- 5 files changed, 44 insertions(+), 130 deletions(-) diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index efe255ce750ed..25a1de0ecf047 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -2,7 +2,6 @@ import { makeStyles } from "@material-ui/core/styles" import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge" import { FC } from "react" -import dayjs from "dayjs" import { useNavigate } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" import { BuildsTable } from "../BuildsTable/BuildsTable" @@ -10,7 +9,6 @@ import { Margins } from "../Margins/Margins" import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "../PageHeader/PageHeader" import { Resources } from "../Resources/Resources" import { Stack } from "../Stack/Stack" -import { Workspace as GenWorkspace } from "../../api/typesGenerated" import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions" import { WorkspaceDeletedBanner } from "../WorkspaceDeletedBanner/WorkspaceDeletedBanner" import { WorkspaceScheduleBanner } from "../WorkspaceScheduleBanner/WorkspaceScheduleBanner" @@ -33,8 +31,8 @@ export interface WorkspaceProps { scheduleProps: { onDeadlinePlus: () => void onDeadlineMinus: () => void - deadlinePlusEnabled: (workspace: GenWorkspace, now: dayjs.Dayjs) => boolean - deadlineMinusEnabled: (workspace: GenWorkspace, now: dayjs.Dayjs) => boolean + deadlinePlusEnabled: () => boolean + deadlineMinusEnabled: () => boolean } handleStart: () => void handleStop: () => void diff --git a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx index 1bd8651efd5d7..49c10ab8dc31a 100644 --- a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx +++ b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx @@ -3,8 +3,6 @@ import utc from "dayjs/plugin/utc" import * as TypesGen from "../../api/typesGenerated" import * as Mocks from "../../testHelpers/entities" import { - deadlineMinusDisabled, - deadlinePlusDisabled, shouldDisplayPlusMinus, } from "./WorkspaceScheduleButton" @@ -29,92 +27,4 @@ describe("WorkspaceScheduleButton", () => { expect(shouldDisplayPlusMinus(workspace)).toBeTruthy() }) }) - - describe("deadlineMinusDisabled", () => { - it("should be false if the deadline is more than 30 minutes in the future", () => { - // Given: a workspace with a deadline set to 31 minutes in the future - const workspace: TypesGen.Workspace = { - ...Mocks.MockWorkspace, - latest_build: { - ...Mocks.MockWorkspaceBuild, - deadline: now.add(31, "minutes").utc().format(), - }, - } - - // Then: deadlineMinusDisabled should be falsy - expect(deadlineMinusDisabled(workspace, now)).toBeFalsy() - }) - - it("should be true if the deadline is 30 minutes or less in the future", () => { - // Given: a workspace with a deadline set to 30 minutes in the future - const workspace: TypesGen.Workspace = { - ...Mocks.MockWorkspace, - latest_build: { - ...Mocks.MockWorkspaceBuild, - deadline: now.add(30, "minutes").utc().format(), - }, - } - - // Then: deadlineMinusDisabled should be truthy - expect(deadlineMinusDisabled(workspace, now)).toBeTruthy() - }) - - it("should be true if the deadline is in the past", () => { - // Given: a workspace with a deadline set to 1 minute in the past - const workspace: TypesGen.Workspace = { - ...Mocks.MockWorkspace, - latest_build: { - ...Mocks.MockWorkspaceBuild, - deadline: now.add(-1, "minutes").utc().format(), - }, - } - - // Then: deadlineMinusDisabled should be truthy - expect(deadlineMinusDisabled(workspace, now)).toBeTruthy() - }) - }) - - describe("deadlinePlusDisabled", () => { - it("should be false if the deadline is less than 24 hours in the future", () => { - // Given: a workspace with a deadline set to 23 hours in the future - const workspace: TypesGen.Workspace = { - ...Mocks.MockWorkspace, - latest_build: { - ...Mocks.MockWorkspaceBuild, - deadline: now.add(23, "hours").utc().format(), - }, - } - - // Then: deadlinePlusDisabled should be falsy - expect(deadlinePlusDisabled(workspace, now)).toBeFalsy() - }) - - it("should be true if the deadline is 24 hours or more in the future", () => { - // Given: a workspace with a deadline set to 25 hours in the future - const workspace: TypesGen.Workspace = { - ...Mocks.MockWorkspace, - latest_build: { - ...Mocks.MockWorkspaceBuild, - deadline: now.add(25, "hours").utc().format(), - }, - } - - // Then: deadlinePlusDisabled should be truthy - expect(deadlinePlusDisabled(workspace, now)).toBeTruthy() - }) - - it("should be false if the deadline is in the past", () => { - // Given: a workspace with a deadline set to 1 minute in the past - const workspace: TypesGen.Workspace = { - ...Mocks.MockWorkspace, - latest_build: { - ...Mocks.MockWorkspaceBuild, - deadline: now.add(-1, "minute").utc().format(), - }, - } - - // Then: deadlinePlusDisabled should be falsy - expect(deadlinePlusDisabled(workspace, now)).toBeFalsy() - }) - }) }) diff --git a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx index bad3814a90d08..9fff8fa8e0539 100644 --- a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx +++ b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx @@ -44,8 +44,8 @@ export interface WorkspaceScheduleButtonProps { workspace: Workspace onDeadlinePlus: () => void onDeadlineMinus: () => void - deadlineMinusEnabled: (workspace: Workspace, now: dayjs.Dayjs) => boolean - deadlinePlusEnabled: (workspace: Workspace, now: dayjs.Dayjs) => boolean + deadlineMinusEnabled: () => boolean + deadlinePlusEnabled: () => boolean canUpdateWorkspace: boolean } @@ -75,7 +75,7 @@ export const WorkspaceScheduleButton: React.FC = ( @@ -85,7 +85,7 @@ export const WorkspaceScheduleButton: React.FC = ( diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 90e7020f16543..f21ea0bff5239 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -8,7 +8,6 @@ import { useParams } from "react-router-dom" import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" -import { Workspace as GenWorkspace } from "../../api/typesGenerated" import { Workspace, WorkspaceErrors } from "../../components/Workspace/Workspace" import { firstOrItem } from "../../util/array" import { pageTitle } from "../../util/page" @@ -20,6 +19,9 @@ import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedul dayjs.extend(minMax) +const deadlineExtensionMin = dayjs.duration(30, "minutes") +const deadlineExtensionMax = dayjs.duration(24, "hours") + export const WorkspacePage: React.FC = () => { const { username: usernameQueryParam, workspace: workspaceQueryParam } = useParams() const username = firstOrItem(usernameQueryParam, null) @@ -35,6 +37,7 @@ export const WorkspacePage: React.FC = () => { }) const { workspace, + template, getWorkspaceError, resources, getResourcesError, @@ -67,9 +70,17 @@ export const WorkspacePage: React.FC = () => { {checkPermissionsError && } ) - } else if (!workspace) { + } else if (!workspace || !template) { return } else { + const now = dayjs().utc() + const deadline = dayjs(workspace.latest_build.deadline).utc() + const startedAt = dayjs(workspace.latest_build.updated_at).utc() + const templateMaxTTL = dayjs.duration(template.max_ttl_ms, "milliseconds") + const maxTemplateDeadline = startedAt.add(templateMaxTTL) + const maxGlobalDeadline = startedAt.add(deadlineExtensionMax) + const maxDeadline = dayjs.min(maxTemplateDeadline, maxGlobalDeadline) + const minDeadline = now.add(deadlineExtensionMin) const favicon = getFaviconByStatus(workspace.latest_build) return ( <> @@ -86,7 +97,7 @@ export const WorkspacePage: React.FC = () => { bannerSend({ type: "UPDATE_DEADLINE", workspaceId: workspace.id, - newDeadline: dayjs(workspace.latest_build.deadline).utc().add(4, "hours"), + newDeadline: dayjs.min(deadline.add(4, "hours"), maxDeadline), }) }, }} @@ -95,24 +106,22 @@ export const WorkspacePage: React.FC = () => { bannerSend({ type: "UPDATE_DEADLINE", workspaceId: workspace.id, - newDeadline: boundedDeadline( - dayjs(workspace.latest_build.deadline).utc().add(-1, "hours"), - dayjs(), - ), + newDeadline: dayjs.max(deadline.add(-1, "hours"), minDeadline), }) }, onDeadlinePlus: () => { bannerSend({ type: "UPDATE_DEADLINE", workspaceId: workspace.id, - newDeadline: boundedDeadline( - dayjs(workspace.latest_build.deadline).utc().add(1, "hours"), - dayjs(), - ), + newDeadline: dayjs.min(deadline.add(1, "hours"), maxDeadline), }) }, - deadlineMinusEnabled, - deadlinePlusEnabled + deadlineMinusEnabled: () => { + return deadline > minDeadline + }, + deadlinePlusEnabled: () => { + return deadline < maxDeadline + }, }} workspace={workspace} handleStart={() => workspaceSend("START")} @@ -142,24 +151,6 @@ export const WorkspacePage: React.FC = () => { } } -export const boundedDeadline = (newDeadline: dayjs.Dayjs, now: dayjs.Dayjs): dayjs.Dayjs => { - const minDeadline = now.add(30, "minutes") - const maxDeadline = now.add(24, "hours") - const bounded = dayjs.min(dayjs.max(minDeadline, newDeadline), maxDeadline) - return bounded -} - -export const deadlineMinusEnabled = (workspace: GenWorkspace, now: dayjs.Dayjs): boolean => { - const delta = dayjs(workspace.latest_build.deadline).diff(now) - return delta > (30 * 60 * 1000) // 30 minutes -} - -export const deadlinePlusEnabled = (workspace: GenWorkspace, now: dayjs.Dayjs): boolean => { - const delta = dayjs(workspace.latest_build.deadline).diff(now) - return delta < (24 * 60 * 60 * 1000) // 24 hours -} - - const useStyles = makeStyles((theme) => ({ error: { margin: theme.spacing(2), diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index aa9fe056ef9f5..e665e7c2c3eea 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -136,7 +136,7 @@ export const workspaceMachine = createMachine( src: "getWorkspace", id: "getWorkspace", onDone: { - target: "gettingPermissions", + target: "refreshingTemplate", actions: ["assignWorkspace"], }, onError: { @@ -146,6 +146,21 @@ export const workspaceMachine = createMachine( }, tags: "loading", }, + refreshingTemplate: { + entry: ["clearRefreshTemplateError"], + invoke: { + id: "refreshTemplate", + src: "getTemplate", + onDone: { + target: "gettingPermissions", + actions: ["assignTemplate"], + }, + onError: { + target: "error", + actions: ["assignRefreshTemplateError", "displayRefreshTemplateError"], + }, + }, + }, gettingPermissions: { entry: "clearGetPermissionsError", invoke: { From 7bdf39218d960cbd37de6c7f7ad4140555d8d441 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 17 Aug 2022 17:38:26 +0000 Subject: [PATCH 03/15] extract to function --- .../WorkspaceScheduleButton.test.tsx | 5 +-- .../src/pages/WorkspacePage/WorkspacePage.tsx | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx index 49c10ab8dc31a..8e0d4e53583f1 100644 --- a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx +++ b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx @@ -2,12 +2,9 @@ import dayjs from "dayjs" import utc from "dayjs/plugin/utc" import * as TypesGen from "../../api/typesGenerated" import * as Mocks from "../../testHelpers/entities" -import { - shouldDisplayPlusMinus, -} from "./WorkspaceScheduleButton" +import { shouldDisplayPlusMinus } from "./WorkspaceScheduleButton" dayjs.extend(utc) -const now = dayjs() describe("WorkspaceScheduleButton", () => { describe("shouldDisplayPlusMinus", () => { diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index f21ea0bff5239..32d4519dff874 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -5,6 +5,7 @@ import minMax from "dayjs/plugin/minMax" import React, { useContext, useEffect } from "react" import { Helmet } from "react-helmet" import { useParams } from "react-router-dom" +import * as TypesGen from "../../api/typesGenerated" import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" @@ -75,12 +76,8 @@ export const WorkspacePage: React.FC = () => { } else { const now = dayjs().utc() const deadline = dayjs(workspace.latest_build.deadline).utc() - const startedAt = dayjs(workspace.latest_build.updated_at).utc() - const templateMaxTTL = dayjs.duration(template.max_ttl_ms, "milliseconds") - const maxTemplateDeadline = startedAt.add(templateMaxTTL) - const maxGlobalDeadline = startedAt.add(deadlineExtensionMax) - const maxDeadline = dayjs.min(maxTemplateDeadline, maxGlobalDeadline) - const minDeadline = now.add(deadlineExtensionMin) + const dmax = maxDeadline(workspace, template) + const dmin = minDeadline(now) const favicon = getFaviconByStatus(workspace.latest_build) return ( <> @@ -97,7 +94,7 @@ export const WorkspacePage: React.FC = () => { bannerSend({ type: "UPDATE_DEADLINE", workspaceId: workspace.id, - newDeadline: dayjs.min(deadline.add(4, "hours"), maxDeadline), + newDeadline: dayjs.min(deadline.add(4, "hours"), dmax), }) }, }} @@ -106,21 +103,21 @@ export const WorkspacePage: React.FC = () => { bannerSend({ type: "UPDATE_DEADLINE", workspaceId: workspace.id, - newDeadline: dayjs.max(deadline.add(-1, "hours"), minDeadline), + newDeadline: dayjs.max(deadline.add(-1, "hours"), dmin), }) }, onDeadlinePlus: () => { bannerSend({ type: "UPDATE_DEADLINE", workspaceId: workspace.id, - newDeadline: dayjs.min(deadline.add(1, "hours"), maxDeadline), + newDeadline: dayjs.min(deadline.add(1, "hours"), dmin), }) }, deadlineMinusEnabled: () => { - return deadline > minDeadline + return deadline > dmin }, deadlinePlusEnabled: () => { - return deadline < maxDeadline + return deadline < dmax }, }} workspace={workspace} @@ -151,6 +148,18 @@ export const WorkspacePage: React.FC = () => { } } +export function maxDeadline(ws: TypesGen.Workspace, tpl: TypesGen.Template): dayjs.Dayjs { + const startedAt = dayjs(ws.latest_build.updated_at) + const templateMaxTTL = dayjs.duration(tpl.max_ttl_ms, "milliseconds") + const maxTemplateDeadline = startedAt.add(templateMaxTTL) + const maxGlobalDeadline = startedAt.add(deadlineExtensionMax) + return dayjs.min(maxTemplateDeadline, maxGlobalDeadline) +} + +export function minDeadline(now: dayjs.Dayjs): dayjs.Dayjs { + return now.add(deadlineExtensionMin) +} + const useStyles = makeStyles((theme) => ({ error: { margin: theme.spacing(2), From 35e5146381d1629d33624f42a32226446d8632cb Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 17 Aug 2022 18:39:00 +0000 Subject: [PATCH 04/15] add tests for deadlineMax, deadlineMin --- .../WorkspaceScheduleButton.test.tsx | 2 + .../WorkspacePage/WorkspacePage.test.tsx | 56 ++++++++++++++++++- .../src/pages/WorkspacePage/WorkspacePage.tsx | 4 +- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx index 8e0d4e53583f1..44a1b0639c238 100644 --- a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx +++ b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx @@ -6,6 +6,8 @@ import { shouldDisplayPlusMinus } from "./WorkspaceScheduleButton" dayjs.extend(utc) +const now = dayjs() + describe("WorkspaceScheduleButton", () => { describe("shouldDisplayPlusMinus", () => { it("should not display if the workspace is not running", () => { diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 25296de6001b3..7f6f4fd06f7e3 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -1,8 +1,10 @@ import { fireEvent, screen, waitFor, within } from "@testing-library/react" +import dayjs from "dayjs" import { rest } from "msw" import * as api from "../../api/api" -import { Workspace } from "../../api/typesGenerated" +import { Template, Workspace } from "../../api/typesGenerated" import { Language } from "../../components/WorkspaceActions/ActionCtas" +import * as Mocks from "../../testHelpers/entities" import { MockBuilds, MockCanceledWorkspace, @@ -23,7 +25,15 @@ import { } from "../../testHelpers/renderHelpers" import { server } from "../../testHelpers/server" import { DisplayAgentStatusLanguage, DisplayStatusLanguage } from "../../util/workspace" -import { WorkspacePage } from "./WorkspacePage" +import { + deadlineExtensionMax, + deadlineExtensionMin, + maxDeadline, + minDeadline, + WorkspacePage, +} from "./WorkspacePage" + +const now = dayjs() // It renders the workspace page and waits for it be loaded const renderWorkspacePage = async () => { @@ -200,3 +210,45 @@ describe("Workspace Page", () => { }) }) }) + +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(), + }, + } + it("should be never be greater than global max deadline", () => { + const template: Template = { + ...Mocks.MockTemplate, + max_ttl_ms: 25 * 60 * 60 * 1000, + } + + // Then: deadlineMinusDisabled should be falsy + const delta = maxDeadline(workspace, template).diff(now) + expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) + }) + + it("should be never be greater than global max deadline", () => { + const template: Template = { + ...Mocks.MockTemplate, + max_ttl_ms: 4 * 60 * 60 * 1000, + } + + // Then: deadlineMinusDisabled should be falsy + const delta = maxDeadline(workspace, template).diff(now) + expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) + }) +}) + +describe("minDeadline", () => { + it("should never be less than 30 minutes", () => { + // Given: some condition + + // Then: what should it do? + const delta = minDeadline(now).diff(now) + expect(delta).toBeGreaterThanOrEqual(deadlineExtensionMin.asMilliseconds()) + }) +}) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 32d4519dff874..e7c3d618563e1 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -20,8 +20,8 @@ import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedul dayjs.extend(minMax) -const deadlineExtensionMin = dayjs.duration(30, "minutes") -const deadlineExtensionMax = dayjs.duration(24, "hours") +export const deadlineExtensionMin = dayjs.duration(30, "minutes") +export const deadlineExtensionMax = dayjs.duration(24, "hours") export const WorkspacePage: React.FC = () => { const { username: usernameQueryParam, workspace: workspaceQueryParam } = useParams() From 0c2496214b2b17b1145e6f0868b68a2fe0a35506 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 17 Aug 2022 19:45:13 +0000 Subject: [PATCH 05/15] wip --- .../WorkspaceScheduleButton.test.tsx | 2 -- .../WorkspacePage/WorkspacePage.test.tsx | 25 +++++++++++-------- .../src/pages/WorkspacePage/WorkspacePage.tsx | 8 +++++- site/src/testHelpers/entities.ts | 4 +-- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx index 44a1b0639c238..8e0d4e53583f1 100644 --- a/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx +++ b/site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx @@ -6,8 +6,6 @@ import { shouldDisplayPlusMinus } from "./WorkspaceScheduleButton" dayjs.extend(utc) -const now = dayjs() - describe("WorkspaceScheduleButton", () => { describe("shouldDisplayPlusMinus", () => { it("should not display if the workspace is not running", () => { diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 7f6f4fd06f7e3..f562bee532353 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -60,11 +60,14 @@ const testButton = async (label: string, actionMock: jest.SpyInstance) => { expect(actionMock).toBeCalled() } -const testStatus = async (mock: Workspace, label: string) => { +const testStatus = async (ws: Workspace, tpl: Template, label: string) => { server.use( rest.get(`/api/v2/users/:username/workspace/:workspaceName`, (req, res, ctx) => { - return res(ctx.status(200), ctx.json(mock)) + return res(ctx.status(200), ctx.json(ws)) }), + // rest.get(`/api/v2/templates/:templateId`, (req, res, ctx) => { + // return res(ctx.status(200), ctx.json(tpl)) + // }), ) await renderWorkspacePage() const status = await screen.findByRole("status") @@ -149,31 +152,31 @@ describe("Workspace Page", () => { await testButton(Language.update, getTemplateMock) }) it("shows the Stopping status when the workspace is stopping", async () => { - await testStatus(MockStoppingWorkspace, DisplayStatusLanguage.stopping) + await testStatus(MockStoppingWorkspace, MockTemplate, DisplayStatusLanguage.stopping) }) it("shows the Stopped status when the workspace is stopped", async () => { - await testStatus(MockStoppedWorkspace, DisplayStatusLanguage.stopped) + await testStatus(MockStoppedWorkspace, MockTemplate, DisplayStatusLanguage.stopped) }) it("shows the Building status when the workspace is starting", async () => { - await testStatus(MockStartingWorkspace, DisplayStatusLanguage.starting) + await testStatus(MockStartingWorkspace, MockTemplate, DisplayStatusLanguage.starting) }) it("shows the Running status when the workspace is started", async () => { - await testStatus(MockWorkspace, DisplayStatusLanguage.started) + await testStatus(MockWorkspace, MockTemplate, DisplayStatusLanguage.started) }) it("shows the Failed status when the workspace is failed or canceled", async () => { - await testStatus(MockFailedWorkspace, DisplayStatusLanguage.failed) + await testStatus(MockFailedWorkspace, MockTemplate, DisplayStatusLanguage.failed) }) it("shows the Canceling status when the workspace is canceling", async () => { - await testStatus(MockCancelingWorkspace, DisplayStatusLanguage.canceling) + await testStatus(MockCancelingWorkspace, MockTemplate, DisplayStatusLanguage.canceling) }) it("shows the Canceled status when the workspace is canceling", async () => { - await testStatus(MockCanceledWorkspace, DisplayStatusLanguage.canceled) + await testStatus(MockCanceledWorkspace, MockTemplate, DisplayStatusLanguage.canceled) }) it("shows the Deleting status when the workspace is deleting", async () => { - await testStatus(MockDeletingWorkspace, DisplayStatusLanguage.deleting) + await testStatus(MockDeletingWorkspace, MockTemplate, DisplayStatusLanguage.deleting) }) it("shows the Deleted status when the workspace is deleted", async () => { - await testStatus(MockDeletedWorkspace, DisplayStatusLanguage.deleted) + await testStatus(MockDeletedWorkspace, MockTemplate, DisplayStatusLanguage.deleted) }) describe("Timeline", () => { diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index e7c3d618563e1..e81dd9b391549 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -71,8 +71,14 @@ export const WorkspacePage: React.FC = () => { {checkPermissionsError && } ) - } else if (!workspace || !template) { + } else if (!workspace) { return + } else if (!template) { + return ( +
+ dude don't block the entier page lol just make the bumper load the template +
+ ) } else { const now = dayjs().utc() const deadline = dayjs(workspace.latest_build.deadline).utc() diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 5c484b7e89662..e544c2faa44c0 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -151,8 +151,8 @@ export const MockTemplate: TypesGen.Template = { active_version_id: MockTemplateVersion.id, workspace_owner_count: 1, description: "This is a test description.", - max_ttl_ms: 604800000, - min_autostart_interval_ms: 3600000, + max_ttl_ms: 24 * 60 * 60 * 1000, + min_autostart_interval_ms: 60 * 60 * 1000, created_by_id: "test-creator-id", created_by_name: "test_creator", } From 28220d5041d3e08a8169e849053c455f1990cabf Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 18 Aug 2022 11:56:38 +0000 Subject: [PATCH 06/15] move deadline-related stuff to util/workspace.ts --- .../WorkspacePage/WorkspacePage.test.tsx | 57 ++----------------- .../src/pages/WorkspacePage/WorkspacePage.tsx | 29 ++-------- site/src/util/workspace.test.ts | 47 +++++++++++++++ site/src/util/workspace.ts | 19 +++++++ 4 files changed, 76 insertions(+), 76 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index f562bee532353..7e7fd3e0f14d5 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -25,15 +25,8 @@ import { } from "../../testHelpers/renderHelpers" import { server } from "../../testHelpers/server" import { DisplayAgentStatusLanguage, DisplayStatusLanguage } from "../../util/workspace" -import { - deadlineExtensionMax, - deadlineExtensionMin, - maxDeadline, - minDeadline, - WorkspacePage, -} from "./WorkspacePage" +import { WorkspacePage } from "./WorkspacePage" -const now = dayjs() // It renders the workspace page and waits for it be loaded const renderWorkspacePage = async () => { @@ -65,9 +58,9 @@ const testStatus = async (ws: Workspace, tpl: Template, label: string) => { rest.get(`/api/v2/users/:username/workspace/:workspaceName`, (req, res, ctx) => { return res(ctx.status(200), ctx.json(ws)) }), - // rest.get(`/api/v2/templates/:templateId`, (req, res, ctx) => { - // return res(ctx.status(200), ctx.json(tpl)) - // }), + rest.get(`/api/v2/templates/:templateId`, (req, res, ctx) => { + return res(ctx.status(200), ctx.json(tpl)) + }), ) await renderWorkspacePage() const status = await screen.findByRole("status") @@ -213,45 +206,3 @@ describe("Workspace Page", () => { }) }) }) - -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(), - }, - } - it("should be never be greater than global max deadline", () => { - const template: Template = { - ...Mocks.MockTemplate, - max_ttl_ms: 25 * 60 * 60 * 1000, - } - - // Then: deadlineMinusDisabled should be falsy - const delta = maxDeadline(workspace, template).diff(now) - expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) - }) - - it("should be never be greater than global max deadline", () => { - const template: Template = { - ...Mocks.MockTemplate, - max_ttl_ms: 4 * 60 * 60 * 1000, - } - - // Then: deadlineMinusDisabled should be falsy - const delta = maxDeadline(workspace, template).diff(now) - expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) - }) -}) - -describe("minDeadline", () => { - it("should never be less than 30 minutes", () => { - // Given: some condition - - // Then: what should it do? - const delta = minDeadline(now).diff(now) - expect(delta).toBeGreaterThanOrEqual(deadlineExtensionMin.asMilliseconds()) - }) -}) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index e81dd9b391549..92d07e324288c 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -12,7 +12,7 @@ import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" import { Workspace, WorkspaceErrors } from "../../components/Workspace/Workspace" import { firstOrItem } from "../../util/array" import { pageTitle } from "../../util/page" -import { getFaviconByStatus } from "../../util/workspace" +import { getFaviconByStatus, minDeadline, maxDeadline } from "../../util/workspace" import { selectUser } from "../../xServices/auth/authSelectors" import { XServiceContext } from "../../xServices/StateContext" import { workspaceMachine } from "../../xServices/workspace/workspaceXService" @@ -20,9 +20,6 @@ import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedul dayjs.extend(minMax) -export const deadlineExtensionMin = dayjs.duration(30, "minutes") -export const deadlineExtensionMax = dayjs.duration(24, "hours") - export const WorkspacePage: React.FC = () => { const { username: usernameQueryParam, workspace: workspaceQueryParam } = useParams() const username = firstOrItem(usernameQueryParam, null) @@ -82,8 +79,6 @@ export const WorkspacePage: React.FC = () => { } else { const now = dayjs().utc() const deadline = dayjs(workspace.latest_build.deadline).utc() - const dmax = maxDeadline(workspace, template) - const dmin = minDeadline(now) const favicon = getFaviconByStatus(workspace.latest_build) return ( <> @@ -100,7 +95,7 @@ export const WorkspacePage: React.FC = () => { bannerSend({ type: "UPDATE_DEADLINE", workspaceId: workspace.id, - newDeadline: dayjs.min(deadline.add(4, "hours"), dmax), + newDeadline: dayjs.min(deadline.add(4, "hours"), maxDeadline(workspace, template)), }) }, }} @@ -109,21 +104,21 @@ export const WorkspacePage: React.FC = () => { bannerSend({ type: "UPDATE_DEADLINE", workspaceId: workspace.id, - newDeadline: dayjs.max(deadline.add(-1, "hours"), dmin), + newDeadline: dayjs.max(deadline.add(-1, "hours"), minDeadline()), }) }, onDeadlinePlus: () => { bannerSend({ type: "UPDATE_DEADLINE", workspaceId: workspace.id, - newDeadline: dayjs.min(deadline.add(1, "hours"), dmin), + newDeadline: dayjs.min(deadline.add(1, "hours"), maxDeadline(workspace, template)), }) }, deadlineMinusEnabled: () => { - return deadline > dmin + return deadline > minDeadline() }, deadlinePlusEnabled: () => { - return deadline < dmax + return deadline < maxDeadline(workspace, template) }, }} workspace={workspace} @@ -154,18 +149,6 @@ export const WorkspacePage: React.FC = () => { } } -export function maxDeadline(ws: TypesGen.Workspace, tpl: TypesGen.Template): dayjs.Dayjs { - const startedAt = dayjs(ws.latest_build.updated_at) - const templateMaxTTL = dayjs.duration(tpl.max_ttl_ms, "milliseconds") - const maxTemplateDeadline = startedAt.add(templateMaxTTL) - const maxGlobalDeadline = startedAt.add(deadlineExtensionMax) - return dayjs.min(maxTemplateDeadline, maxGlobalDeadline) -} - -export function minDeadline(now: dayjs.Dayjs): dayjs.Dayjs { - return now.add(deadlineExtensionMin) -} - const useStyles = makeStyles((theme) => ({ error: { margin: theme.spacing(2), diff --git a/site/src/util/workspace.test.ts b/site/src/util/workspace.test.ts index 349ce1d841fe8..651067246776f 100644 --- a/site/src/util/workspace.test.ts +++ b/site/src/util/workspace.test.ts @@ -1,4 +1,5 @@ import dayjs from "dayjs" +import duration from "dayjs/plugin/duration" import * as TypesGen from "../api/typesGenerated" import * as Mocks from "../testHelpers/entities" import { @@ -6,8 +7,15 @@ import { getDisplayWorkspaceBuildInitiatedBy, isWorkspaceDeleted, isWorkspaceOn, + maxDeadline, + minDeadline, + deadlineExtensionMax, + deadlineExtensionMin, } from "./workspace" +dayjs.extend(duration) +const now = dayjs() + describe("util > workspace", () => { describe("isWorkspaceOn", () => { it.each<[TypesGen.WorkspaceTransition, TypesGen.ProvisionerJobStatus, boolean]>([ @@ -129,3 +137,42 @@ describe("util > workspace", () => { }) }) }) + +describe("maxDeadline", () => { + // Given: a workspace built from a template with a max deadline equal to 25 hours which isn't really possible + const workspace: TypesGen.Workspace = { + ...Mocks.MockWorkspace, + latest_build: { + ...Mocks.MockWorkspaceBuild, + deadline: now.add(8, "hours").utc().format(), + }, + } + it("should be never be greater than global max deadline", () => { + const template: TypesGen.Template = { + ...Mocks.MockTemplate, + max_ttl_ms: 25 * 60 * 60 * 1000, + } + + // Then: deadlineMinusDisabled should be falsy + const delta = maxDeadline(workspace, template).diff(now) + expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) + }) + + it("should be never be greater than global max deadline", () => { + const template: TypesGen.Template = { + ...Mocks.MockTemplate, + max_ttl_ms: 4 * 60 * 60 * 1000, + } + + // Then: deadlineMinusDisabled should be falsy + const delta = maxDeadline(workspace, template).diff(now) + expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) + }) +}) + +describe("minDeadline", () => { + it("should never be less than 30 minutes", () => { + const delta = minDeadline().diff(now) + expect(delta).toBeGreaterThanOrEqual(deadlineExtensionMin.asMilliseconds()) + }) +}) diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index 318f6ec13be6e..c58356aaef260 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -1,9 +1,11 @@ import { Theme } from "@material-ui/core/styles" import dayjs from "dayjs" +import duration from "dayjs/plugin/duration" import utc from "dayjs/plugin/utc" import { WorkspaceBuildTransition } from "../api/types" import * as TypesGen from "../api/typesGenerated" +dayjs.extend(duration) dayjs.extend(utc) // all the possible states returned by the API @@ -265,3 +267,20 @@ export const getFaviconByStatus = (build: TypesGen.WorkspaceBuild): FaviconType } throw new Error("unknown status " + status) } + +export const deadlineExtensionMin = dayjs.duration(30, "minutes") +export const deadlineExtensionMax = dayjs.duration(24, "hours") + +export function maxDeadline(ws: TypesGen.Workspace, tpl: TypesGen.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. + const startedAt = dayjs(ws.latest_build.updated_at) + const templateMaxTTL = dayjs.duration(tpl.max_ttl_ms, "milliseconds") + const maxTemplateDeadline = startedAt.add(templateMaxTTL) + const maxGlobalDeadline = startedAt.add(deadlineExtensionMax) + return dayjs.min(maxTemplateDeadline, maxGlobalDeadline) +} + +export function minDeadline(): dayjs.Dayjs { + return dayjs().add(deadlineExtensionMin) +} From c68b931d3c0e32707ba6de46cdae25c5ff742803 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 18 Aug 2022 12:30:03 +0000 Subject: [PATCH 07/15] fix unit tests, working around dayjs plugin silliness --- site/src/util/workspace.test.ts | 36 ++++++++++++++++++--------------- site/src/util/workspace.ts | 9 ++++----- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/site/src/util/workspace.test.ts b/site/src/util/workspace.test.ts index 651067246776f..cd0f0f66d5bfa 100644 --- a/site/src/util/workspace.test.ts +++ b/site/src/util/workspace.test.ts @@ -147,26 +147,30 @@ describe("maxDeadline", () => { deadline: now.add(8, "hours").utc().format(), }, } - it("should be never be greater than global max deadline", () => { - const template: TypesGen.Template = { - ...Mocks.MockTemplate, - max_ttl_ms: 25 * 60 * 60 * 1000, - } + describe("given a template with 25 hour max ttl", () => { + it("should be never be greater than global max deadline", () => { + const template: TypesGen.Template = { + ...Mocks.MockTemplate, + max_ttl_ms: 25 * 60 * 60 * 1000, + } - // Then: deadlineMinusDisabled should be falsy - const delta = maxDeadline(workspace, template).diff(now) - expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) + // Then: deadlineMinusDisabled should be falsy + const delta = maxDeadline(workspace, template).diff(now) + expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) + }) }) - it("should be never be greater than global max deadline", () => { - const template: TypesGen.Template = { - ...Mocks.MockTemplate, - max_ttl_ms: 4 * 60 * 60 * 1000, - } + describe("given a template with 4 hour max ttl", () => { + it("should be never be greater than global max deadline", () => { + const template: TypesGen.Template = { + ...Mocks.MockTemplate, + max_ttl_ms: 4 * 60 * 60 * 1000, + } - // Then: deadlineMinusDisabled should be falsy - const delta = maxDeadline(workspace, template).diff(now) - expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) + // Then: deadlineMinusDisabled should be falsy + const delta = maxDeadline(workspace, template).diff(now) + expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) + }) }) }) diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index c58356aaef260..7e4e75ad34fbd 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -274,11 +274,10 @@ export const deadlineExtensionMax = dayjs.duration(24, "hours") export function maxDeadline(ws: TypesGen.Workspace, tpl: TypesGen.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. - const startedAt = dayjs(ws.latest_build.updated_at) - const templateMaxTTL = dayjs.duration(tpl.max_ttl_ms, "milliseconds") - const maxTemplateDeadline = startedAt.add(templateMaxTTL) - const maxGlobalDeadline = startedAt.add(deadlineExtensionMax) - return dayjs.min(maxTemplateDeadline, maxGlobalDeadline) + const startedAtMillis = dayjs(ws.latest_build.updated_at).unix() * 1000 + const maxTemplateDeadline = startedAtMillis + tpl.max_ttl_ms + const maxGlobalDeadline = startedAtMillis + deadlineExtensionMax.asMilliseconds() + return dayjs(Math.min(maxTemplateDeadline, maxGlobalDeadline)) } export function minDeadline(): dayjs.Dayjs { From 494213fca5154573c095aece6f42f3c18920619d Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 18 Aug 2022 12:46:28 +0000 Subject: [PATCH 08/15] fix dayjs plugin silliness the right way --- site/src/util/workspace.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index 7e4e75ad34fbd..340dba835053a 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -2,11 +2,13 @@ import { Theme } from "@material-ui/core/styles" import dayjs from "dayjs" import duration from "dayjs/plugin/duration" import utc from "dayjs/plugin/utc" +import minMax from "dayjs/plugin/minMax" import { WorkspaceBuildTransition } from "../api/types" import * as TypesGen from "../api/typesGenerated" dayjs.extend(duration) dayjs.extend(utc) +dayjs.extend(minMax) // all the possible states returned by the API export enum WorkspaceStateEnum { @@ -274,10 +276,10 @@ export const deadlineExtensionMax = dayjs.duration(24, "hours") export function maxDeadline(ws: TypesGen.Workspace, tpl: TypesGen.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. - const startedAtMillis = dayjs(ws.latest_build.updated_at).unix() * 1000 - const maxTemplateDeadline = startedAtMillis + tpl.max_ttl_ms - const maxGlobalDeadline = startedAtMillis + deadlineExtensionMax.asMilliseconds() - return dayjs(Math.min(maxTemplateDeadline, maxGlobalDeadline)) + const startedAt = dayjs(ws.latest_build.updated_at) + const maxTemplateDeadline = startedAt.add(dayjs.duration(tpl.max_ttl_ms, "milliseconds")) + const maxGlobalDeadline = startedAt.add(deadlineExtensionMax) + return dayjs.min(maxTemplateDeadline, maxGlobalDeadline) } export function minDeadline(): dayjs.Dayjs { From 9c8d4d59f5960c51a15de1e3ab5d2c8cdc4ee102 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 18 Aug 2022 14:04:47 +0000 Subject: [PATCH 09/15] linter --- .../src/pages/WorkspacePage/WorkspacePage.test.tsx | 6 ------ site/src/pages/WorkspacePage/WorkspacePage.tsx | 14 +++++--------- site/src/xServices/workspace/workspaceXService.ts | 1 + 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 7e7fd3e0f14d5..3a8de7511ef49 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -1,10 +1,8 @@ import { fireEvent, screen, waitFor, within } from "@testing-library/react" -import dayjs from "dayjs" import { rest } from "msw" import * as api from "../../api/api" import { Template, Workspace } from "../../api/typesGenerated" import { Language } from "../../components/WorkspaceActions/ActionCtas" -import * as Mocks from "../../testHelpers/entities" import { MockBuilds, MockCanceledWorkspace, @@ -27,7 +25,6 @@ import { server } from "../../testHelpers/server" import { DisplayAgentStatusLanguage, DisplayStatusLanguage } from "../../util/workspace" import { WorkspacePage } from "./WorkspacePage" - // It renders the workspace page and waits for it be loaded const renderWorkspacePage = async () => { renderWithAuth(, { @@ -58,9 +55,6 @@ const testStatus = async (ws: Workspace, tpl: Template, label: string) => { rest.get(`/api/v2/users/:username/workspace/:workspaceName`, (req, res, ctx) => { return res(ctx.status(200), ctx.json(ws)) }), - rest.get(`/api/v2/templates/:templateId`, (req, res, ctx) => { - return res(ctx.status(200), ctx.json(tpl)) - }), ) await renderWorkspacePage() const status = await screen.findByRole("status") diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 92d07e324288c..38bfb9a3952b3 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -5,14 +5,13 @@ import minMax from "dayjs/plugin/minMax" import React, { useContext, useEffect } from "react" import { Helmet } from "react-helmet" import { useParams } from "react-router-dom" -import * as TypesGen from "../../api/typesGenerated" import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" import { Workspace, WorkspaceErrors } from "../../components/Workspace/Workspace" import { firstOrItem } from "../../util/array" import { pageTitle } from "../../util/page" -import { getFaviconByStatus, minDeadline, maxDeadline } from "../../util/workspace" +import { getFaviconByStatus, maxDeadline, minDeadline } from "../../util/workspace" import { selectUser } from "../../xServices/auth/authSelectors" import { XServiceContext } from "../../xServices/StateContext" import { workspaceMachine } from "../../xServices/workspace/workspaceXService" @@ -35,8 +34,9 @@ export const WorkspacePage: React.FC = () => { }) const { workspace, - template, getWorkspaceError, + template, + refreshTemplateError, resources, getResourcesError, builds, @@ -65,19 +65,15 @@ export const WorkspacePage: React.FC = () => { return (
{getWorkspaceError && } + {refreshTemplateError && } {checkPermissionsError && }
) } else if (!workspace) { return } else if (!template) { - return ( -
- dude don't block the entier page lol just make the bumper load the template -
- ) + return
Loading template
} else { - const now = dayjs().utc() const deadline = dayjs(workspace.latest_build.deadline).utc() const favicon = getFaviconByStatus(workspace.latest_build) return ( diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index e665e7c2c3eea..67d5bfa6674cb 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -160,6 +160,7 @@ export const workspaceMachine = createMachine( actions: ["assignRefreshTemplateError", "displayRefreshTemplateError"], }, }, + tags: "loading", }, gettingPermissions: { entry: "clearGetPermissionsError", From ca8b95f5b0d7b475b827fc13ff5e809480a9ccb3 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 18 Aug 2022 14:08:00 +0000 Subject: [PATCH 10/15] yarn fumpt --- site/src/util/workspace.test.ts | 4 ++-- site/src/util/workspace.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/util/workspace.test.ts b/site/src/util/workspace.test.ts index cd0f0f66d5bfa..8494ee65090ba 100644 --- a/site/src/util/workspace.test.ts +++ b/site/src/util/workspace.test.ts @@ -3,14 +3,14 @@ import duration from "dayjs/plugin/duration" import * as TypesGen from "../api/typesGenerated" import * as Mocks from "../testHelpers/entities" import { + deadlineExtensionMax, + deadlineExtensionMin, defaultWorkspaceExtension, getDisplayWorkspaceBuildInitiatedBy, isWorkspaceDeleted, isWorkspaceOn, maxDeadline, minDeadline, - deadlineExtensionMax, - deadlineExtensionMin, } from "./workspace" dayjs.extend(duration) diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index 340dba835053a..5a895e0775930 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -1,8 +1,8 @@ import { Theme } from "@material-ui/core/styles" import dayjs from "dayjs" import duration from "dayjs/plugin/duration" -import utc from "dayjs/plugin/utc" import minMax from "dayjs/plugin/minMax" +import utc from "dayjs/plugin/utc" import { WorkspaceBuildTransition } from "../api/types" import * as TypesGen from "../api/typesGenerated" From 4aa6eaf1cbcd75cb8413baceb74683b36d3ae1ee Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 18 Aug 2022 14:56:00 +0000 Subject: [PATCH 11/15] i deserve to be mocked --- .../WorkspacePage/WorkspacePage.test.tsx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 3a8de7511ef49..d78ef02eab3e6 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, screen, waitFor, within } from "@testing-library/react" import { rest } from "msw" import * as api from "../../api/api" -import { Template, Workspace } from "../../api/typesGenerated" +import { Workspace } from "../../api/typesGenerated" import { Language } from "../../components/WorkspaceActions/ActionCtas" import { MockBuilds, @@ -27,11 +27,13 @@ import { WorkspacePage } from "./WorkspacePage" // It renders the workspace page and waits for it be loaded const renderWorkspacePage = async () => { + const getTemplateMock = jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate) renderWithAuth(, { route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`, path: "/@:username/:workspace", }) await screen.findByText(MockWorkspace.name) + expect(getTemplateMock).toBeCalled() } /** @@ -50,7 +52,7 @@ const testButton = async (label: string, actionMock: jest.SpyInstance) => { expect(actionMock).toBeCalled() } -const testStatus = async (ws: Workspace, tpl: Template, label: string) => { +const testStatus = async (ws: Workspace, label: string) => { server.use( rest.get(`/api/v2/users/:username/workspace/:workspaceName`, (req, res, ctx) => { return res(ctx.status(200), ctx.json(ws)) @@ -139,31 +141,31 @@ describe("Workspace Page", () => { await testButton(Language.update, getTemplateMock) }) it("shows the Stopping status when the workspace is stopping", async () => { - await testStatus(MockStoppingWorkspace, MockTemplate, DisplayStatusLanguage.stopping) + await testStatus(MockStoppingWorkspace, DisplayStatusLanguage.stopping) }) it("shows the Stopped status when the workspace is stopped", async () => { - await testStatus(MockStoppedWorkspace, MockTemplate, DisplayStatusLanguage.stopped) + await testStatus(MockStoppedWorkspace, DisplayStatusLanguage.stopped) }) it("shows the Building status when the workspace is starting", async () => { - await testStatus(MockStartingWorkspace, MockTemplate, DisplayStatusLanguage.starting) + await testStatus(MockStartingWorkspace, DisplayStatusLanguage.starting) }) it("shows the Running status when the workspace is started", async () => { - await testStatus(MockWorkspace, MockTemplate, DisplayStatusLanguage.started) + await testStatus(MockWorkspace, DisplayStatusLanguage.started) }) it("shows the Failed status when the workspace is failed or canceled", async () => { - await testStatus(MockFailedWorkspace, MockTemplate, DisplayStatusLanguage.failed) + await testStatus(MockFailedWorkspace, DisplayStatusLanguage.failed) }) it("shows the Canceling status when the workspace is canceling", async () => { - await testStatus(MockCancelingWorkspace, MockTemplate, DisplayStatusLanguage.canceling) + await testStatus(MockCancelingWorkspace, DisplayStatusLanguage.canceling) }) it("shows the Canceled status when the workspace is canceling", async () => { - await testStatus(MockCanceledWorkspace, MockTemplate, DisplayStatusLanguage.canceled) + await testStatus(MockCanceledWorkspace, DisplayStatusLanguage.canceled) }) it("shows the Deleting status when the workspace is deleting", async () => { - await testStatus(MockDeletingWorkspace, MockTemplate, DisplayStatusLanguage.deleting) + await testStatus(MockDeletingWorkspace, DisplayStatusLanguage.deleting) }) it("shows the Deleted status when the workspace is deleted", async () => { - await testStatus(MockDeletedWorkspace, MockTemplate, DisplayStatusLanguage.deleted) + await testStatus(MockDeletedWorkspace, DisplayStatusLanguage.deleted) }) describe("Timeline", () => { @@ -181,6 +183,7 @@ describe("Workspace Page", () => { describe("Resources", () => { it("shows the status of each agent in each resource", async () => { + const getTemplateMock = jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate) renderWithAuth(, { route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`, path: "/@:username/:workspace", @@ -197,6 +200,7 @@ describe("Workspace Page", () => { DisplayAgentStatusLanguage[MockWorkspaceAgentDisconnected.status], ) expect(agent2Status.length).toEqual(2) + expect(getTemplateMock).toBeCalled() }) }) }) From ff50a83256b461ee05183ec3f001901f3f446c89 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 18 Aug 2022 21:36:38 +0000 Subject: [PATCH 12/15] move schedule-related util functions to util/schedule --- .../src/pages/WorkspacePage/WorkspacePage.tsx | 3 +- site/src/util/schedule.test.ts | 59 ++++++++++++++++++- site/src/util/schedule.ts | 30 +++++++++- site/src/util/workspace.test.ts | 51 ---------------- site/src/util/workspace.ts | 16 ----- 5 files changed, 89 insertions(+), 70 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 38bfb9a3952b3..7f7d338a9e8cc 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -11,7 +11,8 @@ import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" import { Workspace, WorkspaceErrors } from "../../components/Workspace/Workspace" import { firstOrItem } from "../../util/array" import { pageTitle } from "../../util/page" -import { getFaviconByStatus, maxDeadline, minDeadline } from "../../util/workspace" +import { maxDeadline, minDeadline } from "../../util/schedule" +import { getFaviconByStatus } from "../../util/workspace" import { selectUser } from "../../xServices/auth/authSelectors" import { XServiceContext } from "../../xServices/StateContext" import { workspaceMachine } from "../../xServices/workspace/workspaceXService" diff --git a/site/src/util/schedule.test.ts b/site/src/util/schedule.test.ts index d7ed65299cd67..a878150aeb13e 100644 --- a/site/src/util/schedule.test.ts +++ b/site/src/util/schedule.test.ts @@ -1,4 +1,18 @@ -import { extractTimezone, stripTimezone } from "./schedule" +import dayjs from "dayjs" +import duration from "dayjs/plugin/duration" +import { Template, Workspace } from "../api/typesGenerated" +import * as Mocks from "../testHelpers/entities" +import { + deadlineExtensionMax, + deadlineExtensionMin, + extractTimezone, + maxDeadline, + minDeadline, + stripTimezone, +} from "./schedule" + +dayjs.extend(duration) +const now = dayjs() describe("util/schedule", () => { describe("stripTimezone", () => { @@ -21,3 +35,46 @@ 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(), + }, + } + describe("given a template with 25 hour max ttl", () => { + it("should be never be greater than global max deadline", () => { + const template: Template = { + ...Mocks.MockTemplate, + max_ttl_ms: 25 * 60 * 60 * 1000, + } + + // Then: deadlineMinusDisabled should be falsy + const delta = maxDeadline(workspace, template).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, + max_ttl_ms: 4 * 60 * 60 * 1000, + } + + // Then: deadlineMinusDisabled should be falsy + const delta = maxDeadline(workspace, template).diff(now) + expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) + }) + }) +}) + +describe("minDeadline", () => { + it("should never be less than 30 minutes", () => { + const delta = minDeadline().diff(now) + expect(delta).toBeGreaterThanOrEqual(deadlineExtensionMin.asMilliseconds()) + }) +}) diff --git a/site/src/util/schedule.ts b/site/src/util/schedule.ts index ca5dd52a24ad2..65f0124d7f147 100644 --- a/site/src/util/schedule.ts +++ b/site/src/util/schedule.ts @@ -5,7 +5,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 { Workspace } from "../api/typesGenerated" +import { Template, Workspace } from "../api/typesGenerated" import { isWorkspaceOn } from "./workspace" // REMARK: some plugins depend on utc, so it's listed first. Otherwise they're @@ -110,3 +110,31 @@ export const autoStopDisplay = (workspace: Workspace): string => { return `${duration.humanize()} ${Language.afterStart}` } } + +export const deadlineExtensionMin = dayjs.duration(30, "minutes") +export const deadlineExtensionMax = dayjs.duration(24, "hours") + +export function maxDeadline(ws: Workspace, 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. + const startedAt = dayjs(ws.latest_build.updated_at) + const maxTemplateDeadline = startedAt.add(dayjs.duration(tpl.max_ttl_ms, "milliseconds")) + const maxGlobalDeadline = startedAt.add(deadlineExtensionMax) + return dayjs.min(maxTemplateDeadline, maxGlobalDeadline) +} + +export function minDeadline(): dayjs.Dayjs { + return dayjs().add(deadlineExtensionMin) +} + +export function canExtendDeadline( + deadline: dayjs.Dayjs, + workspace: Workspace, + template: Template, +): boolean { + return deadline < maxDeadline(workspace, template) +} + +export function canReduceDeadline(deadline: dayjs.Dayjs): boolean { + return deadline > minDeadline() +} diff --git a/site/src/util/workspace.test.ts b/site/src/util/workspace.test.ts index 8494ee65090ba..349ce1d841fe8 100644 --- a/site/src/util/workspace.test.ts +++ b/site/src/util/workspace.test.ts @@ -1,21 +1,13 @@ import dayjs from "dayjs" -import duration from "dayjs/plugin/duration" import * as TypesGen from "../api/typesGenerated" import * as Mocks from "../testHelpers/entities" import { - deadlineExtensionMax, - deadlineExtensionMin, defaultWorkspaceExtension, getDisplayWorkspaceBuildInitiatedBy, isWorkspaceDeleted, isWorkspaceOn, - maxDeadline, - minDeadline, } from "./workspace" -dayjs.extend(duration) -const now = dayjs() - describe("util > workspace", () => { describe("isWorkspaceOn", () => { it.each<[TypesGen.WorkspaceTransition, TypesGen.ProvisionerJobStatus, boolean]>([ @@ -137,46 +129,3 @@ describe("util > workspace", () => { }) }) }) - -describe("maxDeadline", () => { - // Given: a workspace built from a template with a max deadline equal to 25 hours which isn't really possible - const workspace: TypesGen.Workspace = { - ...Mocks.MockWorkspace, - latest_build: { - ...Mocks.MockWorkspaceBuild, - deadline: now.add(8, "hours").utc().format(), - }, - } - describe("given a template with 25 hour max ttl", () => { - it("should be never be greater than global max deadline", () => { - const template: TypesGen.Template = { - ...Mocks.MockTemplate, - max_ttl_ms: 25 * 60 * 60 * 1000, - } - - // Then: deadlineMinusDisabled should be falsy - const delta = maxDeadline(workspace, template).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: TypesGen.Template = { - ...Mocks.MockTemplate, - max_ttl_ms: 4 * 60 * 60 * 1000, - } - - // Then: deadlineMinusDisabled should be falsy - const delta = maxDeadline(workspace, template).diff(now) - expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds()) - }) - }) -}) - -describe("minDeadline", () => { - it("should never be less than 30 minutes", () => { - const delta = minDeadline().diff(now) - expect(delta).toBeGreaterThanOrEqual(deadlineExtensionMin.asMilliseconds()) - }) -}) diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index 5a895e0775930..a634150f6f1e5 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -269,19 +269,3 @@ export const getFaviconByStatus = (build: TypesGen.WorkspaceBuild): FaviconType } throw new Error("unknown status " + status) } - -export const deadlineExtensionMin = dayjs.duration(30, "minutes") -export const deadlineExtensionMax = dayjs.duration(24, "hours") - -export function maxDeadline(ws: TypesGen.Workspace, tpl: TypesGen.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. - const startedAt = dayjs(ws.latest_build.updated_at) - const maxTemplateDeadline = startedAt.add(dayjs.duration(tpl.max_ttl_ms, "milliseconds")) - const maxGlobalDeadline = startedAt.add(deadlineExtensionMax) - return dayjs.min(maxTemplateDeadline, maxGlobalDeadline) -} - -export function minDeadline(): dayjs.Dayjs { - return dayjs().add(deadlineExtensionMin) -} From b8ada4912491473ab03c5e312defc562a0445516 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 18 Aug 2022 21:36:52 +0000 Subject: [PATCH 13/15] use FullScreenLoader if template is null --- site/src/pages/WorkspacePage/WorkspacePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 7f7d338a9e8cc..6ec37244e6067 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -73,7 +73,7 @@ export const WorkspacePage: React.FC = () => { } else if (!workspace) { return } else if (!template) { - return
Loading template
+ return } else { const deadline = dayjs(workspace.latest_build.deadline).utc() const favicon = getFaviconByStatus(workspace.latest_build) From f9832b198dbd29332d59d49ed0161ac998eb6ee8 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 18 Aug 2022 21:55:05 +0000 Subject: [PATCH 14/15] extract [-+] disable logic to util/schedule, add unit tests --- .../src/pages/WorkspacePage/WorkspacePage.tsx | 6 ++-- site/src/util/schedule.test.ts | 36 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 6ec37244e6067..fe591c91e5217 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -11,7 +11,7 @@ import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" import { Workspace, WorkspaceErrors } from "../../components/Workspace/Workspace" import { firstOrItem } from "../../util/array" import { pageTitle } from "../../util/page" -import { maxDeadline, minDeadline } from "../../util/schedule" +import { canExtendDeadline, canReduceDeadline, maxDeadline, minDeadline } from "../../util/schedule" import { getFaviconByStatus } from "../../util/workspace" import { selectUser } from "../../xServices/auth/authSelectors" import { XServiceContext } from "../../xServices/StateContext" @@ -112,10 +112,10 @@ export const WorkspacePage: React.FC = () => { }) }, deadlineMinusEnabled: () => { - return deadline > minDeadline() + return canReduceDeadline(deadline) }, deadlinePlusEnabled: () => { - return deadline < maxDeadline(workspace, template) + return canExtendDeadline(deadline, workspace, template) }, }} workspace={workspace} diff --git a/site/src/util/schedule.test.ts b/site/src/util/schedule.test.ts index a878150aeb13e..584fc9b12a422 100644 --- a/site/src/util/schedule.test.ts +++ b/site/src/util/schedule.test.ts @@ -3,6 +3,8 @@ import duration from "dayjs/plugin/duration" import { Template, Workspace } from "../api/typesGenerated" import * as Mocks from "../testHelpers/entities" import { + canExtendDeadline, + canReduceDeadline, deadlineExtensionMax, deadlineExtensionMin, extractTimezone, @@ -78,3 +80,37 @@ describe("minDeadline", () => { expect(delta).toBeGreaterThanOrEqual(deadlineExtensionMin.asMilliseconds()) }) }) + +describe("canExtendDeadline", () => { + it("should be falsy if the deadline is more than 24 hours in the future", () => { + expect( + canExtendDeadline(dayjs().add(25, "hours"), Mocks.MockWorkspace, Mocks.MockTemplate), + ).toBeFalsy() + }) + + it("should be falsy if the deadline is more than the template max_ttl", () => { + const tooFarAhead = dayjs().add(dayjs.duration(Mocks.MockTemplate.max_ttl_ms, "milliseconds")) + expect(canExtendDeadline(tooFarAhead, Mocks.MockWorkspace, Mocks.MockTemplate)).toBeFalsy() + }) + + it("should be truth if the deadline is within the template max_ttl", () => { + const okDeadline = dayjs().add( + dayjs.duration(Mocks.MockTemplate.max_ttl_ms / 2, "milliseconds"), + ) + expect(canExtendDeadline(okDeadline, Mocks.MockWorkspace, Mocks.MockTemplate)).toBeFalsy() + }) +}) + +describe("canReduceDeadline", () => { + it("should be falsy if the deadline is 30 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() + }) + + it("should be truthy if the deadline is 30 minutes or more in the future", () => { + expect(canReduceDeadline(dayjs().add(31, "minutes"))).toBeTruthy() + expect(canReduceDeadline(dayjs().add(100, "years"))).toBeTruthy() + }) +}) From e69ff7f9e9ec1cca621d30b750ca56cf4b5a07b6 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 18 Aug 2022 22:16:26 +0000 Subject: [PATCH 15/15] fix storybook --- site/src/components/Workspace/Workspace.stories.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/site/src/components/Workspace/Workspace.stories.tsx b/site/src/components/Workspace/Workspace.stories.tsx index 7a64657d4d5dd..7dea59d97cd13 100644 --- a/site/src/components/Workspace/Workspace.stories.tsx +++ b/site/src/components/Workspace/Workspace.stories.tsx @@ -1,5 +1,7 @@ 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" @@ -24,6 +26,16 @@ Started.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, + Mocks.MockTemplate, + ) + }, }, workspace: Mocks.MockWorkspace, handleStart: action("start"),