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

Skip to content
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