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

Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
7 changes: 7 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,10 @@ export const getWorkspaceBuildLogs = async (buildname: string): Promise<TypesGen
const response = await axios.get<TypesGen.ProvisionerJobLog[]>(`/api/v2/workspacebuilds/${buildname}/logs`)
return response.data
}

export const putWorkspaceExtension = async (
workspaceId: string,
extendWorkspaceRequest: TypesGen.PutExtendWorkspaceRequest,
): Promise<void> => {
await axios.put(`/api/v2/workspaces/${workspaceId}/extend`, extendWorkspaceRequest)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { action } from "@storybook/addon-actions"
import { Story } from "@storybook/react"
import dayjs from "dayjs"
import utc from "dayjs/plugin/utc"
Expand All @@ -15,6 +16,7 @@ const Template: Story<WorkspaceScheduleBannerProps> = (args) => <WorkspaceSchedu

export const Example = Template.bind({})
Example.args = {
__onExtend: action("extend"),
workspace: {
...Mocks.MockWorkspace,
latest_build: {
Expand All @@ -26,6 +28,6 @@ Example.args = {
},
transition: "start",
},
ttl: 2 * 60 * 60 * 1000 * 1_000_000, // 2 hours
ttl_ms: 2 * 60 * 60 * 1000, // 2 hours
},
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import Button from "@material-ui/core/Button"
import Alert from "@material-ui/lab/Alert"
import AlertTitle from "@material-ui/lab/AlertTitle"
import { useMachine } from "@xstate/react"
import dayjs from "dayjs"
import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
import utc from "dayjs/plugin/utc"
import { FC } from "react"
import * as TypesGen from "../../api/typesGenerated"
import { isWorkspaceOn } from "../../util/workspace"
import { workspaceScheduleBanner } from "../../xServices/workspaceSchedule/workspaceScheduleBannerXService"

dayjs.extend(utc)
dayjs.extend(isSameOrBefore)

export const Language = {
bannerAction: "Extend",
bannerTitle: "Your workspace is scheduled to automatically shut down soon.",
}

export interface WorkspaceScheduleBannerProps {
/**
* @remarks __onExtend is used for testing purposes
*/
__onExtend?: () => void
workspace: TypesGen.Workspace
}

Expand All @@ -31,12 +39,32 @@ export const shouldDisplay = (workspace: TypesGen.Workspace): boolean => {
}
}

export const WorkspaceScheduleBanner: FC<WorkspaceScheduleBannerProps> = ({ workspace }) => {
export const WorkspaceScheduleBanner: FC<WorkspaceScheduleBannerProps> = ({ __onExtend, workspace }) => {
const [bannerState, bannerSend] = useMachine(workspaceScheduleBanner)

if (!shouldDisplay(workspace)) {
return null
} else {
return (
<Alert severity="warning">
<Alert
action={
<Button
color="inherit"
disabled={bannerState.hasTag("loading")}
onClick={() => {
if (__onExtend) {
__onExtend()
} else {
bannerSend({ type: "EXTEND_DEADLINE_DEFAULT", workspaceId: workspace.id })
}
}}
size="small"
>
{Language.bannerAction}
</Button>
}
severity="warning"
>
<AlertTitle>{Language.bannerTitle}</AlertTitle>
</Alert>
)
Expand Down
7 changes: 5 additions & 2 deletions site/src/testHelpers/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ export const handlers = [
rest.put("/api/v2/workspaces/:workspaceId/ttl", async (req, res, ctx) => {
return res(ctx.status(200))
}),
rest.put("/api/v2/workspaces/:workspaceId/extend", async (req, res, ctx) => {
return res(ctx.status(200))
}),

// workspace builds
rest.post("/api/v2/workspaces/:workspaceId/builds", async (req, res, ctx) => {
const { transition } = req.body as CreateWorkspaceBuildRequest
const transitionToBuild = {
Expand All @@ -122,8 +127,6 @@ export const handlers = [
rest.get("/api/v2/workspaces/:workspaceId/builds", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockBuilds))
}),

// workspace builds
rest.get("/api/v2/workspacebuilds/:workspaceBuildId", (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspaceBuild))
}),
Expand Down
25 changes: 24 additions & 1 deletion site/src/util/workspace.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import dayjs from "dayjs"
import * as TypesGen from "../api/typesGenerated"
import * as Mocks from "../testHelpers/entities"
import { isWorkspaceOn } from "./workspace"
import { defaultWorkspaceExtension, isWorkspaceOn } from "./workspace"

describe("util > workspace", () => {
describe("isWorkspaceOn", () => {
Expand Down Expand Up @@ -40,4 +41,26 @@ describe("util > workspace", () => {
expect(isWorkspaceOn(workspace)).toBe(isOn)
})
})

describe("defaultWorkspaceExtension", () => {
it.each<[string, TypesGen.PutExtendWorkspaceRequest]>([
[
"2022-06-02T14:56:34Z",
{
deadline: "2022-06-02T16:26:34Z",
},
],

// This case is the same as above, but in a different timezone to prove
// that UTC conversion for deadline works as expected
[
"2022-06-02T10:56:20-04:00",
{
deadline: "2022-06-02T16:26:20Z",
},
],
])(`defaultWorkspaceExtension(%p) returns %p`, (startTime, request) => {
expect(defaultWorkspaceExtension(dayjs(startTime))).toEqual(request)
})
})
})
29 changes: 22 additions & 7 deletions site/src/util/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Theme } from "@material-ui/core/styles"
import dayjs from "dayjs"
import utc from "dayjs/plugin/utc"
import { WorkspaceBuildTransition } from "../api/types"
import { Workspace, WorkspaceAgent, WorkspaceBuild } from "../api/typesGenerated"
import * as TypesGen from "../api/typesGenerated"

dayjs.extend(utc)

export type WorkspaceStatus =
| "queued"
Expand Down Expand Up @@ -29,7 +32,7 @@ const succeededToStatus: Record<WorkspaceBuildTransition, WorkspaceStatus> = {
}

// Converts a workspaces status to a human-readable form.
export const getWorkspaceStatus = (workspaceBuild?: WorkspaceBuild): WorkspaceStatus => {
export const getWorkspaceStatus = (workspaceBuild?: TypesGen.WorkspaceBuild): WorkspaceStatus => {
const transition = workspaceBuild?.transition as WorkspaceBuildTransition
const jobStatus = workspaceBuild?.job.status
switch (jobStatus) {
Expand Down Expand Up @@ -66,7 +69,7 @@ export const DisplayStatusLanguage = {

export const getDisplayStatus = (
theme: Theme,
build: WorkspaceBuild,
build: TypesGen.WorkspaceBuild,
): {
color: string
status: string
Expand Down Expand Up @@ -132,7 +135,7 @@ export const getDisplayStatus = (
throw new Error("unknown status " + status)
}

export const getWorkspaceBuildDurationInSeconds = (build: WorkspaceBuild): number | undefined => {
export const getWorkspaceBuildDurationInSeconds = (build: TypesGen.WorkspaceBuild): number | undefined => {
const isCompleted = build.job.started_at && build.job.completed_at

if (!isCompleted) {
Expand All @@ -144,7 +147,10 @@ export const getWorkspaceBuildDurationInSeconds = (build: WorkspaceBuild): numbe
return completedAt.diff(startedAt, "seconds")
}

export const displayWorkspaceBuildDuration = (build: WorkspaceBuild, inProgressLabel = "In progress"): string => {
export const displayWorkspaceBuildDuration = (
build: TypesGen.WorkspaceBuild,
inProgressLabel = "In progress",
): string => {
const duration = getWorkspaceBuildDurationInSeconds(build)
return duration ? `${duration} seconds` : inProgressLabel
}
Expand All @@ -157,7 +163,7 @@ export const DisplayAgentStatusLanguage = {

export const getDisplayAgentStatus = (
theme: Theme,
agent: WorkspaceAgent,
agent: TypesGen.WorkspaceAgent,
): {
color: string
status: string
Expand Down Expand Up @@ -186,8 +192,17 @@ export const getDisplayAgentStatus = (
}
}

export const isWorkspaceOn = (workspace: Workspace): boolean => {
export const isWorkspaceOn = (workspace: TypesGen.Workspace): boolean => {
const transition = workspace.latest_build.transition
const status = workspace.latest_build.job.status
return transition === "start" && status === "succeeded"
}

export const defaultWorkspaceExtension = (__startDate?: dayjs.Dayjs): TypesGen.PutExtendWorkspaceRequest => {
const now = __startDate ? dayjs(__startDate) : dayjs()
const NinetyMinutesFromNow = now.add(90, "minutes").utc()

return {
deadline: NinetyMinutesFromNow.format(),
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* @fileoverview workspaceScheduleBanner is an xstate machine backing a form,
* presented as an Alert/banner, for reactively extending a workspace schedule.
*/
import { createMachine } from "xstate"
import * as API from "../../api/api"
import { displayError, displaySuccess } from "../../components/GlobalSnackbar/utils"
import { defaultWorkspaceExtension } from "../../util/workspace"

export const Language = {
errorExtension: "Failed to extend workspace deadline.",
successExtension: "Successfully extended workspace deadline.",
}

export type WorkspaceScheduleBannerEvent = { type: "EXTEND_DEADLINE_DEFAULT"; workspaceId: string }

export const workspaceScheduleBanner = createMachine(
{
tsTypes: {} as import("./workspaceScheduleBannerXService.typegen").Typegen0,
schema: {
events: {} as WorkspaceScheduleBannerEvent,
},
id: "workspaceScheduleBannerState",
initial: "idle",
states: {
idle: {
on: {
EXTEND_DEADLINE_DEFAULT: "extendingDeadline",
},
},
extendingDeadline: {
invoke: {
src: "extendDeadlineDefault",
id: "extendDeadlineDefault",
onDone: {
target: "idle",
actions: "displaySuccessMessage",
},
onError: {
target: "idle",
actions: "displayFailureMessage",
},
},
tags: "loading",
},
},
},
{
actions: {
displayFailureMessage: () => {
displayError(Language.errorExtension)
},
displaySuccessMessage: () => {
displaySuccess(Language.successExtension)
},
},

services: {
extendDeadlineDefault: async (_, event) => {
await API.putWorkspaceExtension(event.workspaceId, defaultWorkspaceExtension())
},
},
},
)