Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: ui alert <= 30mins from deadline
Summary:

When a workspace build is <= 30 minutes from auto-scheduled shutdown,
then an alert banner is displayed on the workspace page.
  • Loading branch information
greyscaled committed May 27, 2022
commit f049ae9173a616832b9ccc6a5fe691eadd373f7c
5 changes: 5 additions & 0 deletions site/src/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
},
}
Original file line number Diff line number Diff line change
@@ -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()
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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: "Workspace Shutdown",
bannerDetail: "Your workspace will shutdown 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>
{Language.bannerDetail}
</Alert>
)
}
}
3 changes: 2 additions & 1 deletion site/src/xServices/workspace/workspaceXService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down