mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: refactor schedule banner (#4274)
* Start refactor * Fix color of auto stop switch * Format * Use helper functions for min/max check * Fix type * Put new component in own file * Fix decrease deadline bug * Simplify functions * Use ChooseOne * Remove commented code
This commit is contained in:
@@ -309,6 +309,7 @@ export const WorkspaceScheduleForm: FC<React.PropsWithChildren<WorkspaceSchedule
|
||||
name="autoStopEnabled"
|
||||
checked={form.values.autoStopEnabled}
|
||||
onChange={handleToggleAutoStop}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={Language.stopSwitch}
|
||||
|
||||
@@ -1,52 +1,21 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { useActor, useMachine, useSelector } from "@xstate/react"
|
||||
import { FeatureNames } from "api/types"
|
||||
import dayjs from "dayjs"
|
||||
import minMax from "dayjs/plugin/minMax"
|
||||
import { FC, useContext, useEffect } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||
import { FC, useEffect } from "react"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors"
|
||||
import { DeleteDialog } from "../../components/Dialogs/DeleteDialog/DeleteDialog"
|
||||
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
|
||||
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
|
||||
import { Workspace, WorkspaceErrors } from "../../components/Workspace/Workspace"
|
||||
import { firstOrItem } from "../../util/array"
|
||||
import { pageTitle } from "../../util/page"
|
||||
import { canExtendDeadline, canReduceDeadline, maxDeadline, minDeadline } from "../../util/schedule"
|
||||
import { getFaviconByStatus } from "../../util/workspace"
|
||||
import { XServiceContext } from "../../xServices/StateContext"
|
||||
import { workspaceMachine } from "../../xServices/workspace/workspaceXService"
|
||||
import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedule/workspaceScheduleBannerXService"
|
||||
|
||||
dayjs.extend(minMax)
|
||||
import { WorkspaceReadyPage } from "./WorkspaceReadyPage"
|
||||
|
||||
export const WorkspacePage: FC = () => {
|
||||
const { username: usernameQueryParam, workspace: workspaceQueryParam } = useParams()
|
||||
const username = firstOrItem(usernameQueryParam, null)
|
||||
const workspaceName = firstOrItem(workspaceQueryParam, null)
|
||||
const { t } = useTranslation("workspacePage")
|
||||
const xServices = useContext(XServiceContext)
|
||||
const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility)
|
||||
const [workspaceState, workspaceSend] = useMachine(workspaceMachine)
|
||||
const {
|
||||
workspace,
|
||||
getWorkspaceError,
|
||||
template,
|
||||
getTemplateWarning,
|
||||
refreshWorkspaceWarning,
|
||||
builds,
|
||||
getBuildsError,
|
||||
permissions,
|
||||
checkPermissionsError,
|
||||
buildError,
|
||||
cancellationError,
|
||||
applicationsHost,
|
||||
} = workspaceState.context
|
||||
const canUpdateWorkspace = Boolean(permissions?.updateWorkspace)
|
||||
const [bannerState, bannerSend] = useMachine(workspaceScheduleBannerMachine)
|
||||
const [buildInfoState] = useActor(xServices.buildInfoXService)
|
||||
const { workspace, getWorkspaceError, getTemplateWarning, checkPermissionsError } =
|
||||
workspaceState.context
|
||||
const styles = useStyles()
|
||||
|
||||
/**
|
||||
@@ -57,95 +26,23 @@ export const WorkspacePage: FC = () => {
|
||||
username && workspaceName && workspaceSend({ type: "GET_WORKSPACE", username, workspaceName })
|
||||
}, [username, workspaceName, workspaceSend])
|
||||
|
||||
if (workspaceState.matches("error")) {
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
{Boolean(getWorkspaceError) && <ErrorSummary error={getWorkspaceError} />}
|
||||
{Boolean(getTemplateWarning) && <ErrorSummary error={getTemplateWarning} />}
|
||||
{Boolean(checkPermissionsError) && <ErrorSummary error={checkPermissionsError} />}
|
||||
</div>
|
||||
)
|
||||
} else if (!workspace || !permissions) {
|
||||
return <FullScreenLoader />
|
||||
} else if (!template) {
|
||||
return <FullScreenLoader />
|
||||
} else {
|
||||
const deadline = dayjs(workspace.latest_build.deadline).utc()
|
||||
const favicon = getFaviconByStatus(workspace.latest_build)
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle(`${workspace.owner_name}/${workspace.name}`)}</title>
|
||||
<link rel="alternate icon" type="image/png" href={`/favicons/${favicon}.png`} />
|
||||
<link rel="icon" type="image/svg+xml" href={`/favicons/${favicon}.svg`} />
|
||||
</Helmet>
|
||||
|
||||
<Workspace
|
||||
bannerProps={{
|
||||
isLoading: bannerState.hasTag("loading"),
|
||||
onExtend: () => {
|
||||
bannerSend({
|
||||
type: "UPDATE_DEADLINE",
|
||||
workspaceId: workspace.id,
|
||||
newDeadline: dayjs.min(deadline.add(4, "hours"), maxDeadline(workspace, template)),
|
||||
})
|
||||
},
|
||||
}}
|
||||
scheduleProps={{
|
||||
onDeadlineMinus: () => {
|
||||
bannerSend({
|
||||
type: "UPDATE_DEADLINE",
|
||||
workspaceId: workspace.id,
|
||||
newDeadline: dayjs.max(deadline.add(-1, "hours"), minDeadline()),
|
||||
})
|
||||
},
|
||||
onDeadlinePlus: () => {
|
||||
bannerSend({
|
||||
type: "UPDATE_DEADLINE",
|
||||
workspaceId: workspace.id,
|
||||
newDeadline: dayjs.min(deadline.add(1, "hours"), maxDeadline(workspace, template)),
|
||||
})
|
||||
},
|
||||
deadlineMinusEnabled: () => {
|
||||
return canReduceDeadline(deadline)
|
||||
},
|
||||
deadlinePlusEnabled: () => {
|
||||
return canExtendDeadline(deadline, workspace, template)
|
||||
},
|
||||
}}
|
||||
isUpdating={workspaceState.hasTag("updating")}
|
||||
workspace={workspace}
|
||||
handleStart={() => workspaceSend("START")}
|
||||
handleStop={() => workspaceSend("STOP")}
|
||||
handleDelete={() => workspaceSend("ASK_DELETE")}
|
||||
handleUpdate={() => workspaceSend("UPDATE")}
|
||||
handleCancel={() => workspaceSend("CANCEL")}
|
||||
resources={workspace.latest_build.resources}
|
||||
builds={builds}
|
||||
canUpdateWorkspace={canUpdateWorkspace}
|
||||
hideSSHButton={featureVisibility[FeatureNames.BrowserOnly]}
|
||||
workspaceErrors={{
|
||||
[WorkspaceErrors.GET_RESOURCES_ERROR]: refreshWorkspaceWarning,
|
||||
[WorkspaceErrors.GET_BUILDS_ERROR]: getBuildsError,
|
||||
[WorkspaceErrors.BUILD_ERROR]: buildError,
|
||||
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
|
||||
}}
|
||||
buildInfo={buildInfoState.context.buildInfo}
|
||||
applicationsHost={applicationsHost}
|
||||
/>
|
||||
<DeleteDialog
|
||||
entity="workspace"
|
||||
name={workspace.name}
|
||||
info={t("deleteDialog.info", { timeAgo: dayjs(workspace.created_at).fromNow() })}
|
||||
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
|
||||
onCancel={() => workspaceSend("CANCEL_DELETE")}
|
||||
onConfirm={() => {
|
||||
workspaceSend("DELETE")
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ChooseOne>
|
||||
<Cond condition={workspaceState.matches("error")}>
|
||||
<div className={styles.error}>
|
||||
{Boolean(getWorkspaceError) && <ErrorSummary error={getWorkspaceError} />}
|
||||
{Boolean(getTemplateWarning) && <ErrorSummary error={getTemplateWarning} />}
|
||||
{Boolean(checkPermissionsError) && <ErrorSummary error={checkPermissionsError} />}
|
||||
</div>
|
||||
</Cond>
|
||||
<Cond condition={Boolean(workspace) && workspaceState.matches("ready")}>
|
||||
<WorkspaceReadyPage workspaceState={workspaceState} workspaceSend={workspaceSend} />
|
||||
</Cond>
|
||||
<Cond>
|
||||
<FullScreenLoader />
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useActor, useSelector } from "@xstate/react"
|
||||
import { FeatureNames } from "api/types"
|
||||
import dayjs from "dayjs"
|
||||
import { useContext } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors"
|
||||
import { StateFrom } from "xstate"
|
||||
import { DeleteDialog } from "../../components/Dialogs/DeleteDialog/DeleteDialog"
|
||||
import { Workspace, WorkspaceErrors } from "../../components/Workspace/Workspace"
|
||||
import { pageTitle } from "../../util/page"
|
||||
import { getFaviconByStatus } from "../../util/workspace"
|
||||
import { XServiceContext } from "../../xServices/StateContext"
|
||||
import { WorkspaceEvent, workspaceMachine } from "../../xServices/workspace/workspaceXService"
|
||||
|
||||
interface WorkspaceReadyPageProps {
|
||||
workspaceState: StateFrom<typeof workspaceMachine>
|
||||
workspaceSend: (event: WorkspaceEvent) => void
|
||||
}
|
||||
|
||||
export const WorkspaceReadyPage = ({
|
||||
workspaceState,
|
||||
workspaceSend,
|
||||
}: WorkspaceReadyPageProps): JSX.Element => {
|
||||
const [bannerState, bannerSend] = useActor(workspaceState.children["scheduleBannerMachine"])
|
||||
const xServices = useContext(XServiceContext)
|
||||
const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility)
|
||||
const [buildInfoState] = useActor(xServices.buildInfoXService)
|
||||
const {
|
||||
workspace,
|
||||
refreshWorkspaceWarning,
|
||||
builds,
|
||||
getBuildsError,
|
||||
buildError,
|
||||
cancellationError,
|
||||
applicationsHost,
|
||||
permissions,
|
||||
} = workspaceState.context
|
||||
if (workspace === undefined) {
|
||||
throw Error("Workspace is undefined")
|
||||
}
|
||||
const canUpdateWorkspace = Boolean(permissions?.updateWorkspace)
|
||||
const { t } = useTranslation("workspacePage")
|
||||
const favicon = getFaviconByStatus(workspace.latest_build)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle(`${workspace.owner_name}/${workspace.name}`)}</title>
|
||||
<link rel="alternate icon" type="image/png" href={`/favicons/${favicon}.png`} />
|
||||
<link rel="icon" type="image/svg+xml" href={`/favicons/${favicon}.svg`} />
|
||||
</Helmet>
|
||||
|
||||
<Workspace
|
||||
bannerProps={{
|
||||
isLoading: bannerState.hasTag("loading"),
|
||||
onExtend: () => {
|
||||
bannerSend({
|
||||
type: "INCREASE_DEADLINE",
|
||||
hours: 4,
|
||||
})
|
||||
},
|
||||
}}
|
||||
scheduleProps={{
|
||||
onDeadlineMinus: () => {
|
||||
bannerSend({
|
||||
type: "DECREASE_DEADLINE",
|
||||
hours: 1,
|
||||
})
|
||||
},
|
||||
onDeadlinePlus: () => {
|
||||
bannerSend({
|
||||
type: "INCREASE_DEADLINE",
|
||||
hours: 1,
|
||||
})
|
||||
},
|
||||
deadlineMinusEnabled: () => !bannerState.matches("atMinDeadline"),
|
||||
deadlinePlusEnabled: () => !bannerState.matches("atMaxDeadline"),
|
||||
}}
|
||||
isUpdating={workspaceState.hasTag("updating")}
|
||||
workspace={workspace}
|
||||
handleStart={() => workspaceSend({ type: "START" })}
|
||||
handleStop={() => workspaceSend({ type: "STOP" })}
|
||||
handleDelete={() => workspaceSend({ type: "ASK_DELETE" })}
|
||||
handleUpdate={() => workspaceSend({ type: "UPDATE" })}
|
||||
handleCancel={() => workspaceSend({ type: "CANCEL" })}
|
||||
resources={workspace.latest_build.resources}
|
||||
builds={builds}
|
||||
canUpdateWorkspace={canUpdateWorkspace}
|
||||
hideSSHButton={featureVisibility[FeatureNames.BrowserOnly]}
|
||||
workspaceErrors={{
|
||||
[WorkspaceErrors.GET_RESOURCES_ERROR]: refreshWorkspaceWarning,
|
||||
[WorkspaceErrors.GET_BUILDS_ERROR]: getBuildsError,
|
||||
[WorkspaceErrors.BUILD_ERROR]: buildError,
|
||||
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
|
||||
}}
|
||||
buildInfo={buildInfoState.context.buildInfo}
|
||||
applicationsHost={applicationsHost}
|
||||
/>
|
||||
<DeleteDialog
|
||||
entity="workspace"
|
||||
name={workspace.name}
|
||||
info={t("deleteDialog.info", { timeAgo: dayjs(workspace.created_at).fromNow() })}
|
||||
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
|
||||
onCancel={() => workspaceSend({ type: "CANCEL_DELETE" })}
|
||||
onConfirm={() => {
|
||||
workspaceSend({ type: "DELETE" })
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
deadlineExtensionMax,
|
||||
deadlineExtensionMin,
|
||||
extractTimezone,
|
||||
maxDeadline,
|
||||
minDeadline,
|
||||
getMaxDeadline,
|
||||
getMinDeadline,
|
||||
stripTimezone,
|
||||
} from "./schedule"
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("maxDeadline", () => {
|
||||
}
|
||||
|
||||
// Then: deadlineMinusDisabled should be falsy
|
||||
const delta = maxDeadline(workspace, template).diff(now)
|
||||
const delta = getMaxDeadline(workspace, template).diff(now)
|
||||
expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds())
|
||||
})
|
||||
})
|
||||
@@ -68,7 +68,7 @@ describe("maxDeadline", () => {
|
||||
}
|
||||
|
||||
// Then: deadlineMinusDisabled should be falsy
|
||||
const delta = maxDeadline(workspace, template).diff(now)
|
||||
const delta = getMaxDeadline(workspace, template).diff(now)
|
||||
expect(delta).toBeLessThanOrEqual(deadlineExtensionMax.asMilliseconds())
|
||||
})
|
||||
})
|
||||
@@ -76,7 +76,7 @@ describe("maxDeadline", () => {
|
||||
|
||||
describe("minDeadline", () => {
|
||||
it("should never be less than 30 minutes", () => {
|
||||
const delta = minDeadline().diff(now)
|
||||
const delta = getMinDeadline().diff(now)
|
||||
expect(delta).toBeGreaterThanOrEqual(deadlineExtensionMin.asMilliseconds())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -113,16 +113,30 @@ export const autoStopDisplay = (workspace: Workspace): string => {
|
||||
export const deadlineExtensionMin = dayjs.duration(30, "minutes")
|
||||
export const deadlineExtensionMax = dayjs.duration(24, "hours")
|
||||
|
||||
export function maxDeadline(ws: Workspace, tpl: Template): dayjs.Dayjs {
|
||||
/**
|
||||
* Depends on the time the workspace was last updated, the template config,
|
||||
* and a global constant.
|
||||
* @param ws workspace
|
||||
* @param tpl template
|
||||
* @returns the latest datetime at which the workspace can be automatically shut down.
|
||||
*/
|
||||
export function getMaxDeadline(ws: Workspace | undefined, tpl: Template): dayjs.Dayjs {
|
||||
// note: we count runtime from updated_at as started_at counts from the start of
|
||||
// the workspace build process, which can take a while.
|
||||
if (ws === undefined) {
|
||||
throw Error("Cannot calculate max deadline because workspace is undefined")
|
||||
}
|
||||
const startedAt = dayjs(ws.latest_build.updated_at)
|
||||
const maxTemplateDeadline = startedAt.add(dayjs.duration(tpl.max_ttl_ms, "milliseconds"))
|
||||
const maxGlobalDeadline = startedAt.add(deadlineExtensionMax)
|
||||
return dayjs.min(maxTemplateDeadline, maxGlobalDeadline)
|
||||
}
|
||||
|
||||
export function minDeadline(): dayjs.Dayjs {
|
||||
/**
|
||||
* Depends on the current time and a global constant.
|
||||
* @returns the earliest datetime at which the workspace can be automatically shut down.
|
||||
*/
|
||||
export function getMinDeadline(): dayjs.Dayjs {
|
||||
return dayjs().add(deadlineExtensionMin)
|
||||
}
|
||||
|
||||
@@ -131,9 +145,12 @@ export function canExtendDeadline(
|
||||
workspace: Workspace,
|
||||
template: Template,
|
||||
): boolean {
|
||||
return deadline < maxDeadline(workspace, template)
|
||||
return deadline < getMaxDeadline(workspace, template)
|
||||
}
|
||||
|
||||
export function canReduceDeadline(deadline: dayjs.Dayjs): boolean {
|
||||
return deadline > minDeadline()
|
||||
return deadline > getMinDeadline()
|
||||
}
|
||||
|
||||
export const getDeadline = (workspace: Workspace): dayjs.Dayjs =>
|
||||
dayjs(workspace.latest_build.deadline).utc()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getErrorMessage } from "api/errors"
|
||||
import { workspaceScheduleBannerMachine } from "xServices/workspaceSchedule/workspaceScheduleBannerXService"
|
||||
import { assign, createMachine, send } from "xstate"
|
||||
import * as API from "../../api/api"
|
||||
import * as Types from "../../api/types"
|
||||
@@ -78,6 +79,8 @@ export type WorkspaceEvent =
|
||||
| { type: "CANCEL" }
|
||||
| { type: "REFRESH_TIMELINE"; checkRefresh?: boolean; data?: TypesGen.ServerSentEvent["data"] }
|
||||
| { type: "EVENT_SOURCE_ERROR"; error: Error | unknown }
|
||||
| { type: "INCREASE_DEADLINE"; hours: number }
|
||||
| { type: "DECREASE_DEADLINE"; hours: number }
|
||||
|
||||
export const checks = {
|
||||
readWorkspace: "readWorkspace",
|
||||
@@ -216,6 +219,7 @@ export const workspaceMachine = createMachine(
|
||||
},
|
||||
],
|
||||
},
|
||||
tags: "loading",
|
||||
},
|
||||
ready: {
|
||||
type: "parallel",
|
||||
@@ -440,6 +444,19 @@ export const workspaceMachine = createMachine(
|
||||
},
|
||||
},
|
||||
},
|
||||
schedule: {
|
||||
invoke: {
|
||||
id: "scheduleBannerMachine",
|
||||
src: workspaceScheduleBannerMachine,
|
||||
data: {
|
||||
workspace: (context: WorkspaceContext) => context.workspace,
|
||||
template: (context: WorkspaceContext) => context.template,
|
||||
},
|
||||
},
|
||||
on: {
|
||||
REFRESH_WORKSPACE: { actions: "sendWorkspaceToSchedule" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
error: {
|
||||
@@ -551,6 +568,13 @@ export const workspaceMachine = createMachine(
|
||||
const message = getErrorMessage(data, "Error getting the applications host.")
|
||||
displayError(message)
|
||||
},
|
||||
sendWorkspaceToSchedule: send(
|
||||
(context) => ({
|
||||
type: "REFRESH_WORKSPACE",
|
||||
workspace: context.workspace,
|
||||
}),
|
||||
{ to: "scheduleBannerMachine" },
|
||||
),
|
||||
},
|
||||
guards: {
|
||||
moreBuildsAvailable,
|
||||
|
||||
@@ -3,22 +3,49 @@
|
||||
* presented as an Alert/banner, for reactively updating a workspace schedule.
|
||||
*/
|
||||
import { getErrorMessage } from "api/errors"
|
||||
import { Template, Workspace } from "api/typesGenerated"
|
||||
import dayjs from "dayjs"
|
||||
import { createMachine } from "xstate"
|
||||
import minMax from "dayjs/plugin/minMax"
|
||||
import {
|
||||
canExtendDeadline,
|
||||
canReduceDeadline,
|
||||
getDeadline,
|
||||
getMaxDeadline,
|
||||
getMinDeadline,
|
||||
} from "util/schedule"
|
||||
import { ActorRefFrom, assign, createMachine } from "xstate"
|
||||
import * as API from "../../api/api"
|
||||
import { displayError, displaySuccess } from "../../components/GlobalSnackbar/utils"
|
||||
|
||||
dayjs.extend(minMax)
|
||||
|
||||
export const Language = {
|
||||
errorExtension: "Failed to update workspace shutdown time.",
|
||||
successExtension: "Updated workspace shutdown time.",
|
||||
}
|
||||
|
||||
export type WorkspaceScheduleBannerEvent = {
|
||||
type: "UPDATE_DEADLINE"
|
||||
workspaceId: string
|
||||
newDeadline: dayjs.Dayjs
|
||||
export interface WorkspaceScheduleBannerContext {
|
||||
workspace: Workspace
|
||||
template: Template
|
||||
deadline?: dayjs.Dayjs
|
||||
}
|
||||
|
||||
export type WorkspaceScheduleBannerEvent =
|
||||
| {
|
||||
type: "INCREASE_DEADLINE"
|
||||
hours: number
|
||||
}
|
||||
| {
|
||||
type: "DECREASE_DEADLINE"
|
||||
hours: number
|
||||
}
|
||||
| {
|
||||
type: "REFRESH_WORKSPACE"
|
||||
workspace: Workspace
|
||||
}
|
||||
|
||||
export type WorkspaceScheduleBannerMachineRef = ActorRefFrom<typeof workspaceScheduleBannerMachine>
|
||||
|
||||
export const workspaceScheduleBannerMachine = createMachine(
|
||||
{
|
||||
id: "workspaceScheduleBannerState",
|
||||
@@ -26,24 +53,75 @@ export const workspaceScheduleBannerMachine = createMachine(
|
||||
tsTypes: {} as import("./workspaceScheduleBannerXService.typegen").Typegen0,
|
||||
schema: {
|
||||
events: {} as WorkspaceScheduleBannerEvent,
|
||||
context: {} as WorkspaceScheduleBannerContext,
|
||||
},
|
||||
initial: "initialize",
|
||||
on: {
|
||||
REFRESH_WORKSPACE: { actions: "assignWorkspace" },
|
||||
},
|
||||
initial: "idle",
|
||||
states: {
|
||||
idle: {
|
||||
initialize: {
|
||||
always: [
|
||||
{ cond: "isAtMaxDeadline", target: "atMaxDeadline" },
|
||||
{ cond: "isAtMinDeadline", target: "atMinDeadline" },
|
||||
{ target: "midRange" },
|
||||
],
|
||||
},
|
||||
midRange: {
|
||||
on: {
|
||||
UPDATE_DEADLINE: "updatingDeadline",
|
||||
INCREASE_DEADLINE: "increasingDeadline",
|
||||
DECREASE_DEADLINE: "decreasingDeadline",
|
||||
},
|
||||
},
|
||||
updatingDeadline: {
|
||||
atMaxDeadline: {
|
||||
on: {
|
||||
DECREASE_DEADLINE: "decreasingDeadline",
|
||||
},
|
||||
},
|
||||
atMinDeadline: {
|
||||
on: {
|
||||
INCREASE_DEADLINE: "increasingDeadline",
|
||||
},
|
||||
},
|
||||
increasingDeadline: {
|
||||
invoke: {
|
||||
src: "updateDeadline",
|
||||
id: "updateDeadline",
|
||||
onDone: {
|
||||
target: "idle",
|
||||
actions: "displaySuccessMessage",
|
||||
},
|
||||
src: "increaseDeadline",
|
||||
id: "increaseDeadline",
|
||||
onDone: [
|
||||
{
|
||||
cond: "isAtMaxDeadline",
|
||||
target: "atMaxDeadline",
|
||||
actions: "displaySuccessMessage",
|
||||
},
|
||||
{
|
||||
target: "midRange",
|
||||
actions: "displaySuccessMessage",
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: "idle",
|
||||
target: "midRange",
|
||||
actions: "displayFailureMessage",
|
||||
},
|
||||
},
|
||||
tags: "loading",
|
||||
},
|
||||
decreasingDeadline: {
|
||||
invoke: {
|
||||
src: "decreaseDeadline",
|
||||
id: "decreaseDeadline",
|
||||
onDone: [
|
||||
{
|
||||
cond: "isAtMinDeadline",
|
||||
target: "atMinDeadline",
|
||||
actions: "displaySuccessMessage",
|
||||
},
|
||||
{
|
||||
target: "midRange",
|
||||
actions: "displaySuccessMessage",
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: "midRange",
|
||||
actions: "displayFailureMessage",
|
||||
},
|
||||
},
|
||||
@@ -52,6 +130,14 @@ export const workspaceScheduleBannerMachine = createMachine(
|
||||
},
|
||||
},
|
||||
{
|
||||
guards: {
|
||||
isAtMaxDeadline: (context) =>
|
||||
context.deadline
|
||||
? !canExtendDeadline(context.deadline, context.workspace, context.template)
|
||||
: false,
|
||||
isAtMinDeadline: (context) =>
|
||||
context.deadline ? !canReduceDeadline(context.deadline) : false,
|
||||
},
|
||||
actions: {
|
||||
// This error does not have a detail, so using the snackbar is okay
|
||||
displayFailureMessage: (_, event) => {
|
||||
@@ -60,11 +146,31 @@ export const workspaceScheduleBannerMachine = createMachine(
|
||||
displaySuccessMessage: () => {
|
||||
displaySuccess(Language.successExtension)
|
||||
},
|
||||
assignWorkspace: assign((_, event) => ({
|
||||
workspace: event.workspace,
|
||||
deadline: getDeadline(event.workspace),
|
||||
})),
|
||||
},
|
||||
|
||||
services: {
|
||||
updateDeadline: async (_, event) => {
|
||||
await API.putWorkspaceExtension(event.workspaceId, event.newDeadline)
|
||||
increaseDeadline: async (context, event) => {
|
||||
if (!context.deadline) {
|
||||
throw Error("Deadline is undefined.")
|
||||
}
|
||||
const proposedDeadline = context.deadline.add(event.hours, "hours")
|
||||
const newDeadline = dayjs.min(
|
||||
proposedDeadline,
|
||||
getMaxDeadline(context.workspace, context.template),
|
||||
)
|
||||
await API.putWorkspaceExtension(context.workspace.id, newDeadline)
|
||||
},
|
||||
decreaseDeadline: async (context, event) => {
|
||||
if (!context.deadline) {
|
||||
throw Error("Deadline is undefined.")
|
||||
}
|
||||
const proposedDeadline = context.deadline.subtract(event.hours, "hours")
|
||||
const newDeadline = dayjs.max(proposedDeadline, getMinDeadline())
|
||||
await API.putWorkspaceExtension(context.workspace.id, newDeadline)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user