Skip to content

fix: redesign schedule bumper to handle multiple hours of change at once #4535

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 15 commits into from
Oct 14, 2022
8 changes: 6 additions & 2 deletions site/src/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions site/src/components/WorkspaceScheduleButton/EditHours.tsx
Original file line number Diff line number Diff line change
@@ -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",
},
},
}))
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export default {
canUpdateWorkspace: {
defaultValue: true,
},
deadlineMinusEnabled: {
defaultValue: (): boolean => false,
},
deadlinePlusEnabled: {
defaultValue: (): boolean => false,
},
},
}

Expand Down Expand Up @@ -64,6 +70,7 @@ WorkspaceOffShort.args = {

export const WorkspaceOffLong = Template.bind({})
WorkspaceOffLong.args = {
deadlinePlusEnabled: () => true,
workspace: {
...Mocks.MockWorkspace,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -13,15 +13,15 @@ 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", () => {
// Given: a stopped workspace
const workspace: TypesGen.Workspace = Mocks.MockWorkspace

// Then: shouldDisplayPlusMinus should be false
expect(shouldDisplayPlusMinus(workspace)).toBeTruthy()
expect(canEditDeadline(workspace)).toBeTruthy()
})
})
})
146 changes: 105 additions & 41 deletions site/src/components/WorkspaceScheduleButton/WorkspaceScheduleButton.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
Expand All @@ -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)) {
Expand All @@ -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
> = ({
Expand All @@ -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}
Expand Down Expand Up @@ -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",
Expand All @@ -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: {
Expand Down Expand Up @@ -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(
Expand Down
Loading