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

Skip to content

feature: allow editing workspace deadline in UI #2721

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 30, 2022
5 changes: 3 additions & 2 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios, { AxiosRequestHeaders } from "axios"
import dayjs from "dayjs"
import * as Types from "./types"
import { WorkspaceBuildTransition } from "./types"
import * as TypesGen from "./typesGenerated"
Expand Down Expand Up @@ -339,7 +340,7 @@ export const getWorkspaceBuildLogs = async (

export const putWorkspaceExtension = async (
workspaceId: string,
extendWorkspaceRequest: TypesGen.PutExtendWorkspaceRequest,
newDeadline: dayjs.Dayjs,
): Promise<void> => {
await axios.put(`/api/v2/workspaces/${workspaceId}/extend`, extendWorkspaceRequest)
await axios.put(`/api/v2/workspaces/${workspaceId}/extend`, { deadline: newDeadline })
}
8 changes: 8 additions & 0 deletions site/src/components/Workspace/Workspace.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ Started.args = {
isLoading: false,
onExtend: action("extend"),
},
scheduleProps: {
onDeadlineMinus: () => {
// do nothing, this is just for storybook
},
onDeadlinePlus: () => {
// do nothing, this is just for storybook
},
},
workspace: Mocks.MockWorkspace,
handleStart: action("start"),
handleStop: action("stop"),
Expand Down
11 changes: 10 additions & 1 deletion site/src/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export interface WorkspaceProps {
isLoading?: boolean
onExtend: () => void
}
scheduleProps: {
onDeadlinePlus: () => void
onDeadlineMinus: () => void
}
handleStart: () => void
handleStop: () => void
handleDelete: () => void
Expand All @@ -36,6 +40,7 @@ export interface WorkspaceProps {
*/
export const Workspace: FC<WorkspaceProps> = ({
bannerProps,
scheduleProps,
handleStart,
handleStop,
handleDelete,
Expand Down Expand Up @@ -99,7 +104,11 @@ export const Workspace: FC<WorkspaceProps> = ({
</Stack>

<Stack direction="column" className={styles.secondColumnSpacer} spacing={3}>
<WorkspaceSchedule workspace={workspace} />
<WorkspaceSchedule
workspace={workspace}
onDeadlineMinus={scheduleProps.onDeadlineMinus}
onDeadlinePlus={scheduleProps.onDeadlinePlus}
/>
</Stack>
</Stack>
</Margins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dayjs.extend(utc)
// SEE: https:github.com/storybookjs/storybook/issues/12208#issuecomment-697044557
const ONE = 1
const SEVEN = 7
const THIRTY = 30

export default {
title: "components/WorkspaceSchedule",
Expand Down Expand Up @@ -47,6 +48,19 @@ NoTTL.args = {
},
}

export const ShutdownRealSoon = Template.bind({})
ShutdownRealSoon.args = {
workspace: {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
deadline: dayjs().add(THIRTY, "minute").utc().format(),
transition: "start",
},
ttl_ms: 2 * 60 * 60 * 1000, // 2 hours
},
}

export const ShutdownSoon = Template.bind({})
ShutdownSoon.args = {
workspace: {
Expand Down
120 changes: 120 additions & 0 deletions site/src/components/WorkspaceSchedule/WorkspaceSchedule.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import dayjs from "dayjs"
import utc from "dayjs/plugin/utc"
import * as TypesGen from "../../api/typesGenerated"
import * as Mocks from "../../testHelpers/entities"
import {
deadlineMinusDisabled,
deadlinePlusDisabled,
shouldDisplayPlusMinus,
} from "./WorkspaceSchedule"

dayjs.extend(utc)
const now = dayjs()

describe("WorkspaceSchedule", () => {
describe("shouldDisplayPlusMinus", () => {
it("should not display if the workspace is not running", () => {
// Given: a stopped workspace
const workspace: TypesGen.Workspace = Mocks.MockStoppedWorkspace

// Then: shouldDisplayPlusMinus should be false
expect(shouldDisplayPlusMinus(workspace)).toBeFalsy()
})

it("should display if the workspace is running", () => {
// Given: a stopped workspace
const workspace: TypesGen.Workspace = Mocks.MockWorkspace

// Then: shouldDisplayPlusMinus should be false
expect(shouldDisplayPlusMinus(workspace)).toBeTruthy()
})
})

describe("deadlineMinusDisabled", () => {
it("should be false if the deadline is more than 30 minutes in the future", () => {
// Given: a workspace with a deadline set to 31 minutes in the future
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
deadline: now.add(31, "minutes").utc().format(),
},
}

// Then: deadlineMinusDisabled should be falsy
expect(deadlineMinusDisabled(workspace, now)).toBeFalsy()
})

it("should be true if the deadline is 30 minutes or less in the future", () => {
// Given: a workspace with a deadline set to 30 minutes in the future
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
deadline: now.add(30, "minutes").utc().format(),
},
}

// Then: deadlineMinusDisabled should be truthy
expect(deadlineMinusDisabled(workspace, now)).toBeTruthy()
})

it("should be true if the deadline is in the past", () => {
// Given: a workspace with a deadline set to 1 minute in the past
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
deadline: now.add(-1, "minutes").utc().format(),
},
}

// Then: deadlineMinusDisabled should be truthy
expect(deadlineMinusDisabled(workspace, now)).toBeTruthy()
})
})

describe("deadlinePlusDisabled", () => {
it("should be false if the deadline is less than 24 hours in the future", () => {
// Given: a workspace with a deadline set to 23 hours in the future
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
deadline: now.add(23, "hours").utc().format(),
},
}

// Then: deadlinePlusDisabled should be falsy
expect(deadlinePlusDisabled(workspace, now)).toBeFalsy()
})

it("should be true if the deadline is 24 hours or more in the future", () => {
// Given: a workspace with a deadline set to 25 hours in the future
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
deadline: now.add(25, "hours").utc().format(),
},
}

// Then: deadlinePlusDisabled should be truthy
expect(deadlinePlusDisabled(workspace, now)).toBeTruthy()
})

it("should be false if the deadline is in the past", () => {
// Given: a workspace with a deadline set to 1 minute in the past
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
deadline: now.add(-1, "minute").utc().format(),
},
}

// Then: deadlinePlusDisabled should be falsy
expect(deadlinePlusDisabled(workspace, now)).toBeFalsy()
})
})
})
74 changes: 68 additions & 6 deletions site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import IconButton from "@material-ui/core/IconButton"
import Link from "@material-ui/core/Link"
import { makeStyles } from "@material-ui/core/styles"
import Tooltip from "@material-ui/core/Tooltip"
import Typography from "@material-ui/core/Typography"
import AddBoxIcon from "@material-ui/icons/AddBox"
import IndeterminateCheckBoxIcon from "@material-ui/icons/IndeterminateCheckBox"
import ScheduleIcon from "@material-ui/icons/Schedule"
import cronstrue from "cronstrue"
import dayjs from "dayjs"
Expand Down Expand Up @@ -66,6 +70,8 @@ export const Language = {
}
},
editScheduleLink: "Edit schedule",
editDeadlineMinus: "Subtract one hour",
editDeadlinePlus: "Add one hour",
scheduleHeader: (workspace: Workspace): string => {
const tz = workspace.autostart_schedule
? extractTimezone(workspace.autostart_schedule)
Expand All @@ -75,11 +81,61 @@ export const Language = {
}

export interface WorkspaceScheduleProps {
now?: dayjs.Dayjs
workspace: Workspace
onDeadlinePlus: () => void
onDeadlineMinus: () => void
}

export const WorkspaceSchedule: FC<WorkspaceScheduleProps> = ({ workspace }) => {
export const shouldDisplayPlusMinus = (workspace: Workspace): boolean => {
if (!isWorkspaceOn(workspace)) {
return false
}
const deadline = dayjs(workspace.latest_build.deadline).utc()
return deadline.year() > 1
}

export const deadlineMinusDisabled = (workspace: Workspace, now: dayjs.Dayjs): boolean => {
const delta = dayjs(workspace.latest_build.deadline).diff(now)
return delta <= 30 * 60 * 1000 // 30 minutes
}

export const deadlinePlusDisabled = (workspace: Workspace, now: dayjs.Dayjs): boolean => {
const delta = dayjs(workspace.latest_build.deadline).diff(now)
return delta >= 24 * 60 * 60 * 1000 // 24 hours
}

export const WorkspaceSchedule: FC<WorkspaceScheduleProps> = ({
now,
workspace,
onDeadlineMinus,
onDeadlinePlus,
}) => {
const styles = useStyles()
const editDeadlineButtons = shouldDisplayPlusMinus(workspace) ? (
<Stack direction="row" spacing={0}>
<IconButton
size="small"
disabled={deadlineMinusDisabled(workspace, now ?? dayjs())}
className={styles.editDeadline}
onClick={onDeadlineMinus}
>
<Tooltip title={Language.editDeadlineMinus}>
<IndeterminateCheckBoxIcon />
</Tooltip>
</IconButton>
<IconButton
size="small"
disabled={deadlinePlusDisabled(workspace, now ?? dayjs())}
className={styles.editDeadline}
onClick={onDeadlinePlus}
>
<Tooltip title={Language.editDeadlinePlus}>
<AddBoxIcon />
</Tooltip>
</IconButton>
</Stack>
) : null

return (
<div className={styles.schedule}>
Expand All @@ -96,9 +152,12 @@ export const WorkspaceSchedule: FC<WorkspaceScheduleProps> = ({ workspace }) =>
</div>
<div>
<span className={styles.scheduleLabel}>{Language.autoStopLabel}</span>
<span className={[styles.scheduleValue, "chromatic-ignore"].join(" ")}>
{Language.autoStopDisplay(workspace)}
</span>
<Stack direction="row">
<span className={[styles.scheduleValue, "chromatic-ignore"].join(" ")}>
{Language.autoStopDisplay(workspace)}
</span>
{editDeadlineButtons}
</Stack>
</div>
<div>
<Link
Expand Down Expand Up @@ -138,12 +197,15 @@ const useStyles = makeStyles((theme) => ({
color: theme.palette.text.secondary,
},
scheduleValue: {
fontSize: 16,
marginTop: theme.spacing(0.25),
fontSize: 14,
marginTop: theme.spacing(0.75),
display: "inline-block",
color: theme.palette.text.secondary,
},
scheduleAction: {
cursor: "pointer",
},
editDeadline: {
color: theme.palette.text.secondary,
},
}))
38 changes: 37 additions & 1 deletion site/src/pages/WorkspacePage/WorkspacePage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useMachine, useSelector } from "@xstate/react"
import dayjs from "dayjs"
import minMax from "dayjs/plugin/minMax"
import React, { useContext, useEffect } from "react"
import { Helmet } from "react-helmet"
import { useParams } from "react-router-dom"
Expand All @@ -13,6 +15,8 @@ import { XServiceContext } from "../../xServices/StateContext"
import { workspaceMachine } from "../../xServices/workspace/workspaceXService"
import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedule/workspaceScheduleBannerXService"

dayjs.extend(minMax)

export const WorkspacePage: React.FC = () => {
const { username: usernameQueryParam, workspace: workspaceQueryParam } = useParams()
const username = firstOrItem(usernameQueryParam, null)
Expand Down Expand Up @@ -56,7 +60,33 @@ export const WorkspacePage: React.FC = () => {
bannerProps={{
isLoading: bannerState.hasTag("loading"),
onExtend: () => {
bannerSend({ type: "EXTEND_DEADLINE_DEFAULT", workspaceId: workspace.id })
bannerSend({
type: "UPDATE_DEADLINE",
workspaceId: workspace.id,
newDeadline: dayjs(workspace.latest_build.deadline).utc().add(4, "hours"),
})
},
}}
scheduleProps={{
onDeadlineMinus: () => {
bannerSend({
type: "UPDATE_DEADLINE",
workspaceId: workspace.id,
newDeadline: boundedDeadline(
dayjs(workspace.latest_build.deadline).utc().add(-1, "hours"),
dayjs(),
),
})
},
onDeadlinePlus: () => {
bannerSend({
type: "UPDATE_DEADLINE",
workspaceId: workspace.id,
newDeadline: boundedDeadline(
dayjs(workspace.latest_build.deadline).utc().add(1, "hours"),
dayjs(),
),
})
},
}}
workspace={workspace}
Expand All @@ -81,3 +111,9 @@ export const WorkspacePage: React.FC = () => {
)
}
}

export const boundedDeadline = (newDeadline: dayjs.Dayjs, now: dayjs.Dayjs): dayjs.Dayjs => {
const minDeadline = now.add(30, "minutes")
const maxDeadline = now.add(24, "hours")
return dayjs.min(dayjs.max(minDeadline, newDeadline), maxDeadline)
}
Loading