From 8b49100b855cc5350cd36f9cfaeb6cb765058ee1 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 19 Dec 2023 17:24:47 +0000 Subject: [PATCH 1/2] refactor(site): move workspace schedule controls to its own component --- .../pages/WorkspacePage/Workspace.stories.tsx | 10 - site/src/pages/WorkspacePage/Workspace.tsx | 11 - .../WorkspacePage/WorkspaceReadyPage.tsx | 40 +- .../WorkspaceScheduleControls.tsx | 387 ++++++++++++++++++ .../pages/WorkspacePage/WorkspaceStats.tsx | 354 +--------------- 5 files changed, 401 insertions(+), 401 deletions(-) create mode 100644 site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index f3f27e840c9f9..5a86338f6b1f4 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -66,16 +66,6 @@ type Story = StoryObj; export const Running: Story = { args: { - scheduleProps: { - onDeadlineMinus: () => { - // do nothing, this is just for storybook - }, - onDeadlinePlus: () => { - // do nothing, this is just for storybook - }, - maxDeadlineDecrease: 0, - maxDeadlineIncrease: 24, - }, workspace: Mocks.MockWorkspace, handleStart: action("start"), handleStop: action("stop"), diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 08b278bec51a3..22e24c73c9fce 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -37,12 +37,6 @@ export type WorkspaceError = export type WorkspaceErrors = Partial>; export interface WorkspaceProps { - scheduleProps: { - onDeadlinePlus: (hours: number) => void; - onDeadlineMinus: (hours: number) => void; - maxDeadlineIncrease: number; - maxDeadlineDecrease: number; - }; handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; handleStop: () => void; handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; @@ -81,7 +75,6 @@ export interface WorkspaceProps { * Workspace is the top-level component for viewing an individual workspace */ export const Workspace: FC> = ({ - scheduleProps, handleStart, handleStop, handleRestart, @@ -186,10 +179,6 @@ export const Workspace: FC> = ({ workspace={workspace} handleUpdate={handleUpdate} canUpdateWorkspace={canUpdateWorkspace} - maxDeadlineDecrease={scheduleProps.maxDeadlineDecrease} - maxDeadlineIncrease={scheduleProps.maxDeadlineIncrease} - onDeadlineMinus={scheduleProps.onDeadlineMinus} - onDeadlinePlus={scheduleProps.onDeadlinePlus} /> {canUpdateWorkspace && ( diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 67f40ef0f7c24..e917deafa2b74 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -3,12 +3,6 @@ import { useFeatureVisibility } from "hooks/useFeatureVisibility"; import { FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useNavigate } from "react-router-dom"; -import { - getDeadline, - getMaxDeadline, - getMaxDeadlineChange, - getMinDeadline, -} from "utils/schedule"; import { Workspace } from "./Workspace"; import { pageTitle } from "utils/page"; import { hasJobError } from "utils/workspace"; @@ -29,16 +23,14 @@ import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; import { activate, changeVersion, - decreaseDeadline, deleteWorkspace, - increaseDeadline, updateWorkspace, stopWorkspace, startWorkspace, cancelBuild, } from "api/queries/workspaces"; import { getErrorMessage } from "api/errors"; -import { displaySuccess, displayError } from "components/GlobalSnackbar/utils"; +import { displayError } from "components/GlobalSnackbar/utils"; import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment"; import { WorkspacePermissions } from "./permissions"; import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; @@ -101,27 +93,6 @@ export const WorkspaceReadyPage = ({ mutationFn: restartWorkspace, }); - // Schedule controls - const deadline = getDeadline(workspace); - const onDeadlineChangeSuccess = () => { - displaySuccess("Updated workspace shutdown time."); - }; - const onDeadlineChangeFails = (error: unknown) => { - displayError( - getErrorMessage(error, "Failed to update workspace shutdown time."), - ); - }; - const decreaseMutation = useMutation({ - ...decreaseDeadline(workspace), - onSuccess: onDeadlineChangeSuccess, - onError: onDeadlineChangeFails, - }); - const increaseMutation = useMutation({ - ...increaseDeadline(workspace), - onSuccess: onDeadlineChangeSuccess, - onError: onDeadlineChangeFails, - }); - // Auto start const canAutostartResponse = useQuery( workspaceResolveAutostart(workspace.id), @@ -227,15 +198,6 @@ export const WorkspaceReadyPage = ({ = ({ + workspace, + canUpdateSchedule, +}) => { + const deadline = getDeadline(workspace); + const maxDeadlineDecrease = getMaxDeadlineChange(deadline, getMinDeadline()); + const maxDeadlineIncrease = getMaxDeadlineChange( + getMaxDeadline(workspace), + deadline, + ); + const deadlinePlusEnabled = maxDeadlineIncrease >= 1; + const deadlineMinusEnabled = maxDeadlineDecrease >= 1; + + const onDeadlineChangeSuccess = () => { + displaySuccess("Updated workspace shutdown time."); + }; + const onDeadlineChangeFails = (error: unknown) => { + displayError( + getErrorMessage(error, "Failed to update workspace shutdown time."), + ); + }; + const decreaseMutation = useMutation({ + ...decreaseDeadline(workspace), + onSuccess: onDeadlineChangeSuccess, + onError: onDeadlineChangeFails, + }); + const increaseMutation = useMutation({ + ...increaseDeadline(workspace), + onSuccess: onDeadlineChangeSuccess, + onError: onDeadlineChangeFails, + }); + + return ( +
+ {isWorkspaceOn(workspace) ? ( + + ) : ( + + {autostartDisplay(workspace.autostart_schedule)} + + )} + + {canUpdateSchedule && canEditDeadline(workspace) && ( + + + + + + + + + + + + + + + + + + + + + + + )} +
+ ); +}; + +interface AddTimeContentProps { + maxDeadlineIncrease: number; + onDeadlinePlus: (value: number) => void; +} + +const AddTimeContent: FC = ({ + maxDeadlineIncrease, + onDeadlinePlus, +}) => { + const popover = usePopover(); + + return ( + <> + Add hours to deadline + + Delay the shutdown of this workspace for a few more hours. This is only + applied once. + +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const hours = Number(formData.get("hours")); + onDeadlinePlus(hours); + popover.setIsOpen(false); + }} + > + + + + + + ); +}; + +interface DecreaseTimeContentProps { + maxDeadlineDecrease: number; + onDeadlineMinus: (hours: number) => void; +} + +export const DecreaseTimeContent: FC = ({ + maxDeadlineDecrease, + onDeadlineMinus, +}) => { + const popover = usePopover(); + + return ( + <> + Subtract hours to deadline + + Anticipate the shutdown of this workspace for a few more hours. This is + only applied once. + +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const hours = Number(formData.get("hours")); + onDeadlineMinus(hours); + popover.setIsOpen(false); + }} + > + + + + + + ); +}; + +interface AutoStopDisplayProps { + workspace: Workspace; +} + +const AutoStopDisplay: FC = ({ workspace }) => { + const display = autostopDisplay(workspace); + + if (display.tooltip) { + return ( + + ({ + color: isShutdownSoon(workspace) + ? `${theme.palette.warning.light} !important` + : undefined, + })} + > + {display.message} + + + ); + } + + return {display.message}; +}; + +const ScheduleSettingsLink = forwardRef( + (props, ref) => { + return ( + + ); + }, +); + +const hasDeadline = (workspace: Workspace): boolean => { + return Boolean(workspace.latest_build.deadline); +}; + +const hasAutoStart = (workspace: Workspace): boolean => { + return Boolean(workspace.autostart_schedule); +}; + +export const canEditDeadline = (workspace: Workspace): boolean => { + return isWorkspaceOn(workspace) && hasDeadline(workspace); +}; + +export const shouldDisplayScheduleControls = ( + workspace: Workspace, +): boolean => { + const willAutoStop = isWorkspaceOn(workspace) && hasDeadline(workspace); + const willAutoStart = !isWorkspaceOn(workspace) && hasAutoStart(workspace); + return willAutoStop || willAutoStart; +}; + +const isShutdownSoon = (workspace: Workspace): boolean => { + const deadline = workspace.latest_build.deadline; + if (!deadline) { + return false; + } + const deadlineDate = new Date(deadline); + const now = new Date(); + const diff = deadlineDate.getTime() - now.getTime(); + const oneHour = 1000 * 60 * 60; + return diff < oneHour; +}; + +export const scheduleLabel = (workspace: Workspace) => { + return isWorkspaceOn(workspace) ? "Stops" : "Starts at"; +}; + +const classNames = { + paper: css` + padding: 24px; + max-width: 288px; + margin-top: 8px; + border-radius: 4px; + display: flex; + flex-direction: column; + gap: 8px; + `, + + deadlineFormInput: css` + font-size: 14px; + padding: 0px; + border-radius: 4px; + `, +}; + +const styles = { + scheduleValue: { + display: "flex", + alignItems: "center", + gap: 12, + }, + + scheduleControls: { + display: "flex", + alignItems: "center", + gap: 4, + }, + + scheduleButton: (theme) => ({ + border: `1px solid ${theme.palette.divider}`, + borderRadius: 4, + width: 20, + height: 20, + + "& svg.MuiSvgIcon-root": { + width: 12, + height: 12, + }, + }), + + timePopoverTitle: { + fontWeight: 600, + marginBottom: 8, + }, + + timePopoverDescription: (theme) => ({ + color: theme.palette.text.secondary, + }), + + timePopoverForm: { + display: "flex", + alignItems: "center", + gap: 8, + padding: "8px 0", + marginTop: 12, + }, + + timePopoverField: { + margin: 0, + }, + + timePopoverButton: { + borderRadius: 4, + paddingLeft: 16, + paddingRight: 16, + flexShrink: 0, + }, +} satisfies Record>; diff --git a/site/src/pages/WorkspacePage/WorkspaceStats.tsx b/site/src/pages/WorkspacePage/WorkspaceStats.tsx index 866c26da71809..8f76c671e2a6b 100644 --- a/site/src/pages/WorkspacePage/WorkspaceStats.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceStats.tsx @@ -1,33 +1,21 @@ -import { css } from "@emotion/css"; import { type Interpolation, type Theme } from "@emotion/react"; -import Link, { LinkProps } from "@mui/material/Link"; +import Link from "@mui/material/Link"; import { WorkspaceOutdatedTooltip } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; -import { forwardRef, type FC } from "react"; +import { type FC } from "react"; import { Link as RouterLink } from "react-router-dom"; -import { - getDisplayWorkspaceTemplateName, - isWorkspaceOn, -} from "utils/workspace"; +import { getDisplayWorkspaceTemplateName } from "utils/workspace"; import type { Workspace } from "api/typesGenerated"; import { Stats, StatsItem } from "components/Stats/Stats"; -import { autostartDisplay, autostopDisplay } from "utils/schedule"; -import IconButton from "@mui/material/IconButton"; -import RemoveIcon from "@mui/icons-material/RemoveOutlined"; -import AddIcon from "@mui/icons-material/AddOutlined"; -import TextField from "@mui/material/TextField"; -import Button from "@mui/material/Button"; import { WorkspaceStatusText } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"; import { DormantDeletionStat } from "components/WorkspaceDeletion"; -import { - Popover, - PopoverContent, - PopoverTrigger, - usePopover, -} from "components/Popover/Popover"; import { workspaceQuota } from "api/queries/workspaceQuota"; import { useQuery } from "react-query"; -import Tooltip from "@mui/material/Tooltip"; import _ from "lodash"; +import { + WorkspaceScheduleControls, + scheduleLabel, + shouldDisplayScheduleControls, +} from "./WorkspaceScheduleControls"; const Language = { workspaceDetails: "Workspace Details", @@ -38,26 +26,16 @@ const Language = { export interface WorkspaceStatsProps { workspace: Workspace; - maxDeadlineIncrease: number; - maxDeadlineDecrease: number; canUpdateWorkspace: boolean; - onDeadlinePlus: (hours: number) => void; - onDeadlineMinus: (hours: number) => void; handleUpdate: () => void; } export const WorkspaceStats: FC = ({ workspace, - maxDeadlineDecrease, - maxDeadlineIncrease, canUpdateWorkspace, handleUpdate, - onDeadlineMinus, - onDeadlinePlus, }) => { const displayTemplateName = getDisplayWorkspaceTemplateName(workspace); - const deadlinePlusEnabled = maxDeadlineIncrease >= 1; - const deadlineMinusEnabled = maxDeadlineDecrease >= 1; const quotaQuery = useQuery(workspaceQuota(workspace.owner_name)); const quotaBudget = quotaQuery.data?.budget; @@ -107,69 +85,15 @@ export const WorkspaceStats: FC = ({ } /> - {shouldDisplayScheduleLabel(workspace) && ( + {shouldDisplayScheduleControls(workspace) && ( - {isWorkspaceOn(workspace) ? ( - - ) : ( - - {autostartDisplay(workspace.autostart_schedule)} - - )} - - {canUpdateWorkspace && canEditDeadline(workspace) && ( - - - - - - - - - - - - - - - - - - - - - - - )} - + } /> )} @@ -187,206 +111,6 @@ export const WorkspaceStats: FC = ({ ); }; -interface AddTimeContentProps { - maxDeadlineIncrease: number; - onDeadlinePlus: (value: number) => void; -} - -const AddTimeContent: FC = ({ - maxDeadlineIncrease, - onDeadlinePlus, -}) => { - const popover = usePopover(); - - return ( - <> - Add hours to deadline - - Delay the shutdown of this workspace for a few more hours. This is only - applied once. - -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const hours = Number(formData.get("hours")); - onDeadlinePlus(hours); - popover.setIsOpen(false); - }} - > - - - - - - ); -}; - -interface DecreaseTimeContentProps { - maxDeadlineDecrease: number; - onDeadlineMinus: (hours: number) => void; -} - -export const DecreaseTimeContent: FC = ({ - maxDeadlineDecrease, - onDeadlineMinus, -}) => { - const popover = usePopover(); - - return ( - <> - Subtract hours to deadline - - Anticipate the shutdown of this workspace for a few more hours. This is - only applied once. - -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const hours = Number(formData.get("hours")); - onDeadlineMinus(hours); - popover.setIsOpen(false); - }} - > - - - - - - ); -}; - -interface AutoStopDisplayProps { - workspace: Workspace; -} - -const AutoStopDisplay: FC = ({ workspace }) => { - const display = autostopDisplay(workspace); - - if (display.tooltip) { - return ( - - ({ - color: isShutdownSoon(workspace) - ? `${theme.palette.warning.light} !important` - : undefined, - })} - > - {display.message} - - - ); - } - - return {display.message}; -}; - -const ScheduleSettingsLink = forwardRef( - (props, ref) => { - return ( - - ); - }, -); - -const hasDeadline = (workspace: Workspace): boolean => { - return Boolean(workspace.latest_build.deadline); -}; - -const hasAutoStart = (workspace: Workspace): boolean => { - return Boolean(workspace.autostart_schedule); -}; - -export const canEditDeadline = (workspace: Workspace): boolean => { - return isWorkspaceOn(workspace) && hasDeadline(workspace); -}; - -export const shouldDisplayScheduleLabel = (workspace: Workspace): boolean => { - const willAutoStop = isWorkspaceOn(workspace) && hasDeadline(workspace); - const willAutoStart = !isWorkspaceOn(workspace) && hasAutoStart(workspace); - return willAutoStop || willAutoStart; -}; - -const scheduleLabel = (workspace: Workspace) => { - return isWorkspaceOn(workspace) ? "Stops" : "Starts at"; -}; - -const isShutdownSoon = (workspace: Workspace): boolean => { - const deadline = workspace.latest_build.deadline; - if (!deadline) { - return false; - } - const deadlineDate = new Date(deadline); - const now = new Date(); - const diff = deadlineDate.getTime() - now.getTime(); - const oneHour = 1000 * 60 * 60; - return diff < oneHour; -}; - -const classNames = { - paper: css` - padding: 24px; - max-width: 288px; - margin-top: 8px; - border-radius: 4px; - display: flex; - flex-direction: column; - gap: 8px; - `, - - deadlineFormInput: css` - font-size: 14px; - padding: 0px; - border-radius: 4px; - `, -}; - const styles = { stats: (theme) => ({ padding: 0, @@ -413,56 +137,4 @@ const styles = { fontWeight: 500, }, }, - - scheduleValue: { - display: "flex", - alignItems: "center", - gap: 12, - }, - - scheduleControls: { - display: "flex", - alignItems: "center", - gap: 4, - }, - - scheduleButton: (theme) => ({ - border: `1px solid ${theme.palette.divider}`, - borderRadius: 4, - width: 20, - height: 20, - - "& svg.MuiSvgIcon-root": { - width: 12, - height: 12, - }, - }), - - timePopoverTitle: { - fontWeight: 600, - marginBottom: 8, - }, - - timePopoverDescription: (theme) => ({ - color: theme.palette.text.secondary, - }), - - timePopoverForm: { - display: "flex", - alignItems: "center", - gap: 8, - padding: "8px 0", - marginTop: 12, - }, - - timePopoverField: { - margin: 0, - }, - - timePopoverButton: { - borderRadius: 4, - paddingLeft: 16, - paddingRight: 16, - flexShrink: 0, - }, } satisfies Record>; From f0d396bb2234f1e65e92edefc7ba8cd0d5d15a69 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 19 Dec 2023 23:40:35 +0000 Subject: [PATCH 2/2] Improve accessibility --- .../WorkspacePage/WorkspaceScheduleControls.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx index 11282b092bfba..1ef0fb78b0eb5 100644 --- a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx @@ -143,11 +143,11 @@ const AddTimeContent: FC = ({ return ( <> - Add hours to deadline - +

Add hours to deadline

+

Delay the shutdown of this workspace for a few more hours. This is only applied once. - +

{ @@ -195,11 +195,11 @@ export const DecreaseTimeContent: FC = ({ return ( <> - Subtract hours to deadline - +

Subtract hours to deadline

+

Anticipate the shutdown of this workspace for a few more hours. This is only applied once. - +

{ @@ -359,11 +359,13 @@ const styles = { timePopoverTitle: { fontWeight: 600, + margin: 0, marginBottom: 8, }, timePopoverDescription: (theme) => ({ color: theme.palette.text.secondary, + margin: 0, }), timePopoverForm: {