mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor(site): Refactor the schedule controls in the workspace page (#7083)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user