mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: redesign schedule bumper to handle multiple hours of change at once (#4535)
* Start sketching out new design * Working but ugly * Highlight chosen mode * Format * Set hours field width * Alignment on desktop * Use primary button color * Make 1 the default change * Add stepper max * Fix storybook * Handle undefined deadline * Access deadline correctly * Format * Fix overflow on mobile
This commit is contained in:
@@ -34,10 +34,12 @@ export interface WorkspaceProps {
|
||||
onExtend: () => void
|
||||
}
|
||||
scheduleProps: {
|
||||
onDeadlinePlus: () => void
|
||||
onDeadlineMinus: () => void
|
||||
onDeadlinePlus: (hours: number) => void
|
||||
onDeadlineMinus: (hours: number) => void
|
||||
deadlinePlusEnabled: () => boolean
|
||||
deadlineMinusEnabled: () => boolean
|
||||
maxDeadlineIncrease: number
|
||||
maxDeadlineDecrease: number
|
||||
}
|
||||
handleStart: () => void
|
||||
handleStop: () => void
|
||||
@@ -121,6 +123,8 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
||||
onDeadlinePlus={scheduleProps.onDeadlinePlus}
|
||||
deadlineMinusEnabled={scheduleProps.deadlineMinusEnabled}
|
||||
deadlinePlusEnabled={scheduleProps.deadlinePlusEnabled}
|
||||
maxDeadlineDecrease={scheduleProps.maxDeadlineDecrease}
|
||||
maxDeadlineIncrease={scheduleProps.maxDeadlineIncrease}
|
||||
canUpdateWorkspace={canUpdateWorkspace}
|
||||
/>
|
||||
<WorkspaceActions
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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 (
|
||||
<form onSubmit={() => handleSubmit(hours)}>
|
||||
<Stack direction="row" alignItems="baseline" spacing={1}>
|
||||
<TextField
|
||||
className={styles.inputField}
|
||||
inputProps={{ min: 0, max, step: 1 }}
|
||||
label={t("workspaceScheduleButton.hours")}
|
||||
value={hours}
|
||||
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",
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -16,6 +16,12 @@ export default {
|
||||
canUpdateWorkspace: {
|
||||
defaultValue: true,
|
||||
},
|
||||
deadlineMinusEnabled: {
|
||||
defaultValue: (): boolean => false,
|
||||
},
|
||||
deadlinePlusEnabled: {
|
||||
defaultValue: (): boolean => false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -64,6 +70,7 @@ WorkspaceOffShort.args = {
|
||||
|
||||
export const WorkspaceOffLong = Template.bind({})
|
||||
WorkspaceOffLong.args = {
|
||||
deadlinePlusEnabled: () => true,
|
||||
workspace: {
|
||||
...Mocks.MockWorkspace,
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import dayjs from "dayjs"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import * as Mocks from "../../testHelpers/entities"
|
||||
import { shouldDisplayPlusMinus } from "./WorkspaceScheduleButton"
|
||||
import { canEditDeadline } from "./WorkspaceScheduleButton"
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
@@ -13,7 +13,7 @@ describe("WorkspaceScheduleButton", () => {
|
||||
const workspace: TypesGen.Workspace = Mocks.MockStoppedWorkspace
|
||||
|
||||
// Then: shouldDisplayPlusMinus should be false
|
||||
expect(shouldDisplayPlusMinus(workspace)).toBeFalsy()
|
||||
expect(canEditDeadline(workspace)).toBeFalsy()
|
||||
})
|
||||
|
||||
it("should display if the workspace is running", () => {
|
||||
@@ -21,7 +21,7 @@ describe("WorkspaceScheduleButton", () => {
|
||||
const workspace: TypesGen.Workspace = Mocks.MockWorkspace
|
||||
|
||||
// Then: shouldDisplayPlusMinus should be false
|
||||
expect(shouldDisplayPlusMinus(workspace)).toBeTruthy()
|
||||
expect(canEditDeadline(workspace)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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 { 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"
|
||||
@@ -17,6 +19,7 @@ import { useTranslation } from "react-i18next"
|
||||
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
|
||||
@@ -27,12 +30,12 @@ dayjs.extend(duration)
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
export const shouldDisplayPlusMinus = (workspace: Workspace): boolean => {
|
||||
export const canEditDeadline = (workspace: Workspace): boolean => {
|
||||
return isWorkspaceOn(workspace) && Boolean(workspace.latest_build.deadline)
|
||||
}
|
||||
|
||||
export const shouldDisplayScheduleLabel = (workspace: Workspace): boolean => {
|
||||
if (shouldDisplayPlusMinus(workspace)) {
|
||||
if (canEditDeadline(workspace)) {
|
||||
return true
|
||||
}
|
||||
if (isWorkspaceOn(workspace)) {
|
||||
@@ -43,13 +46,17 @@ export const shouldDisplayScheduleLabel = (workspace: Workspace): boolean => {
|
||||
|
||||
export interface WorkspaceScheduleButtonProps {
|
||||
workspace: Workspace
|
||||
onDeadlinePlus: () => void
|
||||
onDeadlineMinus: () => void
|
||||
onDeadlinePlus: (hours: number) => void
|
||||
onDeadlineMinus: (hours: number) => void
|
||||
deadlineMinusEnabled: () => boolean
|
||||
deadlinePlusEnabled: () => boolean
|
||||
maxDeadlineIncrease: number
|
||||
maxDeadlineDecrease: number
|
||||
canUpdateWorkspace: boolean
|
||||
}
|
||||
|
||||
export type EditMode = "add" | "subtract" | "off"
|
||||
|
||||
export const WorkspaceScheduleButton: React.FC<
|
||||
WorkspaceScheduleButtonProps
|
||||
> = ({
|
||||
@@ -58,49 +65,93 @@ export const WorkspaceScheduleButton: React.FC<
|
||||
onDeadlineMinus,
|
||||
deadlinePlusEnabled,
|
||||
deadlineMinusEnabled,
|
||||
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()
|
||||
const styles = useStyles({ editMode })
|
||||
|
||||
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}>
|
||||
{shouldDisplayScheduleLabel(workspace) && (
|
||||
<span className={styles.label}>
|
||||
<WorkspaceScheduleLabel workspace={workspace} />
|
||||
{canUpdateWorkspace && shouldDisplayPlusMinus(workspace) && (
|
||||
<span className={styles.actions}>
|
||||
<IconButton
|
||||
className={styles.iconButton}
|
||||
size="small"
|
||||
disabled={!deadlineMinusEnabled()}
|
||||
onClick={onDeadlineMinus}
|
||||
>
|
||||
<Tooltip title={t("workspaceScheduleButton.editDeadlineMinus")}>
|
||||
<RemoveIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
className={styles.iconButton}
|
||||
size="small"
|
||||
disabled={!deadlinePlusEnabled()}
|
||||
onClick={onDeadlinePlus}
|
||||
>
|
||||
<Tooltip title={t("workspaceScheduleButton.editDeadlinePlus")}>
|
||||
<AddIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<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}
|
||||
size="small"
|
||||
disabled={!deadlineMinusEnabled()}
|
||||
onClick={() => {
|
||||
setEditMode("subtract")
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title={t("workspaceScheduleButton.editDeadlineMinus")}
|
||||
>
|
||||
<RemoveIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
className={styles.addButton}
|
||||
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
|
||||
ref={anchorRef}
|
||||
@@ -139,7 +190,11 @@ export const WorkspaceScheduleButton: React.FC<
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
interface StyleProps {
|
||||
editMode: EditMode
|
||||
}
|
||||
|
||||
const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
|
||||
wrapper: {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
@@ -151,15 +206,13 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
},
|
||||
label: {
|
||||
borderRight: 0,
|
||||
padding: "0 8px 0 16px",
|
||||
padding: theme.spacing(0, 2),
|
||||
color: theme.palette.text.secondary,
|
||||
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: theme.spacing(1.5, 2),
|
||||
flexDirection: "column",
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
@@ -190,8 +243,19 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
iconButton: {
|
||||
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(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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,
|
||||
Language,
|
||||
} from "../../util/schedule"
|
||||
import { isWorkspaceOn } from "../../util/workspace"
|
||||
|
||||
@@ -13,33 +15,38 @@ 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>{Language.autoStopLabel}</strong>}{" "}
|
||||
<span className={styles.value}>{stopLabel}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
return (
|
||||
<span className={combineClasses([styles.labelText, "chromatic-ignore"])}>
|
||||
<strong>{Language.autoStartLabel}</strong>{" "}
|
||||
<span className={styles.value}>
|
||||
{autoStartDisplay(workspace.autostart_schedule)}
|
||||
</span>
|
||||
</span>
|
||||
<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(2),
|
||||
marginRight: theme.spacing(1),
|
||||
marginLeft: theme.spacing(1),
|
||||
lineHeight: "160%",
|
||||
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
"confirmLabel": "Name of {{entity}} to delete",
|
||||
"incorrectName": "Incorrect {{entity}} name."
|
||||
},
|
||||
"schedule": {
|
||||
"autoStartLabel": "Starts at",
|
||||
"autoStopLabel": "Stops at"
|
||||
},
|
||||
"ctas": {
|
||||
"dismissCta": "Dismiss",
|
||||
"expand": "Click here to learn more",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
},
|
||||
"workspaceScheduleButton": {
|
||||
"schedule": "Schedule",
|
||||
"editDeadlineMinus": "Subtract one hour",
|
||||
"editDeadlinePlus": "Add one hour"
|
||||
"editDeadlineMinus": "Subtract hours",
|
||||
"editDeadlinePlus": "Add hours",
|
||||
"submitDeadline": "Set",
|
||||
"hours": "Hours"
|
||||
},
|
||||
"ctas": {
|
||||
"createWorkspaceCta": "Create new workspace",
|
||||
|
||||
@@ -4,6 +4,11 @@ import dayjs from "dayjs"
|
||||
import { useContext } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
getMaxDeadline,
|
||||
getMaxDeadlineChange,
|
||||
getMinDeadline,
|
||||
} from "util/schedule"
|
||||
import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors"
|
||||
import { StateFrom } from "xstate"
|
||||
import { DeleteDialog } from "../../components/Dialogs/DeleteDialog/DeleteDialog"
|
||||
@@ -31,6 +36,7 @@ export const WorkspaceReadyPage = ({
|
||||
const [bannerState, bannerSend] = useActor(
|
||||
workspaceState.children["scheduleBannerMachine"],
|
||||
)
|
||||
const deadline = bannerState.context.deadline
|
||||
const xServices = useContext(XServiceContext)
|
||||
const featureVisibility = useSelector(
|
||||
xServices.entitlementsXService,
|
||||
@@ -39,6 +45,7 @@ export const WorkspaceReadyPage = ({
|
||||
const [buildInfoState] = useActor(xServices.buildInfoXService)
|
||||
const {
|
||||
workspace,
|
||||
template,
|
||||
refreshWorkspaceWarning,
|
||||
builds,
|
||||
getBuildsError,
|
||||
@@ -81,20 +88,30 @@ export const WorkspaceReadyPage = ({
|
||||
},
|
||||
}}
|
||||
scheduleProps={{
|
||||
onDeadlineMinus: () => {
|
||||
onDeadlineMinus: (hours: number) => {
|
||||
bannerSend({
|
||||
type: "DECREASE_DEADLINE",
|
||||
hours: 1,
|
||||
hours,
|
||||
})
|
||||
},
|
||||
onDeadlinePlus: () => {
|
||||
onDeadlinePlus: (hours: number) => {
|
||||
bannerSend({
|
||||
type: "INCREASE_DEADLINE",
|
||||
hours: 1,
|
||||
hours,
|
||||
})
|
||||
},
|
||||
deadlineMinusEnabled: () => !bannerState.matches("atMinDeadline"),
|
||||
deadlinePlusEnabled: () => !bannerState.matches("atMaxDeadline"),
|
||||
maxDeadlineDecrease: deadline
|
||||
? getMaxDeadlineChange(deadline, getMinDeadline())
|
||||
: 0,
|
||||
maxDeadlineIncrease:
|
||||
deadline && template
|
||||
? getMaxDeadlineChange(
|
||||
getMaxDeadline(workspace, template),
|
||||
deadline,
|
||||
)
|
||||
: 0,
|
||||
}}
|
||||
isUpdating={workspaceState.hasTag("updating")}
|
||||
workspace={workspace}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
deadlineExtensionMin,
|
||||
extractTimezone,
|
||||
getMaxDeadline,
|
||||
getMaxDeadlineChange,
|
||||
getMinDeadline,
|
||||
stripTimezone,
|
||||
} from "./schedule"
|
||||
@@ -124,3 +125,19 @@ describe("canReduceDeadline", () => {
|
||||
expect(canReduceDeadline(dayjs().add(100, "years"))).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getMaxDeadlineChange", () => {
|
||||
it("should return the number of hours you can add before hitting the max deadline", () => {
|
||||
const deadline = dayjs()
|
||||
const maxDeadline = dayjs().add(1, "hour").add(40, "minutes")
|
||||
// you can only add one hour even though the max is 1:40 away
|
||||
expect(getMaxDeadlineChange(deadline, maxDeadline)).toEqual(1)
|
||||
})
|
||||
|
||||
it("should return the number of hours you can subtract before hitting the min deadline", () => {
|
||||
const deadline = dayjs().add(2, "hours").add(40, "minutes")
|
||||
const minDeadline = dayjs()
|
||||
// you can only subtract 2 hours even though the min is 2:40 less
|
||||
expect(getMaxDeadlineChange(deadline, minDeadline)).toEqual(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -167,3 +167,15 @@ export function canReduceDeadline(deadline: dayjs.Dayjs): boolean {
|
||||
|
||||
export const getDeadline = (workspace: Workspace): dayjs.Dayjs =>
|
||||
dayjs(workspace.latest_build.deadline).utc()
|
||||
|
||||
/**
|
||||
* Get number of hours you can add or subtract to the current deadline before hitting the max or min deadline.
|
||||
* @param deadline
|
||||
* @param workspace
|
||||
* @param template
|
||||
* @returns number, in hours
|
||||
*/
|
||||
export const getMaxDeadlineChange = (
|
||||
deadline: dayjs.Dayjs,
|
||||
extremeDeadline: dayjs.Dayjs,
|
||||
): number => Math.abs(deadline.diff(extremeDeadline, "hours"))
|
||||
|
||||
Reference in New Issue
Block a user