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"), pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy