mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
refactor: Move schedule to the header (#2775)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { FC } from "react"
|
||||
import React, { FC } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { BuildsTable } from "../BuildsTable/BuildsTable"
|
||||
@@ -9,8 +9,8 @@ import { Resources } from "../Resources/Resources"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
|
||||
import { WorkspaceDeletedBanner } from "../WorkspaceDeletedBanner/WorkspaceDeletedBanner"
|
||||
import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule"
|
||||
import { WorkspaceScheduleBanner } from "../WorkspaceScheduleBanner/WorkspaceScheduleBanner"
|
||||
import { WorkspaceScheduleButton } from "../WorkspaceScheduleButton/WorkspaceScheduleButton"
|
||||
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
|
||||
import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
|
||||
|
||||
@@ -58,16 +58,22 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
return (
|
||||
<Margins>
|
||||
<PageHeader
|
||||
className={styles.header}
|
||||
actions={
|
||||
<WorkspaceActions
|
||||
workspace={workspace}
|
||||
handleStart={handleStart}
|
||||
handleStop={handleStop}
|
||||
handleDelete={handleDelete}
|
||||
handleUpdate={handleUpdate}
|
||||
handleCancel={handleCancel}
|
||||
/>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<WorkspaceScheduleButton
|
||||
workspace={workspace}
|
||||
onDeadlineMinus={scheduleProps.onDeadlineMinus}
|
||||
onDeadlinePlus={scheduleProps.onDeadlinePlus}
|
||||
/>
|
||||
<WorkspaceActions
|
||||
workspace={workspace}
|
||||
handleStart={handleStart}
|
||||
handleStop={handleStop}
|
||||
handleDelete={handleDelete}
|
||||
handleUpdate={handleUpdate}
|
||||
handleCancel={handleCancel}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<PageHeaderTitle>{workspace.name}</PageHeaderTitle>
|
||||
@@ -102,14 +108,6 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
<BuildsTable builds={builds} className={styles.timelineTable} />
|
||||
</WorkspaceSection>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="column" className={styles.secondColumnSpacer} spacing={3}>
|
||||
<WorkspaceSchedule
|
||||
workspace={workspace}
|
||||
onDeadlineMinus={scheduleProps.onDeadlineMinus}
|
||||
onDeadlinePlus={scheduleProps.onDeadlinePlus}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Margins>
|
||||
)
|
||||
@@ -125,10 +123,6 @@ export const useStyles = makeStyles((theme) => {
|
||||
secondColumnSpacer: {
|
||||
flex: `0 0 ${spacerWidth}px`,
|
||||
},
|
||||
header: {
|
||||
// 100% - (the size of sidebar + the space between both )
|
||||
maxWidth: `calc(100% - (${spacerWidth}px + ${theme.spacing(3)}px))`,
|
||||
},
|
||||
layout: {
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
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"
|
||||
import advancedFormat from "dayjs/plugin/advancedFormat"
|
||||
import duration from "dayjs/plugin/duration"
|
||||
@@ -17,8 +10,12 @@ import { FC } from "react"
|
||||
import { Link as RouterLink } from "react-router-dom"
|
||||
import { Workspace } from "../../api/typesGenerated"
|
||||
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
||||
import { extractTimezone, stripTimezone } from "../../util/schedule"
|
||||
import { isWorkspaceOn } from "../../util/workspace"
|
||||
import {
|
||||
autoStartDisplay,
|
||||
autoStopDisplay,
|
||||
extractTimezone,
|
||||
Language as ScheduleLanguage,
|
||||
} from "../../util/schedule"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
|
||||
// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
|
||||
@@ -30,133 +27,39 @@ dayjs.extend(relativeTime)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
export const Language = {
|
||||
autoStartDisplay: (schedule: string | undefined): string => {
|
||||
if (schedule) {
|
||||
return cronstrue.toString(stripTimezone(schedule), { throwExceptionOnParseError: false })
|
||||
} else {
|
||||
return "Manual"
|
||||
}
|
||||
},
|
||||
autoStartLabel: "START",
|
||||
autoStopLabel: "SHUTDOWN",
|
||||
autoStopDisplay: (workspace: Workspace): string => {
|
||||
const deadline = dayjs(workspace.latest_build.deadline).utc()
|
||||
// a manual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
|
||||
// SEE: #1834
|
||||
const hasDeadline = deadline.year() > 1
|
||||
const ttl = workspace.ttl_ms
|
||||
|
||||
if (isWorkspaceOn(workspace) && hasDeadline) {
|
||||
// Workspace is on --> derive from latest_build.deadline. Note that the
|
||||
// user may modify their workspace object (ttl) while the workspace is
|
||||
// running and depending on system semantics, the deadline may still
|
||||
// represent the previously defined ttl. Thus, we always derive from the
|
||||
// deadline as the source of truth.
|
||||
const now = dayjs().utc()
|
||||
if (now.isAfter(deadline)) {
|
||||
return "Workspace is shutting down"
|
||||
} else {
|
||||
return deadline.tz(dayjs.tz.guess()).format("MMM D, YYYY h:mm A")
|
||||
}
|
||||
} else if (!ttl || ttl < 1) {
|
||||
// If the workspace is not on, and the ttl is 0 or undefined, then the
|
||||
// workspace is set to manually shutdown.
|
||||
return "Manual"
|
||||
} else {
|
||||
// The workspace has a ttl set, but is either in an unknown state or is
|
||||
// not running. Therefore, we derive from workspace.ttl.
|
||||
const duration = dayjs.duration(ttl, "milliseconds")
|
||||
return `${duration.humanize()} after start`
|
||||
}
|
||||
},
|
||||
editScheduleLink: "Edit schedule",
|
||||
editDeadlineMinus: "Subtract one hour",
|
||||
editDeadlinePlus: "Add one hour",
|
||||
scheduleHeader: (workspace: Workspace): string => {
|
||||
const tz = workspace.autostart_schedule
|
||||
? extractTimezone(workspace.autostart_schedule)
|
||||
: dayjs.tz.guess()
|
||||
return `Schedule (${tz})`
|
||||
},
|
||||
timezoneLabel: "Timezone",
|
||||
}
|
||||
|
||||
export interface WorkspaceScheduleProps {
|
||||
now?: dayjs.Dayjs
|
||||
workspace: Workspace
|
||||
onDeadlinePlus: () => void
|
||||
onDeadlineMinus: () => void
|
||||
}
|
||||
|
||||
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,
|
||||
}) => {
|
||||
export const WorkspaceSchedule: FC<WorkspaceScheduleProps> = ({ workspace }) => {
|
||||
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
|
||||
const timezone = workspace.autostart_schedule
|
||||
? extractTimezone(workspace.autostart_schedule)
|
||||
: dayjs.tz.guess()
|
||||
|
||||
return (
|
||||
<div className={styles.schedule}>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="body1" className={styles.title}>
|
||||
<ScheduleIcon className={styles.scheduleIcon} />
|
||||
{Language.scheduleHeader(workspace)}
|
||||
</Typography>
|
||||
<div>
|
||||
<span className={styles.scheduleLabel}>{Language.autoStartLabel}</span>
|
||||
<span className={styles.scheduleLabel}>{Language.timezoneLabel}</span>
|
||||
<span className={styles.scheduleValue}>{timezone}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.scheduleLabel}>{ScheduleLanguage.autoStartLabel}</span>
|
||||
<span className={[styles.scheduleValue, "chromatic-ignore"].join(" ")}>
|
||||
{Language.autoStartDisplay(workspace.autostart_schedule)}
|
||||
{autoStartDisplay(workspace.autostart_schedule)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.scheduleLabel}>{Language.autoStopLabel}</span>
|
||||
<span className={styles.scheduleLabel}>{ScheduleLanguage.autoStopLabel}</span>
|
||||
<Stack direction="row">
|
||||
<span className={[styles.scheduleValue, "chromatic-ignore"].join(" ")}>
|
||||
{Language.autoStopDisplay(workspace)}
|
||||
{autoStopDisplay(workspace)}
|
||||
</span>
|
||||
{editDeadlineButtons}
|
||||
</Stack>
|
||||
</div>
|
||||
<div>
|
||||
@@ -177,18 +80,6 @@ const useStyles = makeStyles((theme) => ({
|
||||
schedule: {
|
||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||
},
|
||||
title: {
|
||||
fontWeight: 600,
|
||||
|
||||
fontFamily: "inherit",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
scheduleIcon: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
scheduleLabel: {
|
||||
fontSize: 12,
|
||||
textTransform: "uppercase",
|
||||
@@ -198,14 +89,11 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
scheduleValue: {
|
||||
fontSize: 14,
|
||||
marginTop: theme.spacing(0.75),
|
||||
marginTop: theme.spacing(0.5),
|
||||
display: "inline-block",
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
scheduleAction: {
|
||||
cursor: "pointer",
|
||||
},
|
||||
editDeadline: {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import dayjs from "dayjs"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import * as Mocks from "../../testHelpers/entities"
|
||||
import { WorkspaceScheduleButton, WorkspaceScheduleButtonProps } from "./WorkspaceScheduleButton"
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
// REMARK: There's a known problem with storybook and using date libraries that
|
||||
// call string.toLowerCase
|
||||
// SEE: https:github.com/storybookjs/storybook/issues/12208#issuecomment-697044557
|
||||
const ONE = 1
|
||||
const SEVEN = 7
|
||||
const THIRTY = 30
|
||||
|
||||
export default {
|
||||
title: "components/WorkspaceScheduleButton",
|
||||
component: WorkspaceScheduleButton,
|
||||
}
|
||||
|
||||
const Template: Story<WorkspaceScheduleButtonProps> = (args) => (
|
||||
<WorkspaceScheduleButton {...args} />
|
||||
)
|
||||
|
||||
export const NoScheduleNoTTL = Template.bind({})
|
||||
NoScheduleNoTTL.args = {
|
||||
workspace: {
|
||||
...Mocks.MockWorkspace,
|
||||
|
||||
latest_build: {
|
||||
...Mocks.MockWorkspaceBuild,
|
||||
transition: "stop",
|
||||
},
|
||||
autostart_schedule: undefined,
|
||||
ttl_ms: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
export const NoTTL = Template.bind({})
|
||||
NoTTL.args = {
|
||||
workspace: {
|
||||
...Mocks.MockWorkspace,
|
||||
latest_build: {
|
||||
...Mocks.MockWorkspaceBuild,
|
||||
// a mannual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
|
||||
// SEE: #1834
|
||||
deadline: "0001-01-01T00:00:00Z",
|
||||
},
|
||||
ttl_ms: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
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: {
|
||||
...Mocks.MockWorkspace,
|
||||
latest_build: {
|
||||
...Mocks.MockWorkspaceBuild,
|
||||
deadline: dayjs().add(ONE, "hour").utc().format(),
|
||||
transition: "start",
|
||||
},
|
||||
ttl_ms: 2 * 60 * 60 * 1000, // 2 hours
|
||||
},
|
||||
}
|
||||
|
||||
export const ShutdownLong = Template.bind({})
|
||||
ShutdownLong.args = {
|
||||
workspace: {
|
||||
...Mocks.MockWorkspace,
|
||||
|
||||
latest_build: {
|
||||
...Mocks.MockWorkspaceBuild,
|
||||
deadline: dayjs().add(SEVEN, "days").utc().format(),
|
||||
transition: "start",
|
||||
},
|
||||
ttl_ms: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||
},
|
||||
}
|
||||
|
||||
export const WorkspaceOffShort = Template.bind({})
|
||||
WorkspaceOffShort.args = {
|
||||
workspace: {
|
||||
...Mocks.MockWorkspace,
|
||||
|
||||
latest_build: {
|
||||
...Mocks.MockWorkspaceBuild,
|
||||
transition: "stop",
|
||||
},
|
||||
ttl_ms: 2 * 60 * 60 * 1000, // 2 hours
|
||||
},
|
||||
}
|
||||
|
||||
export const WorkspaceOffLong = Template.bind({})
|
||||
WorkspaceOffLong.args = {
|
||||
workspace: {
|
||||
...Mocks.MockWorkspace,
|
||||
|
||||
latest_build: {
|
||||
...Mocks.MockWorkspaceBuild,
|
||||
transition: "stop",
|
||||
},
|
||||
ttl_ms: 2 * 365 * 24 * 60 * 60 * 1000, // 2 years
|
||||
},
|
||||
}
|
||||
+2
-2
@@ -6,12 +6,12 @@ import {
|
||||
deadlineMinusDisabled,
|
||||
deadlinePlusDisabled,
|
||||
shouldDisplayPlusMinus,
|
||||
} from "./WorkspaceSchedule"
|
||||
} from "./WorkspaceScheduleButton"
|
||||
|
||||
dayjs.extend(utc)
|
||||
const now = dayjs()
|
||||
|
||||
describe("WorkspaceSchedule", () => {
|
||||
describe("WorkspaceScheduleButton", () => {
|
||||
describe("shouldDisplayPlusMinus", () => {
|
||||
it("should not display if the workspace is not running", () => {
|
||||
// Given: a stopped workspace
|
||||
@@ -0,0 +1,159 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import IconButton from "@material-ui/core/IconButton"
|
||||
import Popover from "@material-ui/core/Popover"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import Tooltip from "@material-ui/core/Tooltip"
|
||||
import AddIcon from "@material-ui/icons/Add"
|
||||
import RemoveIcon from "@material-ui/icons/Remove"
|
||||
import ScheduleIcon from "@material-ui/icons/Schedule"
|
||||
import dayjs from "dayjs"
|
||||
import advancedFormat from "dayjs/plugin/advancedFormat"
|
||||
import duration from "dayjs/plugin/duration"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import timezone from "dayjs/plugin/timezone"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import { useRef, useState } from "react"
|
||||
import { Workspace } from "../../api/typesGenerated"
|
||||
import { isWorkspaceOn } from "../../util/workspace"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule"
|
||||
import { WorkspaceScheduleLabel } from "./WorkspaceScheduleLabel"
|
||||
|
||||
// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
|
||||
// sorted alphabetically.
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(advancedFormat)
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
export const Language = {
|
||||
editDeadlineMinus: "Subtract one hour",
|
||||
editDeadlinePlus: "Add one hour",
|
||||
}
|
||||
|
||||
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 interface WorkspaceScheduleButtonProps {
|
||||
workspace: Workspace
|
||||
onDeadlinePlus: () => void
|
||||
onDeadlineMinus: () => void
|
||||
}
|
||||
|
||||
export const WorkspaceScheduleButton: React.FC<WorkspaceScheduleButtonProps> = ({
|
||||
workspace,
|
||||
onDeadlinePlus,
|
||||
onDeadlineMinus,
|
||||
}) => {
|
||||
const anchorRef = useRef<HTMLButtonElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const id = isOpen ? "schedule-popover" : undefined
|
||||
const styles = useStyles()
|
||||
|
||||
const onClose = () => {
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.label}>
|
||||
<WorkspaceScheduleLabel workspace={workspace} />
|
||||
{shouldDisplayPlusMinus(workspace) && (
|
||||
<Stack direction="row" spacing={0}>
|
||||
<IconButton
|
||||
className={styles.iconButton}
|
||||
size="small"
|
||||
disabled={deadlineMinusDisabled(workspace, dayjs())}
|
||||
onClick={onDeadlineMinus}
|
||||
>
|
||||
<Tooltip title={Language.editDeadlineMinus}>
|
||||
<RemoveIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
className={styles.iconButton}
|
||||
size="small"
|
||||
disabled={deadlinePlusDisabled(workspace, dayjs())}
|
||||
onClick={onDeadlinePlus}
|
||||
>
|
||||
<Tooltip title={Language.editDeadlinePlus}>
|
||||
<AddIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
ref={anchorRef}
|
||||
startIcon={<ScheduleIcon />}
|
||||
onClick={() => {
|
||||
setIsOpen(true)
|
||||
}}
|
||||
>
|
||||
Schedule
|
||||
</Button>
|
||||
<Popover
|
||||
classes={{ paper: styles.popoverPaper }}
|
||||
id={id}
|
||||
open={isOpen}
|
||||
anchorEl={anchorRef.current}
|
||||
onClose={onClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<WorkspaceSchedule workspace={workspace} />
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
wrapper: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
|
||||
label: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRight: 0,
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "0 8px 0 16px",
|
||||
color: theme.palette.text.secondary,
|
||||
// It is from the button props
|
||||
minHeight: 42,
|
||||
},
|
||||
|
||||
iconButton: {
|
||||
borderRadius: 2,
|
||||
},
|
||||
|
||||
popoverPaper: {
|
||||
padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing(3)}px`,
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,41 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { Workspace } from "../../api/typesGenerated"
|
||||
import { combineClasses } from "../../util/combineClasses"
|
||||
import { autoStartDisplay, autoStopDisplay, isShuttingDown, Language } from "../../util/schedule"
|
||||
import { isWorkspaceOn } from "../../util/workspace"
|
||||
|
||||
export const WorkspaceScheduleLabel: React.FC<{ workspace: Workspace }> = ({ workspace }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
if (isWorkspaceOn(workspace)) {
|
||||
const stopLabel = autoStopDisplay(workspace)
|
||||
const shouldDisplayStrongLabel = !isShuttingDown(workspace)
|
||||
|
||||
// If it is shutting down, we don't need to display the auto stop label
|
||||
return (
|
||||
<span className={combineClasses([styles.labelText, "chromatic-ignore"])}>
|
||||
{shouldDisplayStrongLabel && (
|
||||
<strong className={styles.labelStrong}>{Language.autoStopLabel}</strong>
|
||||
)}
|
||||
{stopLabel}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={combineClasses([styles.labelText, "chromatic-ignore"])}>
|
||||
<strong className={styles.labelStrong}>{Language.autoStartLabel}</strong>
|
||||
{autoStartDisplay(workspace.autostart_schedule)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
labelText: {
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
|
||||
labelStrong: {
|
||||
marginRight: theme.spacing(0.5),
|
||||
},
|
||||
}))
|
||||
@@ -44,6 +44,14 @@ export const getOverrides = (palette: PaletteOptions) => {
|
||||
marginRight: 12,
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
sizeSmall: {
|
||||
"& .MuiSvgIcon-root": {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableHead: {
|
||||
root: {
|
||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
import cronstrue from "cronstrue"
|
||||
import dayjs, { Dayjs } from "dayjs"
|
||||
import advancedFormat from "dayjs/plugin/advancedFormat"
|
||||
import duration from "dayjs/plugin/duration"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import timezone from "dayjs/plugin/timezone"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import { Workspace } from "../api/typesGenerated"
|
||||
import { isWorkspaceOn } from "./workspace"
|
||||
|
||||
// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
|
||||
// sorted alphabetically.
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(advancedFormat)
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
/**
|
||||
* @fileoverview Client-side counterpart of the coderd/autostart/schedule Go
|
||||
* package. This package is a variation on crontab that uses minute, hour and
|
||||
@@ -30,3 +48,65 @@ export const extractTimezone = (raw: string, defaultTZ = DEFAULT_TIMEZONE): stri
|
||||
return defaultTZ
|
||||
}
|
||||
}
|
||||
|
||||
/** Language used in the schedule components */
|
||||
export const Language = {
|
||||
manual: "Manual",
|
||||
workspaceShuttingDownLabel: "Workspace is shutting down",
|
||||
afterStart: "after start",
|
||||
autoStartLabel: "Starts at",
|
||||
autoStopLabel: "Stops at",
|
||||
}
|
||||
|
||||
export const autoStartDisplay = (schedule: string | undefined): string => {
|
||||
if (schedule) {
|
||||
return (
|
||||
cronstrue
|
||||
.toString(stripTimezone(schedule), { throwExceptionOnParseError: false })
|
||||
// We don't want to keep the At because it is on the label
|
||||
.replace("At", "")
|
||||
)
|
||||
} else {
|
||||
return Language.manual
|
||||
}
|
||||
}
|
||||
|
||||
export const isShuttingDown = (workspace: Workspace, deadline?: Dayjs): boolean => {
|
||||
if (!deadline) {
|
||||
deadline = dayjs(workspace.latest_build.deadline).utc()
|
||||
}
|
||||
const hasDeadline = deadline.year() > 1
|
||||
const now = dayjs().utc()
|
||||
return isWorkspaceOn(workspace) && hasDeadline && now.isAfter(deadline)
|
||||
}
|
||||
|
||||
export const autoStopDisplay = (workspace: Workspace): string => {
|
||||
const deadline = dayjs(workspace.latest_build.deadline).utc()
|
||||
// a manual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
|
||||
// SEE: #1834
|
||||
const hasDeadline = deadline.year() > 1
|
||||
const ttl = workspace.ttl_ms
|
||||
|
||||
if (isWorkspaceOn(workspace) && hasDeadline) {
|
||||
// Workspace is on --> derive from latest_build.deadline. Note that the
|
||||
// user may modify their workspace object (ttl) while the workspace is
|
||||
// running and depending on system semantics, the deadline may still
|
||||
// represent the previously defined ttl. Thus, we always derive from the
|
||||
// deadline as the source of truth.
|
||||
|
||||
if (isShuttingDown(workspace, deadline)) {
|
||||
return Language.workspaceShuttingDownLabel
|
||||
} else {
|
||||
return deadline.tz(dayjs.tz.guess()).format("MMM D, YYYY h:mm A")
|
||||
}
|
||||
} else if (!ttl || ttl < 1) {
|
||||
// If the workspace is not on, and the ttl is 0 or undefined, then the
|
||||
// workspace is set to manually shutdown.
|
||||
return Language.manual
|
||||
} else {
|
||||
// The workspace has a ttl set, but is either in an unknown state or is
|
||||
// not running. Therefore, we derive from workspace.ttl.
|
||||
const duration = dayjs.duration(ttl, "milliseconds")
|
||||
return `${duration.humanize()} ${Language.afterStart}`
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user