Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 56ec53d

Browse files
authored
fix: derive running ws stop time from deadline (#1920)
* refactor: isWorkspaceOn utility Summary: A utility is function is added that answers the question if a workspace is on. Impact: This is a shared piece of logic in workspace scheduling presentations. In particular it unblocks work in 1779, or at least allows an implementation that shares details with the WorkspaceScheduleBanner. Notes: We could possibly instead return whether the workspace is "ON", "UNKNOWN", or "OFF". Maybe a future improvement for that could be made as the neds arrises. * fix: derive running ws stop time from deadline Summary: When a workspace is on, the remaining time until shutdown needs to be derived from the deadline timestamp, not implied from the TTL
1 parent c6167a9 commit 56ec53d

File tree

8 files changed

+164
-60
lines changed

8 files changed

+164
-60
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,6 @@
11
import { action } from "@storybook/addon-actions"
22
import { Story } from "@storybook/react"
3-
import {
4-
MockCanceledWorkspace,
5-
MockCancelingWorkspace,
6-
MockDeletedWorkspace,
7-
MockDeletingWorkspace,
8-
MockFailedWorkspace,
9-
MockOutdatedWorkspace,
10-
MockStartingWorkspace,
11-
MockStoppedWorkspace,
12-
MockStoppingWorkspace,
13-
MockWorkspace,
14-
MockWorkspaceBuild,
15-
MockWorkspaceResource,
16-
MockWorkspaceResource2,
17-
} from "../../testHelpers/renderHelpers"
3+
import * as Mocks from "../../testHelpers/entities"
184
import { Workspace, WorkspaceProps } from "./Workspace"
195

206
export default {
@@ -27,36 +13,73 @@ const Template: Story<WorkspaceProps> = (args) => <Workspace {...args} />
2713

2814
export const Started = Template.bind({})
2915
Started.args = {
30-
workspace: MockWorkspace,
16+
workspace: Mocks.MockWorkspace,
3117
handleStart: action("start"),
3218
handleStop: action("stop"),
33-
resources: [MockWorkspaceResource, MockWorkspaceResource2],
34-
builds: [MockWorkspaceBuild],
19+
resources: [Mocks.MockWorkspaceResource, Mocks.MockWorkspaceResource2],
20+
builds: [Mocks.MockWorkspaceBuild],
3521
}
3622

3723
export const Starting = Template.bind({})
38-
Starting.args = { ...Started.args, workspace: MockStartingWorkspace }
24+
Starting.args = {
25+
...Started.args,
26+
workspace: Mocks.MockStartingWorkspace,
27+
}
3928

4029
export const Stopped = Template.bind({})
41-
Stopped.args = { ...Started.args, workspace: MockStoppedWorkspace }
30+
Stopped.args = {
31+
...Started.args,
32+
workspace: Mocks.MockStoppedWorkspace,
33+
}
4234

4335
export const Stopping = Template.bind({})
44-
Stopping.args = { ...Started.args, workspace: MockStoppingWorkspace }
36+
Stopping.args = {
37+
...Started.args,
38+
workspace: Mocks.MockStoppingWorkspace,
39+
}
4540

4641
export const Error = Template.bind({})
47-
Error.args = { ...Started.args, workspace: MockFailedWorkspace }
42+
Error.args = {
43+
...Started.args,
44+
workspace: {
45+
...Mocks.MockFailedWorkspace,
46+
latest_build: {
47+
...Mocks.MockWorkspaceBuild,
48+
job: {
49+
...Mocks.MockProvisionerJob,
50+
status: "failed",
51+
},
52+
transition: "start",
53+
},
54+
},
55+
}
4856

4957
export const Deleting = Template.bind({})
50-
Deleting.args = { ...Started.args, workspace: MockDeletingWorkspace }
58+
Deleting.args = {
59+
...Started.args,
60+
workspace: Mocks.MockDeletingWorkspace,
61+
}
5162

5263
export const Deleted = Template.bind({})
53-
Deleted.args = { ...Started.args, workspace: MockDeletedWorkspace }
64+
Deleted.args = {
65+
...Started.args,
66+
workspace: Mocks.MockDeletedWorkspace,
67+
}
5468

5569
export const Canceling = Template.bind({})
56-
Canceling.args = { ...Started.args, workspace: MockCancelingWorkspace }
70+
Canceling.args = {
71+
...Started.args,
72+
workspace: Mocks.MockCancelingWorkspace,
73+
}
5774

5875
export const Canceled = Template.bind({})
59-
Canceled.args = { ...Started.args, workspace: MockCanceledWorkspace }
76+
Canceled.args = {
77+
...Started.args,
78+
workspace: Mocks.MockCanceledWorkspace,
79+
}
6080

6181
export const Outdated = Template.bind({})
62-
Outdated.args = { ...Started.args, workspace: MockOutdatedWorkspace }
82+
Outdated.args = {
83+
...Started.args,
84+
workspace: Mocks.MockOutdatedWorkspace,
85+
}

site/src/components/WorkspaceSchedule/WorkspaceSchedule.stories.tsx

+17-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { Story } from "@storybook/react"
22
import dayjs from "dayjs"
3+
import utc from "dayjs/plugin/utc"
34
import * as Mocks from "../../testHelpers/entities"
45
import { WorkspaceSchedule, WorkspaceScheduleProps } from "./WorkspaceSchedule"
56

7+
dayjs.extend(utc)
8+
9+
// REMARK: There's a known problem with storybook and using date libraries that
10+
// call string.toLowerCase
11+
// SEE: https:github.com/storybookjs/storybook/issues/12208#issuecomment-697044557
12+
const ONE = 1
13+
const SEVEN = 7
14+
615
export default {
716
title: "components/WorkspaceSchedule",
817
component: WorkspaceSchedule,
9-
argTypes: {},
1018
}
1119

1220
const Template: Story<WorkspaceScheduleProps> = (args) => <WorkspaceSchedule {...args} />
@@ -15,6 +23,12 @@ export const NoTTL = Template.bind({})
1523
NoTTL.args = {
1624
workspace: {
1725
...Mocks.MockWorkspace,
26+
latest_build: {
27+
...Mocks.MockWorkspaceBuild,
28+
// a mannual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
29+
// SEE: #1834
30+
deadline: "0001-01-01T00:00:00Z",
31+
},
1832
ttl: undefined,
1933
},
2034
}
@@ -23,11 +37,10 @@ export const ShutdownSoon = Template.bind({})
2337
ShutdownSoon.args = {
2438
workspace: {
2539
...Mocks.MockWorkspace,
26-
2740
latest_build: {
2841
...Mocks.MockWorkspaceBuild,
42+
deadline: dayjs().add(ONE, "hour").utc().format(),
2943
transition: "start",
30-
updated_at: dayjs().subtract(1, "hour").toString(), // 1 hour ago
3144
},
3245
ttl: 2 * 60 * 60 * 1000 * 1_000_000, // 2 hours
3346
},
@@ -40,8 +53,8 @@ ShutdownLong.args = {
4053

4154
latest_build: {
4255
...Mocks.MockWorkspaceBuild,
56+
deadline: dayjs().add(SEVEN, "days").utc().format(),
4357
transition: "start",
44-
updated_at: dayjs().toString(),
4558
},
4659
ttl: 7 * 24 * 60 * 60 * 1000 * 1_000_000, // 7 days
4760
},
@@ -55,7 +68,6 @@ WorkspaceOffShort.args = {
5568
latest_build: {
5669
...Mocks.MockWorkspaceBuild,
5770
transition: "stop",
58-
updated_at: dayjs().subtract(2, "days").toString(),
5971
},
6072
ttl: 2 * 60 * 60 * 1000 * 1_000_000, // 2 hours
6173
},
@@ -69,7 +81,6 @@ WorkspaceOffLong.args = {
6981
latest_build: {
7082
...Mocks.MockWorkspaceBuild,
7183
transition: "stop",
72-
updated_at: dayjs().subtract(2, "days").toString(),
7384
},
7485
ttl: 2 * 365 * 24 * 60 * 60 * 1000 * 1_000_000, // 2 years
7586
},

site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx

+28-15
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ import cronstrue from "cronstrue"
66
import dayjs from "dayjs"
77
import duration from "dayjs/plugin/duration"
88
import relativeTime from "dayjs/plugin/relativeTime"
9+
import utc from "dayjs/plugin/utc"
910
import { FC } from "react"
1011
import { Link as RouterLink } from "react-router-dom"
1112
import { Workspace } from "../../api/typesGenerated"
1213
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
1314
import { extractTimezone, stripTimezone } from "../../util/schedule"
15+
import { isWorkspaceOn } from "../../util/workspace"
1416
import { Stack } from "../Stack/Stack"
1517

18+
dayjs.extend(utc)
1619
dayjs.extend(duration)
1720
dayjs.extend(relativeTime)
1821

19-
const Language = {
22+
export const Language = {
2023
autoStartDisplay: (schedule: string): string => {
2124
if (schedule) {
2225
return cronstrue.toString(stripTimezone(schedule), { throwExceptionOnParseError: false })
@@ -33,24 +36,34 @@ const Language = {
3336
}
3437
},
3538
autoStopDisplay: (workspace: Workspace): string => {
36-
const latest = workspace.latest_build
39+
const deadline = dayjs(workspace.latest_build.deadline).utc()
40+
// a mannual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
41+
// SEE: #1834
42+
const hasDeadline = deadline.year() > 1
43+
const ttl = workspace.ttl
3744

38-
if (!workspace.ttl || workspace.ttl < 1) {
39-
return "Manual"
40-
}
41-
42-
if (latest.transition === "start") {
43-
const now = dayjs()
44-
const updatedAt = dayjs(latest.updated_at)
45-
const deadline = updatedAt.add(workspace.ttl / 1_000_000, "ms")
45+
if (isWorkspaceOn(workspace) && hasDeadline) {
46+
// Workspace is on --> derive from latest_build.deadline. Note that the
47+
// user may modify their workspace object (ttl) while the workspace is
48+
// running and depending on system semantics, the deadline may still
49+
// represent the previously defined ttl. Thus, we always derive from the
50+
// deadline as the source of truth.
51+
const now = dayjs().utc()
4652
if (now.isAfter(deadline)) {
47-
return "Workspace is shutting down now"
53+
return "Workspace is shutting down"
54+
} else {
55+
return now.to(deadline)
4856
}
49-
return now.to(deadline)
57+
} else if (!ttl || ttl < 1) {
58+
// If the workspace is not on, and the ttl is 0 or undefined, then the
59+
// workspace is set to manually shutdown.
60+
return "Manual"
61+
} else {
62+
// The workspace has a ttl set, but is either in an unknown state or is
63+
// not running. Therefore, we derive from workspace.ttl.
64+
const duration = dayjs.duration(ttl / 1_000_000, "milliseconds")
65+
return `${duration.humanize()} after start`
5066
}
51-
52-
const duration = dayjs.duration(workspace.ttl / 1_000_000, "milliseconds")
53-
return `${duration.humanize()} after start`
5467
},
5568
editScheduleLink: "Edit schedule",
5669
schedule: "Schedule",

site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.test.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ describe("WorkspaceScheduleBanner", () => {
9191
latest_build: {
9292
...Mocks.MockWorkspaceBuild,
9393
deadline: dayjs().add(27, "minutes").utc().format(),
94-
job: Mocks.MockRunningProvisionerJob,
9594
transition: "start",
9695
},
9796
}

site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.tsx

+2-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
55
import utc from "dayjs/plugin/utc"
66
import { FC } from "react"
77
import * as TypesGen from "../../api/typesGenerated"
8+
import { isWorkspaceOn } from "../../util/workspace"
89

910
dayjs.extend(utc)
1011
dayjs.extend(isSameOrBefore)
@@ -18,12 +19,7 @@ export interface WorkspaceScheduleBannerProps {
1819
}
1920

2021
export const shouldDisplay = (workspace: TypesGen.Workspace): boolean => {
21-
const transition = workspace.latest_build.transition
22-
const status = workspace.latest_build.job.status
23-
24-
if (transition !== "start") {
25-
return false
26-
} else if (status === "canceled" || status === "canceling" || status === "failed") {
22+
if (!isWorkspaceOn(workspace)) {
2723
return false
2824
} else {
2925
// a mannual shutdown has a deadline of '"0001-01-01T00:00:00Z"'

site/src/testHelpers/entities.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,24 @@ export const MockWorkspace: TypesGen.Workspace = {
167167
latest_build: MockWorkspaceBuild,
168168
}
169169

170-
export const MockStoppedWorkspace: TypesGen.Workspace = { ...MockWorkspace, latest_build: MockWorkspaceBuildStop }
170+
export const MockStoppedWorkspace: TypesGen.Workspace = {
171+
...MockWorkspace,
172+
latest_build: MockWorkspaceBuildStop,
173+
}
171174
export const MockStoppingWorkspace: TypesGen.Workspace = {
172175
...MockWorkspace,
173-
latest_build: { ...MockWorkspaceBuildStop, job: MockRunningProvisionerJob },
176+
latest_build: {
177+
...MockWorkspaceBuildStop,
178+
job: MockRunningProvisionerJob,
179+
},
174180
}
175181
export const MockStartingWorkspace: TypesGen.Workspace = {
176182
...MockWorkspace,
177-
latest_build: { ...MockWorkspaceBuild, job: MockRunningProvisionerJob },
183+
latest_build: {
184+
...MockWorkspaceBuild,
185+
job: MockRunningProvisionerJob,
186+
transition: "start",
187+
},
178188
}
179189
export const MockCancelingWorkspace: TypesGen.Workspace = {
180190
...MockWorkspace,
@@ -186,7 +196,10 @@ export const MockCanceledWorkspace: TypesGen.Workspace = {
186196
}
187197
export const MockFailedWorkspace: TypesGen.Workspace = {
188198
...MockWorkspace,
189-
latest_build: { ...MockWorkspaceBuild, job: MockFailedProvisionerJob },
199+
latest_build: {
200+
...MockWorkspaceBuild,
201+
job: MockFailedProvisionerJob,
202+
},
190203
}
191204
export const MockDeletingWorkspace: TypesGen.Workspace = {
192205
...MockWorkspace,

site/src/util/workspace.test.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as TypesGen from "../api/typesGenerated"
2+
import * as Mocks from "../testHelpers/entities"
3+
import { isWorkspaceOn } from "./workspace"
4+
5+
describe("util > workspace", () => {
6+
describe("isWorkspaceOn", () => {
7+
it.each<[TypesGen.WorkspaceTransition, TypesGen.ProvisionerJobStatus, boolean]>([
8+
["delete", "canceled", false],
9+
["delete", "canceling", false],
10+
["delete", "failed", false],
11+
["delete", "pending", false],
12+
["delete", "running", false],
13+
["delete", "succeeded", false],
14+
15+
["stop", "canceled", false],
16+
["stop", "canceling", false],
17+
["stop", "failed", false],
18+
["stop", "pending", false],
19+
["stop", "running", false],
20+
["stop", "succeeded", false],
21+
22+
["start", "canceled", false],
23+
["start", "canceling", false],
24+
["start", "failed", false],
25+
["start", "pending", false],
26+
["start", "running", false],
27+
["start", "succeeded", true],
28+
])(`transition=%p, status=%p, isWorkspaceOn=%p`, (transition, status, isOn) => {
29+
const workspace: TypesGen.Workspace = {
30+
...Mocks.MockWorkspace,
31+
latest_build: {
32+
...Mocks.MockWorkspaceBuild,
33+
job: {
34+
...Mocks.MockProvisionerJob,
35+
status,
36+
},
37+
transition,
38+
},
39+
}
40+
expect(isWorkspaceOn(workspace)).toBe(isOn)
41+
})
42+
})
43+
})

site/src/util/workspace.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Theme } from "@material-ui/core/styles"
22
import dayjs from "dayjs"
33
import { WorkspaceBuildTransition } from "../api/types"
4-
import { WorkspaceAgent, WorkspaceBuild } from "../api/typesGenerated"
4+
import { Workspace, WorkspaceAgent, WorkspaceBuild } from "../api/typesGenerated"
55

66
export type WorkspaceStatus =
77
| "queued"
@@ -185,3 +185,9 @@ export const getDisplayAgentStatus = (
185185
}
186186
}
187187
}
188+
189+
export const isWorkspaceOn = (workspace: Workspace): boolean => {
190+
const transition = workspace.latest_build.transition
191+
const status = workspace.latest_build.job.status
192+
return transition === "start" && status === "succeeded"
193+
}

0 commit comments

Comments
 (0)