mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
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:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user