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.
This commit is contained in:
G r e y
2022-05-27 15:23:56 -04:00
committed by GitHub
parent ff542afe87
commit 8d7499feb7
5 changed files with 204 additions and 1 deletions
@@ -8,6 +8,7 @@ import { Resources } from "../Resources/Resources"
import { Stack } from "../Stack/Stack"
import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule"
import { WorkspaceScheduleBanner } from "../WorkspaceScheduleBanner/WorkspaceScheduleBanner"
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
@@ -63,8 +64,12 @@ export const Workspace: React.FC<WorkspaceProps> = ({
<Stack direction="row" spacing={3} className={styles.layout}>
<Stack spacing={3} className={styles.main}>
<WorkspaceScheduleBanner workspace={workspace} />
<WorkspaceStats workspace={workspace} />
<Resources resources={resources} getResourcesError={getResourcesError} workspace={workspace} />
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
<BuildsTable builds={builds} className={styles.timelineTable} />
</WorkspaceSection>
@@ -0,0 +1,32 @@
import { Story } from "@storybook/react"
import dayjs from "dayjs"
import utc from "dayjs/plugin/utc"
import React from "react"
import * as Mocks from "../../testHelpers/entities"
import { WorkspaceScheduleBanner, WorkspaceScheduleBannerProps } from "./WorkspaceScheduleBanner"
dayjs.extend(utc)
export default {
title: "components/WorkspaceScheduleBanner",
component: WorkspaceScheduleBanner,
}
const Template: Story<WorkspaceScheduleBannerProps> = (args) => <WorkspaceScheduleBanner {...args} />
export const Example = Template.bind({})
Example.args = {
workspace: {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
deadline: dayjs().utc().format(),
job: {
...Mocks.MockProvisionerJob,
status: "succeeded",
},
transition: "start",
},
ttl: 2 * 60 * 60 * 1000 * 1_000_000, // 2 hours
},
}
@@ -0,0 +1,117 @@
import dayjs from "dayjs"
import utc from "dayjs/plugin/utc"
import * as TypesGen from "../../api/typesGenerated"
import * as Mocks from "../../testHelpers/entities"
import { shouldDisplay } from "./WorkspaceScheduleBanner"
dayjs.extend(utc)
describe("WorkspaceScheduleBanner", () => {
describe("shouldDisplay", () => {
// Manual TTL case
it("should not display if the build does not have a deadline", () => {
// Given: a workspace with deadline of '"0001-01-01T00:00:00Z"'
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
deadline: "0001-01-01T00:00:00Z",
transition: "start",
},
}
// Then: shouldDisplay is false
expect(shouldDisplay(workspace)).toBeFalsy()
})
// Transition Checks
it("should not display if the latest build is not transition=start", () => {
// Given: a workspace with latest build as "stop"
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
transition: "stop",
},
}
// Then: shouldDisplay is false
expect(shouldDisplay(workspace)).toBeFalsy()
})
// Provisioner Job Checks
it("should not display if the latest build is canceling", () => {
// Given: a workspace with latest build as "canceling"
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
job: Mocks.MockCancelingProvisionerJob,
transition: "start",
},
}
// Then: shouldDisplay is false
expect(shouldDisplay(workspace)).toBeFalsy()
})
it("should not display if the latest build is canceled", () => {
// Given: a workspace with latest build as "canceled"
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
job: Mocks.MockCanceledProvisionerJob,
transition: "start",
},
}
// Then: shouldDisplay is false
expect(shouldDisplay(workspace)).toBeFalsy()
})
it("should not display if the latest build failed", () => {
// Given: a workspace with latest build as "failed"
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
job: Mocks.MockFailedProvisionerJob,
transition: "start",
},
}
// Then: shouldDisplay is false
expect(shouldDisplay(workspace)).toBeFalsy()
})
// Deadline Checks
it("should display if deadline is within 30 minutes", () => {
// Given: a workspace with latest build as start and deadline in ~30 mins
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
deadline: dayjs().add(27, "minutes").utc().format(),
job: Mocks.MockRunningProvisionerJob,
transition: "start",
},
}
// Then: shouldDisplay is true
expect(shouldDisplay(workspace)).toBeTruthy()
})
it("should not display if deadline is 45 minutes", () => {
// Given: a workspace with latest build as start and deadline in 45 mins
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
deadline: dayjs().add(45, "minutes").utc().format(),
transition: "start",
},
}
// Then: shouldDisplay is false
expect(shouldDisplay(workspace)).toBeFalsy()
})
})
})
@@ -0,0 +1,48 @@
import Alert from "@material-ui/lab/Alert"
import AlertTitle from "@material-ui/lab/AlertTitle"
import dayjs from "dayjs"
import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
import utc from "dayjs/plugin/utc"
import React from "react"
import * as TypesGen from "../../api/typesGenerated"
dayjs.extend(utc)
dayjs.extend(isSameOrBefore)
export const Language = {
bannerTitle: "Your workspace is scheduled to automatically shut down soon.",
}
export interface WorkspaceScheduleBannerProps {
workspace: TypesGen.Workspace
}
export const shouldDisplay = (workspace: TypesGen.Workspace): boolean => {
const transition = workspace.latest_build.transition
const status = workspace.latest_build.job.status
if (transition !== "start") {
return false
} else if (status === "canceled" || status === "canceling" || status === "failed") {
return false
} else {
// a mannual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
// SEE: #1834
const deadline = dayjs(workspace.latest_build.deadline).utc()
const hasDeadline = deadline.year() > 1
const thirtyMinutesFromNow = dayjs().add(30, "minutes").utc()
return hasDeadline && deadline.isSameOrBefore(thirtyMinutesFromNow)
}
}
export const WorkspaceScheduleBanner: React.FC<WorkspaceScheduleBannerProps> = ({ workspace }) => {
if (!shouldDisplay(workspace)) {
return null
} else {
return (
<Alert severity="warning">
<AlertTitle>{Language.bannerTitle}</AlertTitle>
</Alert>
)
}
}
@@ -370,7 +370,8 @@ export const workspaceMachine = createMachine(
const oldBuilds = context.builds
if (!oldBuilds) {
throw new Error("Builds not loaded")
// This state is theoretically impossible, but helps TS
throw new Error("workspaceXService: failed to load workspace builds")
}
return [...oldBuilds, ...event.data]