refactor(site): Refactor the schedule controls in the workspace page (#7083)

This commit is contained in:
Bruno Quaresma
2023-04-11 13:33:24 -03:00
committed by GitHub
parent b92e7d4fab
commit b86ed11bc0
8 changed files with 321 additions and 648 deletions
+1
View File
@@ -30,6 +30,7 @@ const useStyles = makeStyles((theme) => ({
alignItems: "center",
color: theme.palette.text.secondary,
margin: "0px",
flexWrap: "wrap",
[theme.breakpoints.down("sm")]: {
display: "block",
+5 -10
View File
@@ -25,7 +25,6 @@ import { Resources } from "../Resources/Resources"
import { Stack } from "../Stack/Stack"
import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
import { WorkspaceDeletedBanner } from "../WorkspaceDeletedBanner/WorkspaceDeletedBanner"
import { WorkspaceScheduleButton } from "../WorkspaceScheduleButton/WorkspaceScheduleButton"
import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
export enum WorkspaceErrors {
@@ -33,7 +32,6 @@ export enum WorkspaceErrors {
BUILD_ERROR = "buildError",
CANCELLATION_ERROR = "cancellationError",
}
export interface WorkspaceProps {
scheduleProps: {
onDeadlinePlus: (hours: number) => void
@@ -129,14 +127,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
<PageHeader
actions={
<Stack direction="row" spacing={1} className={styles.actions}>
<WorkspaceScheduleButton
workspace={workspace}
onDeadlineMinus={scheduleProps.onDeadlineMinus}
onDeadlinePlus={scheduleProps.onDeadlinePlus}
maxDeadlineDecrease={scheduleProps.maxDeadlineDecrease}
maxDeadlineIncrease={scheduleProps.maxDeadlineIncrease}
canUpdateWorkspace={canUpdateWorkspace}
/>
<WorkspaceActions
workspaceStatus={workspace.latest_build.status}
isOutdated={workspace.outdated}
@@ -194,6 +184,11 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
workspace={workspace}
quota_budget={quota_budget}
handleUpdate={handleUpdate}
canUpdateWorkspace={canUpdateWorkspace}
maxDeadlineDecrease={scheduleProps.maxDeadlineDecrease}
maxDeadlineIncrease={scheduleProps.maxDeadlineIncrease}
onDeadlineMinus={scheduleProps.onDeadlineMinus}
onDeadlinePlus={scheduleProps.onDeadlinePlus}
/>
{failedBuildLogs && (
@@ -1,54 +0,0 @@
import Button from "@material-ui/core/Button"
import { makeStyles } from "@material-ui/core/styles"
import TextField from "@material-ui/core/TextField"
import { Stack } from "components/Stack/Stack"
import { useState } from "react"
import { useTranslation } from "react-i18next"
interface EditHoursProps {
handleSubmit: (hours: number) => void
max: number
}
export const EditHours = ({
handleSubmit,
max,
}: EditHoursProps): JSX.Element => {
const { t } = useTranslation("workspacePage")
const [hours, setHours] = useState(1)
const styles = useStyles()
return (
// hours is NaN when user deletes the value, so treat it as 0
<form onSubmit={() => handleSubmit(Number.isNaN(hours) ? 0 : hours)}>
<Stack direction="row" alignItems="baseline" spacing={1}>
<TextField
className={styles.inputField}
inputProps={{ min: 0, max, step: 1 }}
label={t("workspaceScheduleButton.hours")}
value={hours.toString()}
onChange={(e) => setHours(parseInt(e.target.value))}
type="number"
/>
<Button className={styles.button} type="submit" color="primary">
{t("workspaceScheduleButton.submitDeadline")}
</Button>
</Stack>
</form>
)
}
const useStyles = makeStyles(() => ({
inputField: {
width: "70px",
"& .MuiOutlinedInput-root": {
height: "30px",
},
},
button: {
"&.MuiButton-root": {
minHeight: "30px",
height: "30px",
},
},
}))
@@ -1,121 +0,0 @@
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)
export default {
title: "components/WorkspaceScheduleButton",
component: WorkspaceScheduleButton,
argTypes: {
canUpdateWorkspace: {
defaultValue: true,
},
deadlineMinusEnabled: {
defaultValue: (): boolean => false,
},
deadlinePlusEnabled: {
defaultValue: (): boolean => false,
},
},
}
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,
deadline: undefined,
},
ttl_ms: undefined,
},
}
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 = {
deadlinePlusEnabled: () => true,
workspace: {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
transition: "stop",
},
ttl_ms: 2 * 365 * 24 * 60 * 60 * 1000, // 2 years
},
}
export const WorkspaceOn = Template.bind({})
WorkspaceOn.args = {
deadlineMinusEnabled: () => true,
deadlinePlusEnabled: () => true,
workspace: {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
transition: "start",
deadline: "2022-05-17T23:59:00.00Z",
},
ttl_ms: 2 * 365 * 24 * 60 * 60 * 1000, // 2 years
},
}
export const CannotEdit = Template.bind({})
CannotEdit.args = {
workspace: {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
transition: "stop",
},
ttl_ms: 2 * 60 * 60 * 1000, // 2 hours
},
canUpdateWorkspace: false,
}
export const SmallViewport = Template.bind({})
SmallViewport.args = {
...WorkspaceOffShort.args,
}
SmallViewport.parameters = {
chromatic: { viewports: [320] },
}
@@ -1,88 +0,0 @@
import { screen } from "@testing-library/react"
import dayjs from "dayjs"
import utc from "dayjs/plugin/utc"
import { render } from "testHelpers/renderHelpers"
import * as TypesGen from "../../api/typesGenerated"
import * as Mocks from "../../testHelpers/entities"
import {
canEditDeadline,
WorkspaceScheduleButton,
} from "./WorkspaceScheduleButton"
dayjs.extend(utc)
describe("WorkspaceScheduleButton", () => {
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(canEditDeadline(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(canEditDeadline(workspace)).toBeTruthy()
})
})
describe("enabling plus and minus buttons", () => {
it("should enable plus and minus buttons when deadline can be changed in either direction", async () => {
render(
<WorkspaceScheduleButton
workspace={Mocks.MockWorkspace}
onDeadlineMinus={jest.fn()}
onDeadlinePlus={jest.fn()}
maxDeadlineDecrease={4}
maxDeadlineIncrease={4}
canUpdateWorkspace
/>,
)
const plusButton = await screen.findByLabelText("Add hours to deadline")
const minusButton = await screen.findByLabelText(
"Subtract hours from deadline",
)
expect(plusButton).toBeEnabled()
expect(minusButton).toBeEnabled()
})
it("should disable plus button when deadline can't be extended", async () => {
render(
<WorkspaceScheduleButton
workspace={Mocks.MockWorkspace}
onDeadlineMinus={jest.fn()}
onDeadlinePlus={jest.fn()}
maxDeadlineDecrease={4}
maxDeadlineIncrease={0}
canUpdateWorkspace
/>,
)
const plusButton = await screen.findByLabelText("Add hours to deadline")
const minusButton = await screen.findByLabelText(
"Subtract hours from deadline",
)
expect(plusButton).toBeDisabled()
expect(minusButton).toBeEnabled()
})
it("should disable minus button when deadline can't be reduced", async () => {
render(
<WorkspaceScheduleButton
workspace={Mocks.MockWorkspace}
onDeadlineMinus={jest.fn()}
onDeadlinePlus={jest.fn()}
maxDeadlineDecrease={0}
maxDeadlineIncrease={4}
canUpdateWorkspace
/>,
)
const plusButton = await screen.findByLabelText("Add hours to deadline")
const minusButton = await screen.findByLabelText(
"Subtract hours from deadline",
)
expect(plusButton).toBeEnabled()
expect(minusButton).toBeDisabled()
})
})
})
@@ -1,270 +0,0 @@
import Button from "@material-ui/core/Button"
import IconButton from "@material-ui/core/IconButton"
import Popover from "@material-ui/core/Popover"
import { makeStyles, Theme } 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 { Maybe } from "components/Conditionals/Maybe"
import { Stack } from "components/Stack/Stack"
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 { useTranslation } from "react-i18next"
import { colors } from "theme/colors"
import { Workspace } from "../../api/typesGenerated"
import { isWorkspaceOn } from "../../util/workspace"
import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule"
import { EditHours } from "./EditHours"
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 canEditDeadline = (workspace: Workspace): boolean => {
return isWorkspaceOn(workspace) && Boolean(workspace.latest_build.deadline)
}
export const shouldDisplayScheduleLabel = (workspace: Workspace): boolean => {
if (canEditDeadline(workspace)) {
return true
}
if (isWorkspaceOn(workspace)) {
return false
}
return Boolean(workspace.autostart_schedule)
}
export interface WorkspaceScheduleButtonProps {
workspace: Workspace
onDeadlinePlus: (hours: number) => void
onDeadlineMinus: (hours: number) => void
maxDeadlineIncrease: number
maxDeadlineDecrease: number
canUpdateWorkspace: boolean
}
export type EditMode = "add" | "subtract" | "off"
export const WorkspaceScheduleButton: React.FC<
WorkspaceScheduleButtonProps
> = ({
workspace,
onDeadlinePlus,
onDeadlineMinus,
maxDeadlineDecrease,
maxDeadlineIncrease,
canUpdateWorkspace,
}) => {
const { t } = useTranslation("workspacePage")
const anchorRef = useRef<HTMLButtonElement>(null)
const [isOpen, setIsOpen] = useState(false)
const [editMode, setEditMode] = useState<EditMode>("off")
const id = isOpen ? "schedule-popover" : undefined
const styles = useStyles({ editMode })
const deadlinePlusEnabled = maxDeadlineIncrease >= 1
const deadlineMinusEnabled = maxDeadlineDecrease >= 1
const onClose = () => {
setIsOpen(false)
}
const handleSubmitHours = (hours: number) => {
if (hours !== 0) {
if (editMode === "add") {
onDeadlinePlus(hours)
}
if (editMode === "subtract") {
onDeadlineMinus(hours)
}
}
setEditMode("off")
}
return (
<span className={styles.wrapper}>
<Maybe condition={shouldDisplayScheduleLabel(workspace)}>
<Stack
className={styles.label}
spacing={1}
direction="row"
alignItems="center"
>
<Stack spacing={1} direction="row" alignItems="center">
<WorkspaceScheduleLabel workspace={workspace} />
<Maybe condition={canUpdateWorkspace && canEditDeadline(workspace)}>
<span className={styles.actions}>
<IconButton
className={styles.subtractButton}
aria-label="Subtract hours from deadline"
size="small"
disabled={!deadlineMinusEnabled}
onClick={() => {
setEditMode("subtract")
}}
>
<Tooltip
title={t("workspaceScheduleButton.editDeadlineMinus")}
>
<RemoveIcon />
</Tooltip>
</IconButton>
<IconButton
className={styles.addButton}
aria-label="Add hours to deadline"
size="small"
disabled={!deadlinePlusEnabled}
onClick={() => {
setEditMode("add")
}}
>
<Tooltip
title={t("workspaceScheduleButton.editDeadlinePlus")}
>
<AddIcon />
</Tooltip>
</IconButton>
</span>
</Maybe>
</Stack>
<Maybe
condition={
canUpdateWorkspace &&
canEditDeadline(workspace) &&
editMode !== "off"
}
>
<EditHours
handleSubmit={handleSubmitHours}
max={
editMode === "add" ? maxDeadlineIncrease : maxDeadlineDecrease
}
/>
</Maybe>
</Stack>
</Maybe>
<>
<Button
variant="outlined"
ref={anchorRef}
startIcon={<ScheduleIcon />}
onClick={() => {
setIsOpen(true)
}}
className={`${styles.scheduleButton} ${
shouldDisplayScheduleLabel(workspace) ? "label" : ""
}`}
>
{t("workspaceScheduleButton.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}
canUpdateWorkspace={canUpdateWorkspace}
/>
</Popover>
</>
</span>
)
}
interface StyleProps {
editMode: EditMode
}
const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
wrapper: {
display: "inline-flex",
alignItems: "center",
[theme.breakpoints.down("sm")]: {
flexDirection: "column",
},
},
label: {
padding: theme.spacing(0, 2),
color: theme.palette.text.secondary,
borderRadius: theme.shape.borderRadius,
border: `1px solid ${colors.gray[11]}`, // Same as outlined button
display: "flex",
height: theme.spacing(5), // Same as button
alignItems: "center",
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderRight: 0,
[theme.breakpoints.down("sm")]: {
width: "100%",
padding: theme.spacing(1.5, 2),
flexDirection: "column",
},
},
actions: {
[theme.breakpoints.down("sm")]: {
marginLeft: "auto",
display: "flex",
paddingLeft: theme.spacing(1),
marginRight: -theme.spacing(1),
},
},
scheduleButton: {
flexShrink: 0,
"&.label": {
borderRadius: `0px ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0px`,
},
[theme.breakpoints.down("sm")]: {
width: "100%",
"&.label": {
borderRadius: `0 0 ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px`,
borderLeft: 0,
borderTop: `1px solid ${theme.palette.divider}`,
},
},
},
addButton: {
borderRadius: theme.shape.borderRadius,
border: ({ editMode }) =>
editMode === "add"
? `2px solid ${theme.palette.primary.main}`
: "2px solid transparent",
},
subtractButton: {
borderRadius: theme.shape.borderRadius,
border: ({ editMode }) =>
editMode === "subtract"
? `2px solid ${theme.palette.primary.main}`
: "2px solid transparent",
},
popoverPaper: {
padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing(
3,
)}px`,
},
}))
@@ -1,63 +0,0 @@
import { makeStyles } from "@material-ui/core/styles"
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
import { Maybe } from "components/Conditionals/Maybe"
import { useTranslation } from "react-i18next"
import { Workspace } from "../../api/typesGenerated"
import { combineClasses } from "../../util/combineClasses"
import {
autostartDisplay,
autostopDisplay,
isShuttingDown,
} from "../../util/schedule"
import { isWorkspaceOn } from "../../util/workspace"
export const WorkspaceScheduleLabel: React.FC<{ workspace: Workspace }> = ({
workspace,
}) => {
const styles = useStyles()
const { t } = useTranslation("common")
return (
<ChooseOne>
<Cond condition={isWorkspaceOn(workspace)}>
<span
className={combineClasses([styles.labelText, "chromatic-ignore"])}
>
<Maybe condition={!isShuttingDown(workspace)}>
<strong>{t("schedule.autostopLabel")}</strong>
</Maybe>{" "}
<span className={styles.value}>{autostopDisplay(workspace)}</span>
</span>
</Cond>
<Cond>
<span
className={combineClasses([styles.labelText, "chromatic-ignore"])}
>
<strong>{t("schedule.autostartLabel")}</strong>{" "}
<span className={styles.value}>
{autostartDisplay(workspace.autostart_schedule)}
</span>
</span>
</Cond>
</ChooseOne>
)
}
const useStyles = makeStyles((theme) => ({
labelText: {
marginRight: theme.spacing(1),
marginLeft: theme.spacing(1),
lineHeight: "160%",
[theme.breakpoints.down("sm")]: {
marginRight: 0,
width: "100%",
},
},
value: {
[theme.breakpoints.down("sm")]: {
whiteSpace: "nowrap",
},
},
}))
@@ -1,14 +1,24 @@
import Link from "@material-ui/core/Link"
import { OutdatedHelpTooltip } from "components/Tooltips"
import { FC } from "react"
import { FC, useRef, useState } from "react"
import { Link as RouterLink } from "react-router-dom"
import { createDayString } from "util/createDayString"
import {
getDisplayWorkspaceBuildInitiatedBy,
getDisplayWorkspaceTemplateName,
isWorkspaceOn,
} from "util/workspace"
import { Workspace } from "../../api/typesGenerated"
import { Stats, StatsItem } from "components/Stats/Stats"
import upperFirst from "lodash/upperFirst"
import { autostartDisplay, autostopDisplay } from "util/schedule"
import IconButton from "@material-ui/core/IconButton"
import RemoveIcon from "@material-ui/icons/RemoveOutlined"
import { makeStyles } from "@material-ui/core/styles"
import AddIcon from "@material-ui/icons/AddOutlined"
import Popover from "@material-ui/core/Popover"
import TextField from "@material-ui/core/TextField"
import Button from "@material-ui/core/Button"
const Language = {
workspaceDetails: "Workspace Details",
@@ -24,66 +34,329 @@ const Language = {
export interface WorkspaceStatsProps {
workspace: Workspace
maxDeadlineIncrease: number
maxDeadlineDecrease: number
canUpdateWorkspace: boolean
quota_budget?: number
onDeadlinePlus: (hours: number) => void
onDeadlineMinus: (hours: number) => void
handleUpdate: () => void
}
export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
workspace,
quota_budget,
maxDeadlineDecrease,
maxDeadlineIncrease,
canUpdateWorkspace,
handleUpdate,
onDeadlineMinus,
onDeadlinePlus,
}) => {
const initiatedBy = getDisplayWorkspaceBuildInitiatedBy(
workspace.latest_build,
)
const displayTemplateName = getDisplayWorkspaceTemplateName(workspace)
const styles = useStyles()
const deadlinePlusEnabled = maxDeadlineIncrease >= 1
const deadlineMinusEnabled = maxDeadlineDecrease >= 1
const addButtonRef = useRef<HTMLButtonElement>(null)
const subButtonRef = useRef<HTMLButtonElement>(null)
const [isAddingTime, setIsAddingTime] = useState(false)
const [isSubTime, setIsSubTime] = useState(false)
return (
<Stats aria-label={Language.workspaceDetails}>
<StatsItem
label={Language.templateLabel}
value={
<Link
component={RouterLink}
to={`/templates/${workspace.template_name}`}
>
{displayTemplateName}
</Link>
}
/>
<StatsItem
label={Language.versionLabel}
value={
<>
<>
<Stats aria-label={Language.workspaceDetails}>
<StatsItem
label={Language.templateLabel}
value={
<Link
component={RouterLink}
to={`/templates/${workspace.template_name}/versions/${workspace.latest_build.template_version_name}`}
to={`/templates/${workspace.template_name}`}
>
{workspace.latest_build.template_version_name}
{displayTemplateName}
</Link>
{workspace.outdated && (
<OutdatedHelpTooltip
onUpdateVersion={handleUpdate}
ariaLabel="update version"
/>
)}
</>
}
/>
<StatsItem
label={Language.lastBuiltLabel}
value={createDayString(workspace.latest_build.created_at)}
/>
<StatsItem label={Language.byLabel} value={initiatedBy} />
{workspace.latest_build.daily_cost > 0 && (
<StatsItem
label={Language.costLabel}
value={`${workspace.latest_build.daily_cost} ${
quota_budget ? `/ ${quota_budget}` : ""
}`}
}
/>
)}
</Stats>
<StatsItem
label={Language.versionLabel}
value={
<>
<Link
component={RouterLink}
to={`/templates/${workspace.template_name}/versions/${workspace.latest_build.template_version_name}`}
>
{workspace.latest_build.template_version_name}
</Link>
{workspace.outdated && (
<OutdatedHelpTooltip
onUpdateVersion={handleUpdate}
ariaLabel="update version"
/>
)}
</>
}
/>
<StatsItem
label={Language.lastBuiltLabel}
value={
<>
{upperFirst(createDayString(workspace.latest_build.created_at))}{" "}
by {initiatedBy}
</>
}
/>
{shouldDisplayScheduleLabel(workspace) && (
<StatsItem
label={getScheduleLabel(workspace)}
value={
<span className={styles.scheduleValue}>
<Link
component={RouterLink}
to="settings/schedule"
title="Schedule settings"
>
{isWorkspaceOn(workspace)
? autostopDisplay(workspace)
: autostartDisplay(workspace.autostart_schedule)}
</Link>
{canUpdateWorkspace && canEditDeadline(workspace) && (
<span className={styles.scheduleControls}>
<IconButton
disabled={!deadlineMinusEnabled}
size="small"
title="Subtract hours from deadline"
className={styles.scheduleButton}
ref={subButtonRef}
onClick={() => setIsSubTime(true)}
>
<RemoveIcon />
</IconButton>
<IconButton
disabled={!deadlinePlusEnabled}
size="small"
title="Add hours to deadline"
className={styles.scheduleButton}
ref={addButtonRef}
onClick={() => setIsAddingTime(true)}
>
<AddIcon />
</IconButton>
</span>
)}
</span>
}
/>
)}
{workspace.latest_build.daily_cost > 0 && (
<StatsItem
label={Language.costLabel}
value={`${workspace.latest_build.daily_cost} ${
quota_budget ? `/ ${quota_budget}` : ""
}`}
/>
)}
</Stats>
<Popover
id="schedule-add"
classes={{ paper: styles.timePopoverPaper }}
open={isAddingTime}
anchorEl={addButtonRef.current}
onClose={() => setIsAddingTime(false)}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
>
<span className={styles.timePopoverTitle}>Add hours to deadline</span>
<span className={styles.timePopoverDescription}>
Delay the shutdown of this workspace for a few more hours. This is
only applied once.
</span>
<form
className={styles.timePopoverForm}
onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const hours = Number(formData.get("hours"))
onDeadlinePlus(hours)
setIsAddingTime(false)
}}
>
<TextField
name="hours"
type="number"
size="small"
fullWidth
className={styles.timePopoverField}
InputProps={{ className: styles.timePopoverFieldInput }}
inputProps={{
min: 0,
max: maxDeadlineIncrease,
step: 1,
defaultValue: 1,
}}
/>
<Button
variant="outlined"
size="small"
className={styles.timePopoverButton}
type="submit"
>
Apply
</Button>
</form>
</Popover>
<Popover
id="schedule-sub"
classes={{ paper: styles.timePopoverPaper }}
open={isSubTime}
anchorEl={subButtonRef.current}
onClose={() => setIsSubTime(false)}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
>
<span className={styles.timePopoverTitle}>
Subtract hours to deadline
</span>
<span className={styles.timePopoverDescription}>
Anticipate the shutdown of this workspace for a few more hours. This
is only applied once.
</span>
<form
className={styles.timePopoverForm}
onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const hours = Number(formData.get("hours"))
onDeadlineMinus(hours)
setIsSubTime(false)
}}
>
<TextField
name="hours"
type="number"
size="small"
fullWidth
className={styles.timePopoverField}
InputProps={{ className: styles.timePopoverFieldInput }}
inputProps={{
min: 0,
max: maxDeadlineDecrease,
step: 1,
defaultValue: 1,
}}
/>
<Button
variant="outlined"
size="small"
className={styles.timePopoverButton}
type="submit"
>
Apply
</Button>
</form>
</Popover>
</>
)
}
export const canEditDeadline = (workspace: Workspace): boolean => {
return isWorkspaceOn(workspace) && Boolean(workspace.latest_build.deadline)
}
export const shouldDisplayScheduleLabel = (workspace: Workspace): boolean => {
if (canEditDeadline(workspace)) {
return true
}
if (isWorkspaceOn(workspace)) {
return false
}
return Boolean(workspace.autostart_schedule)
}
const getScheduleLabel = (workspace: Workspace) => {
return isWorkspaceOn(workspace) ? "Stops at" : "Starts at"
}
const useStyles = makeStyles((theme) => ({
scheduleValue: {
display: "flex",
alignItems: "center",
gap: theme.spacing(1.5),
},
scheduleControls: {
display: "flex",
alignItems: "center",
gap: theme.spacing(0.5),
},
scheduleButton: {
border: `1px solid ${theme.palette.divider}`,
borderRadius: 4,
"& svg.MuiSvgIcon-root": {
width: theme.spacing(1.5),
height: theme.spacing(1.5),
},
},
timePopoverPaper: {
padding: theme.spacing(3),
maxWidth: theme.spacing(36),
marginTop: theme.spacing(1),
borderRadius: 4,
display: "flex",
flexDirection: "column",
gap: theme.spacing(1),
},
timePopoverTitle: {
fontWeight: 600,
},
timePopoverDescription: {
color: theme.palette.text.secondary,
},
timePopoverForm: {
display: "flex",
alignItems: "center",
gap: theme.spacing(1),
padding: theme.spacing(1, 0),
},
timePopoverField: {
margin: 0,
},
timePopoverFieldInput: {
fontSize: 14,
padding: theme.spacing(0),
borderRadius: 4,
},
timePopoverButton: {
borderRadius: 4,
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
flexShrink: 0,
},
}))