Skip to content

Commit 04c5f92

Browse files
authored
fix: ui: workspace bumpers now honour template max_ttl (#3532)
- chore: WorkspacePage: invert workspace schedule bumper logic for readibility - fix: make workspace bumpers honour template max_ttl - chore: refactor workspace schedule bumper logic to util/schedule.ts and unit test separately
1 parent 7599ad4 commit 04c5f92

File tree

11 files changed

+191
-128
lines changed

11 files changed

+191
-128
lines changed

site/src/components/Workspace/Workspace.stories.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { action } from "@storybook/addon-actions"
22
import { Story } from "@storybook/react"
3+
import dayjs from "dayjs"
4+
import { canExtendDeadline, canReduceDeadline } from "util/schedule"
35
import * as Mocks from "../../testHelpers/entities"
46
import { Workspace, WorkspaceErrors, WorkspaceProps } from "./Workspace"
57

@@ -24,6 +26,16 @@ Started.args = {
2426
onDeadlinePlus: () => {
2527
// do nothing, this is just for storybook
2628
},
29+
deadlineMinusEnabled: () => {
30+
return canReduceDeadline(dayjs(Mocks.MockWorkspace.latest_build.deadline))
31+
},
32+
deadlinePlusEnabled: () => {
33+
return canExtendDeadline(
34+
dayjs(Mocks.MockWorkspace.latest_build.deadline),
35+
Mocks.MockWorkspace,
36+
Mocks.MockTemplate,
37+
)
38+
},
2739
},
2840
workspace: Mocks.MockWorkspace,
2941
handleStart: action("start"),

site/src/components/Workspace/Workspace.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export interface WorkspaceProps {
3131
scheduleProps: {
3232
onDeadlinePlus: () => void
3333
onDeadlineMinus: () => void
34+
deadlinePlusEnabled: () => boolean
35+
deadlineMinusEnabled: () => boolean
3436
}
3537
handleStart: () => void
3638
handleStop: () => void
@@ -81,6 +83,8 @@ export const Workspace: FC<WorkspaceProps> = ({
8183
workspace={workspace}
8284
onDeadlineMinus={scheduleProps.onDeadlineMinus}
8385
onDeadlinePlus={scheduleProps.onDeadlinePlus}
86+
deadlineMinusEnabled={scheduleProps.deadlineMinusEnabled}
87+
deadlinePlusEnabled={scheduleProps.deadlinePlusEnabled}
8488
canUpdateWorkspace={canUpdateWorkspace}
8589
/>
8690
<WorkspaceActions

site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.test.tsx

Lines changed: 1 addition & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,9 @@ import dayjs from "dayjs"
22
import utc from "dayjs/plugin/utc"
33
import * as TypesGen from "../../api/typesGenerated"
44
import * as Mocks from "../../testHelpers/entities"
5-
import {
6-
deadlineMinusDisabled,
7-
deadlinePlusDisabled,
8-
shouldDisplayPlusMinus,
9-
} from "./WorkspaceScheduleButton"
5+
import { shouldDisplayPlusMinus } from "./WorkspaceScheduleButton"
106

117
dayjs.extend(utc)
12-
const now = dayjs()
138

149
describe("WorkspaceScheduleButton", () => {
1510
describe("shouldDisplayPlusMinus", () => {
@@ -29,92 +24,4 @@ describe("WorkspaceScheduleButton", () => {
2924
expect(shouldDisplayPlusMinus(workspace)).toBeTruthy()
3025
})
3126
})
32-
33-
describe("deadlineMinusDisabled", () => {
34-
it("should be false if the deadline is more than 30 minutes in the future", () => {
35-
// Given: a workspace with a deadline set to 31 minutes in the future
36-
const workspace: TypesGen.Workspace = {
37-
...Mocks.MockWorkspace,
38-
latest_build: {
39-
...Mocks.MockWorkspaceBuild,
40-
deadline: now.add(31, "minutes").utc().format(),
41-
},
42-
}
43-
44-
// Then: deadlineMinusDisabled should be falsy
45-
expect(deadlineMinusDisabled(workspace, now)).toBeFalsy()
46-
})
47-
48-
it("should be true if the deadline is 30 minutes or less in the future", () => {
49-
// Given: a workspace with a deadline set to 30 minutes in the future
50-
const workspace: TypesGen.Workspace = {
51-
...Mocks.MockWorkspace,
52-
latest_build: {
53-
...Mocks.MockWorkspaceBuild,
54-
deadline: now.add(30, "minutes").utc().format(),
55-
},
56-
}
57-
58-
// Then: deadlineMinusDisabled should be truthy
59-
expect(deadlineMinusDisabled(workspace, now)).toBeTruthy()
60-
})
61-
62-
it("should be true if the deadline is in the past", () => {
63-
// Given: a workspace with a deadline set to 1 minute in the past
64-
const workspace: TypesGen.Workspace = {
65-
...Mocks.MockWorkspace,
66-
latest_build: {
67-
...Mocks.MockWorkspaceBuild,
68-
deadline: now.add(-1, "minutes").utc().format(),
69-
},
70-
}
71-
72-
// Then: deadlineMinusDisabled should be truthy
73-
expect(deadlineMinusDisabled(workspace, now)).toBeTruthy()
74-
})
75-
})
76-
77-
describe("deadlinePlusDisabled", () => {
78-
it("should be false if the deadline is less than 24 hours in the future", () => {
79-
// Given: a workspace with a deadline set to 23 hours in the future
80-
const workspace: TypesGen.Workspace = {
81-
...Mocks.MockWorkspace,
82-
latest_build: {
83-
...Mocks.MockWorkspaceBuild,
84-
deadline: now.add(23, "hours").utc().format(),
85-
},
86-
}
87-
88-
// Then: deadlinePlusDisabled should be falsy
89-
expect(deadlinePlusDisabled(workspace, now)).toBeFalsy()
90-
})
91-
92-
it("should be true if the deadline is 24 hours or more in the future", () => {
93-
// Given: a workspace with a deadline set to 25 hours in the future
94-
const workspace: TypesGen.Workspace = {
95-
...Mocks.MockWorkspace,
96-
latest_build: {
97-
...Mocks.MockWorkspaceBuild,
98-
deadline: now.add(25, "hours").utc().format(),
99-
},
100-
}
101-
102-
// Then: deadlinePlusDisabled should be truthy
103-
expect(deadlinePlusDisabled(workspace, now)).toBeTruthy()
104-
})
105-
106-
it("should be false if the deadline is in the past", () => {
107-
// Given: a workspace with a deadline set to 1 minute in the past
108-
const workspace: TypesGen.Workspace = {
109-
...Mocks.MockWorkspace,
110-
latest_build: {
111-
...Mocks.MockWorkspaceBuild,
112-
deadline: now.add(-1, "minute").utc().format(),
113-
},
114-
}
115-
116-
// Then: deadlinePlusDisabled should be falsy
117-
expect(deadlinePlusDisabled(workspace, now)).toBeFalsy()
118-
})
119-
})
12027
})

site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,27 +40,21 @@ export const shouldDisplayPlusMinus = (workspace: Workspace): boolean => {
4040
return deadline.year() > 1
4141
}
4242

43-
export const deadlineMinusDisabled = (workspace: Workspace, now: dayjs.Dayjs): boolean => {
44-
const delta = dayjs(workspace.latest_build.deadline).diff(now)
45-
return delta <= 30 * 60 * 1000 // 30 minutes
46-
}
47-
48-
export const deadlinePlusDisabled = (workspace: Workspace, now: dayjs.Dayjs): boolean => {
49-
const delta = dayjs(workspace.latest_build.deadline).diff(now)
50-
return delta >= 24 * 60 * 60 * 1000 // 24 hours
51-
}
52-
5343
export interface WorkspaceScheduleButtonProps {
5444
workspace: Workspace
5545
onDeadlinePlus: () => void
5646
onDeadlineMinus: () => void
47+
deadlineMinusEnabled: () => boolean
48+
deadlinePlusEnabled: () => boolean
5749
canUpdateWorkspace: boolean
5850
}
5951

6052
export const WorkspaceScheduleButton: React.FC<WorkspaceScheduleButtonProps> = ({
6153
workspace,
6254
onDeadlinePlus,
6355
onDeadlineMinus,
56+
deadlinePlusEnabled,
57+
deadlineMinusEnabled,
6458
canUpdateWorkspace,
6559
}) => {
6660
const anchorRef = useRef<HTMLButtonElement>(null)
@@ -81,7 +75,7 @@ export const WorkspaceScheduleButton: React.FC<WorkspaceScheduleButtonProps> = (
8175
<IconButton
8276
className={styles.iconButton}
8377
size="small"
84-
disabled={deadlineMinusDisabled(workspace, dayjs())}
78+
disabled={!deadlineMinusEnabled()}
8579
onClick={onDeadlineMinus}
8680
>
8781
<Tooltip title={Language.editDeadlineMinus}>
@@ -91,7 +85,7 @@ export const WorkspaceScheduleButton: React.FC<WorkspaceScheduleButtonProps> = (
9185
<IconButton
9286
className={styles.iconButton}
9387
size="small"
94-
disabled={deadlinePlusDisabled(workspace, dayjs())}
88+
disabled={!deadlinePlusEnabled()}
9589
onClick={onDeadlinePlus}
9690
>
9791
<Tooltip title={Language.editDeadlinePlus}>

site/src/pages/WorkspacePage/WorkspacePage.test.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ import { WorkspacePage } from "./WorkspacePage"
2727

2828
// It renders the workspace page and waits for it be loaded
2929
const renderWorkspacePage = async () => {
30+
const getTemplateMock = jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate)
3031
renderWithAuth(<WorkspacePage />, {
3132
route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`,
3233
path: "/@:username/:workspace",
3334
})
3435
await screen.findByText(MockWorkspace.name)
36+
expect(getTemplateMock).toBeCalled()
3537
}
3638

3739
/**
@@ -50,10 +52,10 @@ const testButton = async (label: string, actionMock: jest.SpyInstance) => {
5052
expect(actionMock).toBeCalled()
5153
}
5254

53-
const testStatus = async (mock: Workspace, label: string) => {
55+
const testStatus = async (ws: Workspace, label: string) => {
5456
server.use(
5557
rest.get(`/api/v2/users/:username/workspace/:workspaceName`, (req, res, ctx) => {
56-
return res(ctx.status(200), ctx.json(mock))
58+
return res(ctx.status(200), ctx.json(ws))
5759
}),
5860
)
5961
await renderWorkspacePage()
@@ -181,6 +183,7 @@ describe("Workspace Page", () => {
181183

182184
describe("Resources", () => {
183185
it("shows the status of each agent in each resource", async () => {
186+
const getTemplateMock = jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate)
184187
renderWithAuth(<WorkspacePage />, {
185188
route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`,
186189
path: "/@:username/:workspace",
@@ -197,6 +200,7 @@ describe("Workspace Page", () => {
197200
DisplayAgentStatusLanguage[MockWorkspaceAgentDisconnected.status],
198201
)
199202
expect(agent2Status.length).toEqual(2)
203+
expect(getTemplateMock).toBeCalled()
200204
})
201205
})
202206
})

site/src/pages/WorkspacePage/WorkspacePage.tsx

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
1111
import { Workspace, WorkspaceErrors } from "../../components/Workspace/Workspace"
1212
import { firstOrItem } from "../../util/array"
1313
import { pageTitle } from "../../util/page"
14+
import { canExtendDeadline, canReduceDeadline, maxDeadline, minDeadline } from "../../util/schedule"
1415
import { getFaviconByStatus } from "../../util/workspace"
1516
import { selectUser } from "../../xServices/auth/authSelectors"
1617
import { XServiceContext } from "../../xServices/StateContext"
@@ -35,6 +36,8 @@ export const WorkspacePage: React.FC = () => {
3536
const {
3637
workspace,
3738
getWorkspaceError,
39+
template,
40+
refreshTemplateError,
3841
resources,
3942
getResourcesError,
4043
builds,
@@ -63,12 +66,16 @@ export const WorkspacePage: React.FC = () => {
6366
return (
6467
<div className={styles.error}>
6568
{getWorkspaceError && <ErrorSummary error={getWorkspaceError} />}
69+
{refreshTemplateError && <ErrorSummary error={refreshTemplateError} />}
6670
{checkPermissionsError && <ErrorSummary error={checkPermissionsError} />}
6771
</div>
6872
)
6973
} else if (!workspace) {
7074
return <FullScreenLoader />
75+
} else if (!template) {
76+
return <FullScreenLoader />
7177
} else {
78+
const deadline = dayjs(workspace.latest_build.deadline).utc()
7279
const favicon = getFaviconByStatus(workspace.latest_build)
7380
return (
7481
<>
@@ -85,7 +92,7 @@ export const WorkspacePage: React.FC = () => {
8592
bannerSend({
8693
type: "UPDATE_DEADLINE",
8794
workspaceId: workspace.id,
88-
newDeadline: dayjs(workspace.latest_build.deadline).utc().add(4, "hours"),
95+
newDeadline: dayjs.min(deadline.add(4, "hours"), maxDeadline(workspace, template)),
8996
})
9097
},
9198
}}
@@ -94,22 +101,22 @@ export const WorkspacePage: React.FC = () => {
94101
bannerSend({
95102
type: "UPDATE_DEADLINE",
96103
workspaceId: workspace.id,
97-
newDeadline: boundedDeadline(
98-
dayjs(workspace.latest_build.deadline).utc().add(-1, "hours"),
99-
dayjs(),
100-
),
104+
newDeadline: dayjs.max(deadline.add(-1, "hours"), minDeadline()),
101105
})
102106
},
103107
onDeadlinePlus: () => {
104108
bannerSend({
105109
type: "UPDATE_DEADLINE",
106110
workspaceId: workspace.id,
107-
newDeadline: boundedDeadline(
108-
dayjs(workspace.latest_build.deadline).utc().add(1, "hours"),
109-
dayjs(),
110-
),
111+
newDeadline: dayjs.min(deadline.add(1, "hours"), maxDeadline(workspace, template)),
111112
})
112113
},
114+
deadlineMinusEnabled: () => {
115+
return canReduceDeadline(deadline)
116+
},
117+
deadlinePlusEnabled: () => {
118+
return canExtendDeadline(deadline, workspace, template)
119+
},
113120
}}
114121
workspace={workspace}
115122
handleStart={() => workspaceSend("START")}
@@ -139,12 +146,6 @@ export const WorkspacePage: React.FC = () => {
139146
}
140147
}
141148

142-
export const boundedDeadline = (newDeadline: dayjs.Dayjs, now: dayjs.Dayjs): dayjs.Dayjs => {
143-
const minDeadline = now.add(30, "minutes")
144-
const maxDeadline = now.add(24, "hours")
145-
return dayjs.min(dayjs.max(minDeadline, newDeadline), maxDeadline)
146-
}
147-
148149
const useStyles = makeStyles((theme) => ({
149150
error: {
150151
margin: theme.spacing(2),

site/src/testHelpers/entities.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ export const MockTemplate: TypesGen.Template = {
151151
active_version_id: MockTemplateVersion.id,
152152
workspace_owner_count: 1,
153153
description: "This is a test description.",
154-
max_ttl_ms: 604800000,
155-
min_autostart_interval_ms: 3600000,
154+
max_ttl_ms: 24 * 60 * 60 * 1000,
155+
min_autostart_interval_ms: 60 * 60 * 1000,
156156
created_by_id: "test-creator-id",
157157
created_by_name: "test_creator",
158158
}

0 commit comments

Comments
 (0)
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