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

Skip to content

Commit 8d7499f

Browse files
authored
feat: ui alert <= 30mins from deadline (#1825)
Summary: When a workspace build is <= 30 minutes from auto-scheduled shutdown, then an alert banner is displayed on the workspace page.
1 parent ff542af commit 8d7499f

File tree

5 files changed

+204
-1
lines changed

5 files changed

+204
-1
lines changed

site/src/components/Workspace/Workspace.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Resources } from "../Resources/Resources"
88
import { Stack } from "../Stack/Stack"
99
import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
1010
import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule"
11+
import { WorkspaceScheduleBanner } from "../WorkspaceScheduleBanner/WorkspaceScheduleBanner"
1112
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
1213
import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
1314

@@ -63,8 +64,12 @@ export const Workspace: React.FC<WorkspaceProps> = ({
6364

6465
<Stack direction="row" spacing={3} className={styles.layout}>
6566
<Stack spacing={3} className={styles.main}>
67+
<WorkspaceScheduleBanner workspace={workspace} />
68+
6669
<WorkspaceStats workspace={workspace} />
70+
6771
<Resources resources={resources} getResourcesError={getResourcesError} workspace={workspace} />
72+
6873
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
6974
<BuildsTable builds={builds} className={styles.timelineTable} />
7075
</WorkspaceSection>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Story } from "@storybook/react"
2+
import dayjs from "dayjs"
3+
import utc from "dayjs/plugin/utc"
4+
import React from "react"
5+
import * as Mocks from "../../testHelpers/entities"
6+
import { WorkspaceScheduleBanner, WorkspaceScheduleBannerProps } from "./WorkspaceScheduleBanner"
7+
8+
dayjs.extend(utc)
9+
10+
export default {
11+
title: "components/WorkspaceScheduleBanner",
12+
component: WorkspaceScheduleBanner,
13+
}
14+
15+
const Template: Story<WorkspaceScheduleBannerProps> = (args) => <WorkspaceScheduleBanner {...args} />
16+
17+
export const Example = Template.bind({})
18+
Example.args = {
19+
workspace: {
20+
...Mocks.MockWorkspace,
21+
latest_build: {
22+
...Mocks.MockWorkspaceBuild,
23+
deadline: dayjs().utc().format(),
24+
job: {
25+
...Mocks.MockProvisionerJob,
26+
status: "succeeded",
27+
},
28+
transition: "start",
29+
},
30+
ttl: 2 * 60 * 60 * 1000 * 1_000_000, // 2 hours
31+
},
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import dayjs from "dayjs"
2+
import utc from "dayjs/plugin/utc"
3+
import * as TypesGen from "../../api/typesGenerated"
4+
import * as Mocks from "../../testHelpers/entities"
5+
import { shouldDisplay } from "./WorkspaceScheduleBanner"
6+
7+
dayjs.extend(utc)
8+
9+
describe("WorkspaceScheduleBanner", () => {
10+
describe("shouldDisplay", () => {
11+
// Manual TTL case
12+
it("should not display if the build does not have a deadline", () => {
13+
// Given: a workspace with deadline of '"0001-01-01T00:00:00Z"'
14+
const workspace: TypesGen.Workspace = {
15+
...Mocks.MockWorkspace,
16+
latest_build: {
17+
...Mocks.MockWorkspaceBuild,
18+
deadline: "0001-01-01T00:00:00Z",
19+
transition: "start",
20+
},
21+
}
22+
23+
// Then: shouldDisplay is false
24+
expect(shouldDisplay(workspace)).toBeFalsy()
25+
})
26+
27+
// Transition Checks
28+
it("should not display if the latest build is not transition=start", () => {
29+
// Given: a workspace with latest build as "stop"
30+
const workspace: TypesGen.Workspace = {
31+
...Mocks.MockWorkspace,
32+
latest_build: {
33+
...Mocks.MockWorkspaceBuild,
34+
transition: "stop",
35+
},
36+
}
37+
38+
// Then: shouldDisplay is false
39+
expect(shouldDisplay(workspace)).toBeFalsy()
40+
})
41+
42+
// Provisioner Job Checks
43+
it("should not display if the latest build is canceling", () => {
44+
// Given: a workspace with latest build as "canceling"
45+
const workspace: TypesGen.Workspace = {
46+
...Mocks.MockWorkspace,
47+
latest_build: {
48+
...Mocks.MockWorkspaceBuild,
49+
job: Mocks.MockCancelingProvisionerJob,
50+
transition: "start",
51+
},
52+
}
53+
54+
// Then: shouldDisplay is false
55+
expect(shouldDisplay(workspace)).toBeFalsy()
56+
})
57+
it("should not display if the latest build is canceled", () => {
58+
// Given: a workspace with latest build as "canceled"
59+
const workspace: TypesGen.Workspace = {
60+
...Mocks.MockWorkspace,
61+
latest_build: {
62+
...Mocks.MockWorkspaceBuild,
63+
job: Mocks.MockCanceledProvisionerJob,
64+
transition: "start",
65+
},
66+
}
67+
68+
// Then: shouldDisplay is false
69+
expect(shouldDisplay(workspace)).toBeFalsy()
70+
})
71+
it("should not display if the latest build failed", () => {
72+
// Given: a workspace with latest build as "failed"
73+
const workspace: TypesGen.Workspace = {
74+
...Mocks.MockWorkspace,
75+
latest_build: {
76+
...Mocks.MockWorkspaceBuild,
77+
job: Mocks.MockFailedProvisionerJob,
78+
transition: "start",
79+
},
80+
}
81+
82+
// Then: shouldDisplay is false
83+
expect(shouldDisplay(workspace)).toBeFalsy()
84+
})
85+
86+
// Deadline Checks
87+
it("should display if deadline is within 30 minutes", () => {
88+
// Given: a workspace with latest build as start and deadline in ~30 mins
89+
const workspace: TypesGen.Workspace = {
90+
...Mocks.MockWorkspace,
91+
latest_build: {
92+
...Mocks.MockWorkspaceBuild,
93+
deadline: dayjs().add(27, "minutes").utc().format(),
94+
job: Mocks.MockRunningProvisionerJob,
95+
transition: "start",
96+
},
97+
}
98+
99+
// Then: shouldDisplay is true
100+
expect(shouldDisplay(workspace)).toBeTruthy()
101+
})
102+
it("should not display if deadline is 45 minutes", () => {
103+
// Given: a workspace with latest build as start and deadline in 45 mins
104+
const workspace: TypesGen.Workspace = {
105+
...Mocks.MockWorkspace,
106+
latest_build: {
107+
...Mocks.MockWorkspaceBuild,
108+
deadline: dayjs().add(45, "minutes").utc().format(),
109+
transition: "start",
110+
},
111+
}
112+
113+
// Then: shouldDisplay is false
114+
expect(shouldDisplay(workspace)).toBeFalsy()
115+
})
116+
})
117+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Alert from "@material-ui/lab/Alert"
2+
import AlertTitle from "@material-ui/lab/AlertTitle"
3+
import dayjs from "dayjs"
4+
import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
5+
import utc from "dayjs/plugin/utc"
6+
import React from "react"
7+
import * as TypesGen from "../../api/typesGenerated"
8+
9+
dayjs.extend(utc)
10+
dayjs.extend(isSameOrBefore)
11+
12+
export const Language = {
13+
bannerTitle: "Your workspace is scheduled to automatically shut down soon.",
14+
}
15+
16+
export interface WorkspaceScheduleBannerProps {
17+
workspace: TypesGen.Workspace
18+
}
19+
20+
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") {
27+
return false
28+
} else {
29+
// a mannual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
30+
// SEE: #1834
31+
const deadline = dayjs(workspace.latest_build.deadline).utc()
32+
const hasDeadline = deadline.year() > 1
33+
const thirtyMinutesFromNow = dayjs().add(30, "minutes").utc()
34+
return hasDeadline && deadline.isSameOrBefore(thirtyMinutesFromNow)
35+
}
36+
}
37+
38+
export const WorkspaceScheduleBanner: React.FC<WorkspaceScheduleBannerProps> = ({ workspace }) => {
39+
if (!shouldDisplay(workspace)) {
40+
return null
41+
} else {
42+
return (
43+
<Alert severity="warning">
44+
<AlertTitle>{Language.bannerTitle}</AlertTitle>
45+
</Alert>
46+
)
47+
}
48+
}

site/src/xServices/workspace/workspaceXService.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,8 @@ export const workspaceMachine = createMachine(
370370
const oldBuilds = context.builds
371371

372372
if (!oldBuilds) {
373-
throw new Error("Builds not loaded")
373+
// This state is theoretically impossible, but helps TS
374+
throw new Error("workspaceXService: failed to load workspace builds")
374375
}
375376

376377
return [...oldBuilds, ...event.data]

0 commit comments

Comments
 (0)