From 84f1bc4e6ac1390236949c9b764842e1d910105e Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 14 Dec 2023 18:41:39 +0000 Subject: [PATCH 01/17] Extract a few components to topbar module --- .vscode/settings.json | 5 +- site/src/components/FullPageLayout/Topbar.tsx | 80 +++++++++++++++++++ .../TemplateVersionEditor.tsx | 59 ++++---------- 3 files changed, 99 insertions(+), 45 deletions(-) create mode 100644 site/src/components/FullPageLayout/Topbar.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index bcbdb7baeb9fa..d1b99fd3f373e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,8 +21,8 @@ "contravariance", "cronstrue", "databasefake", - "dbmem", "dbgen", + "dbmem", "dbtype", "DERP", "derphttp", @@ -118,13 +118,13 @@ "stretchr", "STTY", "stuntest", - "tanstack", "tailbroker", "tailcfg", "tailexchange", "tailnet", "tailnettest", "Tailscale", + "tanstack", "tbody", "TCGETS", "tcpip", @@ -141,6 +141,7 @@ "tios", "tmpdir", "tokenconfig", + "Topbar", "tparallel", "trialer", "trimprefix", diff --git a/site/src/components/FullPageLayout/Topbar.tsx b/site/src/components/FullPageLayout/Topbar.tsx new file mode 100644 index 0000000000000..7bd90bc3a79c4 --- /dev/null +++ b/site/src/components/FullPageLayout/Topbar.tsx @@ -0,0 +1,80 @@ +import Button, { ButtonProps } from "@mui/material/Button"; +import IconButton, { IconButtonProps } from "@mui/material/IconButton"; +import { useTheme } from "@mui/material/styles"; +import { HTMLAttributes, forwardRef } from "react"; + +export const Topbar = (props: HTMLAttributes) => { + const theme = useTheme(); + + return ( +
+ ); +}; + +export const TopbarIconButton = forwardRef( + (props, ref) => { + return ( + + ); + }, +) as typeof IconButton; + +export const TopbarButton = (props: ButtonProps) => { + return ( + - ); -}; - const styles = { tab: (theme) => ({ "&:not(:disabled)": { From 4ed5550cb373dab853b537906642defea1b43d94 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 14 Dec 2023 18:44:39 +0000 Subject: [PATCH 02/17] Extract and refactor a few more topbar componetns --- site/src/components/FullPageLayout/Topbar.tsx | 14 +++++++++++++- .../TemplateVersionEditor.tsx | 15 +++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/site/src/components/FullPageLayout/Topbar.tsx b/site/src/components/FullPageLayout/Topbar.tsx index 7bd90bc3a79c4..8b74abd54db34 100644 --- a/site/src/components/FullPageLayout/Topbar.tsx +++ b/site/src/components/FullPageLayout/Topbar.tsx @@ -1,6 +1,7 @@ import Button, { ButtonProps } from "@mui/material/Button"; import IconButton, { IconButtonProps } from "@mui/material/IconButton"; import { useTheme } from "@mui/material/styles"; +import { Avatar, AvatarProps } from "components/Avatar/Avatar"; import { HTMLAttributes, forwardRef } from "react"; export const Topbar = (props: HTMLAttributes) => { @@ -56,7 +57,7 @@ export const TopbarButton = (props: ButtonProps) => { ); }; -export const TopbarDataGroup = (props: HTMLAttributes) => { +export const TopbarData = (props: HTMLAttributes) => { return (
) => { ); }; + +export const TopbarAvatar = (props: AvatarProps) => { + return ( + + ); +}; diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx index 49f10db4ca30b..a4d585e32534d 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx @@ -12,7 +12,6 @@ import type { } from "api/typesGenerated"; import { Link as RouterLink } from "react-router-dom"; import { Alert, AlertDetail } from "components/Alert/Alert"; -import { Avatar } from "components/Avatar/Avatar"; import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"; import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"; import { PublishVersionData } from "pages/TemplateVersionEditorPage/types"; @@ -47,8 +46,9 @@ import { MONOSPACE_FONT_FAMILY } from "theme/constants"; import { Loader } from "components/Loader/Loader"; import { Topbar, + TopbarAvatar, TopbarButton, - TopbarDataGroup, + TopbarData, TopbarDivider, TopbarIconButton, } from "components/FullPageLayout/Topbar"; @@ -201,13 +201,8 @@ export const TemplateVersionEditor: FC = ({
- - + + = ({ {templateVersion.name} - +
Date: Fri, 15 Dec 2023 13:36:03 +0000 Subject: [PATCH 03/17] WIP: Refactor workspace header --- site/src/components/FullPageLayout/Topbar.tsx | 27 +++++- .../WorkspaceOutdatedTooltip.tsx | 28 ++++--- site/src/pages/WorkspacePage/Workspace.tsx | 83 ++++++++++++++++++- 3 files changed, 121 insertions(+), 17 deletions(-) diff --git a/site/src/components/FullPageLayout/Topbar.tsx b/site/src/components/FullPageLayout/Topbar.tsx index 8b74abd54db34..662e1d8f4bab2 100644 --- a/site/src/components/FullPageLayout/Topbar.tsx +++ b/site/src/components/FullPageLayout/Topbar.tsx @@ -1,8 +1,15 @@ +import { css } from "@emotion/css"; import Button, { ButtonProps } from "@mui/material/Button"; import IconButton, { IconButtonProps } from "@mui/material/IconButton"; import { useTheme } from "@mui/material/styles"; import { Avatar, AvatarProps } from "components/Avatar/Avatar"; -import { HTMLAttributes, forwardRef } from "react"; +import { + HTMLAttributes, + PropsWithChildren, + ReactElement, + cloneElement, + forwardRef, +} from "react"; export const Topbar = (props: HTMLAttributes) => { const theme = useTheme(); @@ -30,9 +37,10 @@ export const TopbarIconButton = forwardRef( {...props} size="small" css={{ - padding: "0 16px", + padding: 0, borderRadius: 0, height: 48, + width: 48, "& svg": { fontSize: 20, @@ -90,3 +98,18 @@ export const TopbarAvatar = (props: AvatarProps) => { /> ); }; + +export const TopbarIcon = ({ + children, + ...props +}: PropsWithChildren>) => { + const theme = useTheme(); + + return cloneElement( + children as ReactElement>, + { + ...props, + className: css({ fontSize: 16, color: theme.palette.text.secondary }), + }, + ); +}; diff --git a/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx b/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx index c93d91366ecfc..36fe34677fd22 100644 --- a/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx +++ b/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx @@ -28,15 +28,7 @@ interface TooltipProps { ariaLabel?: string; } -export const WorkspaceOutdatedTooltip: FC = ({ - onUpdateVersion, - ariaLabel, - latestVersionId, - templateName, -}) => { - const { data: activeVersion } = useQuery(templateVersion(latestVersionId)); - const theme = useTheme(); - +export const WorkspaceOutdatedTooltip: FC = (props) => { return ( = ({ iconStyles={styles.icon} buttonStyles={styles.button} > + + + ); +}; + +export const OutdatedTooltipContent = (props: TooltipProps) => { + const theme = useTheme(); + const { onUpdateVersion, ariaLabel, latestVersionId, templateName } = props; + const { data: activeVersion } = useQuery(templateVersion(latestVersionId)); + + return ( + <> {Language.outdatedLabel} {Language.versionTooltipText} @@ -81,7 +85,7 @@ export const WorkspaceOutdatedTooltip: FC = ({
- + {/* = ({ > {Language.updateVersionLabel} - - + */} + ); }; diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 08b278bec51a3..2a8a348de3f2e 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -2,7 +2,7 @@ import { type Interpolation, type Theme } from "@emotion/react"; import Button from "@mui/material/Button"; import AlertTitle from "@mui/material/AlertTitle"; import { type FC, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, Link as RouterLink } from "react-router-dom"; import dayjs from "dayjs"; import type * as TypesGen from "api/typesGenerated"; import { Alert, AlertDetail } from "components/Alert/Alert"; @@ -28,6 +28,28 @@ import { import { BuildsTable } from "./BuildsTable"; import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner"; import { WorkspaceStats } from "./WorkspaceStats"; +import { + Topbar, + TopbarAvatar, + TopbarData, + TopbarDivider, + TopbarIcon, + TopbarIconButton, +} from "components/FullPageLayout/Topbar"; +import Tooltip from "@mui/material/Tooltip"; +import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; +import { useTheme } from "@mui/material/styles"; +import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined"; +import LayersOutlined from "@mui/icons-material/LayersOutlined"; +import { + OutdatedTooltipContent, + WorkspaceOutdatedTooltip, +} from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; export type WorkspaceError = | "getBuildsError" @@ -114,6 +136,7 @@ export const Workspace: FC> = ({ hasMoreBuilds, canAutostart, }) => { + const theme = useTheme(); const navigate = useNavigate(); const { saveLocal, getLocal } = useLocalStorage(); @@ -166,7 +189,61 @@ export const Workspace: FC> = ({ return ( <> - + + + + + + + +
+ + + {workspace.name} + + + {workspace.template_display_name ?? workspace.template_name} + + + {workspace.outdated ? ( + + + + {workspace.latest_build.template_version_name} + + + + + + + ) : ( + + {workspace.latest_build.template_version_name} + + )} + + + + + + + {workspace.owner_name} + +
+
+ {/* > = ({ /> )} - +
*/} From 68c13aed6dd31dfb7f3e4c8e5455495ca0ccb3dc Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 19 Dec 2023 16:46:43 +0000 Subject: [PATCH 04/17] Improve outdated spacing --- .../WorkspaceOutdatedTooltip.tsx | 8 +++--- site/src/pages/WorkspacePage/Workspace.tsx | 25 +++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx b/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx index 7f8cdf6915f66..dc44b466068a5 100644 --- a/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx +++ b/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx @@ -42,12 +42,12 @@ export const WorkspaceOutdatedTooltip: FC = (props) => { - + ); }; -const OutdatedTooltipContent = (props: TooltipProps) => { +export const WorkspaceOutdatedTooltipContent = (props: TooltipProps) => { const popover = usePopover(); const { onUpdateVersion, ariaLabel, latestVersionId, templateName } = props; const { data: activeVersion } = useQuery({ @@ -62,7 +62,7 @@ const OutdatedTooltipContent = (props: TooltipProps) => { {Language.versionTooltipText}
-
+
New version
{activeVersion ? ( @@ -79,7 +79,7 @@ const OutdatedTooltipContent = (props: TooltipProps) => {
-
+
Message
{activeVersion ? ( diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 2a8a348de3f2e..0c619b8a63a31 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -42,7 +42,7 @@ import { useTheme } from "@mui/material/styles"; import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined"; import LayersOutlined from "@mui/icons-material/LayersOutlined"; import { - OutdatedTooltipContent, + WorkspaceOutdatedTooltipContent, WorkspaceOutdatedTooltip, } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; import { @@ -215,18 +215,23 @@ export const Workspace: FC> = ({ {workspace.outdated ? ( - + {workspace.latest_build.template_version_name} - - - + ) : ( From 379608584bb12ce2aed7faf2458ab718caec6992 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 20 Dec 2023 12:39:48 +0000 Subject: [PATCH 05/17] Add schedule controls --- site/src/pages/WorkspacePage/Workspace.tsx | 12 ++++++++++++ .../WorkspacePage/WorkspaceScheduleControls.tsx | 9 +++------ site/src/pages/WorkspacePage/WorkspaceStats.tsx | 3 +-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 4d8116edc2a84..5fbdf041d11f1 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -50,6 +50,8 @@ import { PopoverContent, PopoverTrigger, } from "components/Popover/Popover"; +import ScheduleOutlined from "@mui/icons-material/ScheduleOutlined"; +import { WorkspaceScheduleControls } from "./WorkspaceScheduleControls"; export type WorkspaceError = | "getBuildsError" @@ -239,6 +241,16 @@ export const Workspace: FC> = ({ {workspace.owner_name} + + + + + + +
{/* diff --git a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx index 1ef0fb78b0eb5..3327c3035ada6 100644 --- a/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceScheduleControls.tsx @@ -74,7 +74,7 @@ export const WorkspaceScheduleControls: FC = ({ ) : ( - {autostartDisplay(workspace.autostart_schedule)} + Starts at {autostartDisplay(workspace.autostart_schedule)} )} @@ -251,7 +251,7 @@ const AutoStopDisplay: FC = ({ workspace }) => { : undefined, })} > - {display.message} + Stop {display.message} ); @@ -268,6 +268,7 @@ const ScheduleSettingsLink = forwardRef( component={RouterLink} to="settings/schedule" css={{ + color: "inherit", "&:first-letter": { textTransform: "uppercase", }, @@ -310,10 +311,6 @@ const isShutdownSoon = (workspace: Workspace): boolean => { return diff < oneHour; }; -export const scheduleLabel = (workspace: Workspace) => { - return isWorkspaceOn(workspace) ? "Stops" : "Starts at"; -}; - const classNames = { paper: css` padding: 24px; diff --git a/site/src/pages/WorkspacePage/WorkspaceStats.tsx b/site/src/pages/WorkspacePage/WorkspaceStats.tsx index 8f76c671e2a6b..c1d701d64d585 100644 --- a/site/src/pages/WorkspacePage/WorkspaceStats.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceStats.tsx @@ -13,7 +13,6 @@ import { useQuery } from "react-query"; import _ from "lodash"; import { WorkspaceScheduleControls, - scheduleLabel, shouldDisplayScheduleControls, } from "./WorkspaceScheduleControls"; @@ -88,7 +87,7 @@ export const WorkspaceStats: FC = ({ {shouldDisplayScheduleControls(workspace) && ( Date: Wed, 20 Dec 2023 17:17:51 +0000 Subject: [PATCH 06/17] Use topbar buttons --- site/src/components/FullPageLayout/Topbar.tsx | 30 ++++---- site/src/pages/WorkspacePage/Workspace.tsx | 73 ++++++++++--------- .../BuildParametersPopover.tsx | 7 +- .../WorkspaceActions/Buttons.tsx | 64 ++++++---------- .../WorkspaceActions/WorkspaceActions.tsx | 20 +++-- 5 files changed, 91 insertions(+), 103 deletions(-) diff --git a/site/src/components/FullPageLayout/Topbar.tsx b/site/src/components/FullPageLayout/Topbar.tsx index 662e1d8f4bab2..54fb15ad299cf 100644 --- a/site/src/components/FullPageLayout/Topbar.tsx +++ b/site/src/components/FullPageLayout/Topbar.tsx @@ -51,19 +51,23 @@ export const TopbarIconButton = forwardRef( }, ) as typeof IconButton; -export const TopbarButton = (props: ButtonProps) => { - return ( -
+ +
+ + +
{/* diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx index f7756a2c8cb52..6a65af99ed6b0 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/BuildParametersPopover.tsx @@ -28,6 +28,7 @@ import { PopoverTrigger, usePopover, } from "components/Popover/Popover"; +import { TopbarButton } from "components/FullPageLayout/Topbar"; interface BuildParametersPopoverProps { workspace: Workspace; @@ -51,14 +52,14 @@ export const BuildParametersPopover: FC = ({ return ( - + + = ({ loading, }) => { return ( - } onClick={() => handleAction()} > {loading ? <>Updating… : <>Update…} - + ); }; @@ -44,14 +42,13 @@ export const ActivateButton: FC = ({ loading, }) => { return ( - } onClick={() => handleAction()} > {loading ? <>Activating… : "Activate"} - + ); }; @@ -77,15 +74,13 @@ export const StartButton: FC = ({ }} disabled={disabled} > - } onClick={() => handleAction()} - disabled={disabled} + disabled={disabled || loading} > {loading ? <>Starting… : "Start"} - + = ({ loading, }) => { return ( - } onClick={() => handleAction()} data-testid="workspace-stop-button" > {loading ? <>Stopping… : "Stop"} - + ); }; @@ -136,16 +130,14 @@ export const RestartButton: FC = ({ }} disabled={disabled} > - } onClick={() => handleAction()} data-testid="workspace-restart-button" - disabled={disabled} + disabled={disabled || loading} > {loading ? <>Restarting… : <>Restart…} - + = ({ export const CancelButton: FC = ({ handleAction }) => { return ( - + ); }; @@ -175,21 +167,9 @@ interface DisabledButtonProps { export const DisabledButton: FC = ({ label }) => { return ( - - ); -}; - -interface LoadingProps { - label: string; -} - -export const ActionLoadingButton: FC = ({ label }) => { - return ( - }> - {label} - + ); }; @@ -202,11 +182,11 @@ export const RetryButton: FC = ({ debug = false, }) => { return ( - + ); }; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index 9da528838608c..0032c933e3b03 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -1,12 +1,9 @@ import { type FC, type ReactNode, Fragment } from "react"; import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"; import { useWorkspaceDuplication } from "pages/CreateWorkspacePage/useWorkspaceDuplication"; - import { workspaceUpdatePolicy } from "utils/workspace"; import { type ActionType, abilitiesByWorkspaceStatus } from "./constants"; - import { - ActionLoadingButton, CancelButton, DisabledButton, StartButton, @@ -22,14 +19,14 @@ import DuplicateIcon from "@mui/icons-material/FileCopyOutlined"; import SettingsIcon from "@mui/icons-material/SettingsOutlined"; import HistoryIcon from "@mui/icons-material/HistoryOutlined"; import DeleteIcon from "@mui/icons-material/DeleteOutlined"; - import { MoreMenu, MoreMenuContent, MoreMenuItem, MoreMenuTrigger, - ThreeDotsButton, } from "components/MoreMenu/MoreMenu"; +import { TopbarIconButton } from "components/FullPageLayout/Topbar"; +import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined"; export interface WorkspaceActionsProps { workspace: Workspace; @@ -124,10 +121,10 @@ export const WorkspaceActions: FC = ({ tooltipText={tooltipText} /> ), - deleting: , + deleting: , canceling: , deleted: , - pending: , + pending: , activate: , activating: , retry: , @@ -136,7 +133,7 @@ export const WorkspaceActions: FC = ({ return (
{canBeUpdated && ( @@ -153,13 +150,14 @@ export const WorkspaceActions: FC = ({ - + > + + From c56d2f5f6fe490b08177aeeb89a490f596da7277 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Wed, 20 Dec 2023 19:24:08 +0000 Subject: [PATCH 07/17] Add dormant to topbar --- .../WorkspaceDeletion/DormantDeletionStat.tsx | 61 ------ .../WorkspaceDeletion/DormantDeletionText.tsx | 2 +- .../src/components/WorkspaceDeletion/index.ts | 2 +- site/src/pages/WorkspacePage/Workspace.tsx | 175 +++--------------- .../pages/WorkspacePage/WorkspaceStats.tsx | 2 - .../WorkspaceTopbar/DormantTopbarData.tsx | 49 +++++ .../WorkspaceTopbar/WorkspaceTopbar.tsx | 165 +++++++++++++++++ .../utils.test.ts => utils/dormant.test.ts} | 2 +- .../utils.ts => utils/dormant.ts} | 0 9 files changed, 238 insertions(+), 220 deletions(-) delete mode 100644 site/src/components/WorkspaceDeletion/DormantDeletionStat.tsx create mode 100644 site/src/pages/WorkspacePage/WorkspaceTopbar/DormantTopbarData.tsx create mode 100644 site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx rename site/src/{components/WorkspaceDeletion/utils.test.ts => utils/dormant.test.ts} (96%) rename site/src/{components/WorkspaceDeletion/utils.ts => utils/dormant.ts} (100%) diff --git a/site/src/components/WorkspaceDeletion/DormantDeletionStat.tsx b/site/src/components/WorkspaceDeletion/DormantDeletionStat.tsx deleted file mode 100644 index 7d881f8ca67a3..0000000000000 --- a/site/src/components/WorkspaceDeletion/DormantDeletionStat.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { StatsItem } from "components/Stats/Stats"; -import Link from "@mui/material/Link"; -import { type FC } from "react"; -import { Link as RouterLink } from "react-router-dom"; -import type { Workspace } from "api/typesGenerated"; -import { useDashboard } from "components/Dashboard/DashboardProvider"; -import { displayDormantDeletion } from "./utils"; - -interface DormantDeletionStatProps { - workspace: Workspace; -} - -export const DormantDeletionStat: FC = ({ - workspace, -}) => { - const { entitlements, experiments } = useDashboard(); - const allowAdvancedScheduling = - entitlements.features["advanced_template_scheduling"].enabled; - // This check can be removed when https://github.com/coder/coder/milestone/19 - // is merged up - const allowWorkspaceActions = experiments.includes("workspace_actions"); - - if ( - !displayDormantDeletion( - workspace, - allowAdvancedScheduling, - allowWorkspaceActions, - ) - ) { - return null; - } - - return ( - - {/* We check for string existence in the conditional */} - {new Date(workspace.deleting_at!).toLocaleString()} - - } - css={{ - "&.containerClass": { - flexDirection: "column", - gap: 0, - padding: 0, - - "& > span:first-of-type": { - fontSize: 12, - fontWeight: 500, - }, - }, - }} - /> - ); -}; diff --git a/site/src/components/WorkspaceDeletion/DormantDeletionText.tsx b/site/src/components/WorkspaceDeletion/DormantDeletionText.tsx index a6747ca9ce458..f6b1d393bb7f5 100644 --- a/site/src/components/WorkspaceDeletion/DormantDeletionText.tsx +++ b/site/src/components/WorkspaceDeletion/DormantDeletionText.tsx @@ -1,6 +1,6 @@ import { type FC } from "react"; import type { Workspace } from "api/typesGenerated"; -import { displayDormantDeletion } from "./utils"; +import { displayDormantDeletion } from "utils/dormant"; import { useDashboard } from "components/Dashboard/DashboardProvider"; interface DormantDeletionTextProps { diff --git a/site/src/components/WorkspaceDeletion/index.ts b/site/src/components/WorkspaceDeletion/index.ts index 3f9637cf4a655..acc6e16192277 100644 --- a/site/src/components/WorkspaceDeletion/index.ts +++ b/site/src/components/WorkspaceDeletion/index.ts @@ -1,3 +1,3 @@ -export * from "./DormantDeletionStat"; +export * from "../../pages/WorkspacePage/WorkspaceTopbar/DormantTopbarData"; export * from "./DormantDeletionText"; export * from "./DormantWorkspaceBanner"; diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index a11fbc80d6170..e61bfa130fb86 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -2,7 +2,7 @@ import { type Interpolation, type Theme } from "@emotion/react"; import Button from "@mui/material/Button"; import AlertTitle from "@mui/material/AlertTitle"; import { type FC, useEffect, useState } from "react"; -import { useNavigate, Link as RouterLink } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import dayjs from "dayjs"; import type * as TypesGen from "api/typesGenerated"; import { Alert, AlertDetail } from "components/Alert/Alert"; @@ -13,30 +13,13 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"; import { DormantWorkspaceBanner } from "components/WorkspaceDeletion"; import { AgentRow } from "components/Resources/AgentRow"; import { useLocalStorage } from "hooks"; -import { WorkspaceActions } from "pages/WorkspacePage/WorkspaceActions/WorkspaceActions"; import { ActiveTransition, WorkspaceBuildProgress, } from "./WorkspaceBuildProgress"; import { BuildsTable } from "./BuildsTable"; import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner"; -import { - Topbar, - TopbarAvatar, - TopbarData, - TopbarDivider, - TopbarIcon, - TopbarIconButton, -} from "components/FullPageLayout/Topbar"; -import Tooltip from "@mui/material/Tooltip"; -import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; -import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined"; -import { WorkspaceOutdatedTooltipContent } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; -import { Popover, PopoverTrigger } from "components/Popover/Popover"; -import ScheduleOutlined from "@mui/icons-material/ScheduleOutlined"; -import { WorkspaceScheduleControls } from "./WorkspaceScheduleControls"; -import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"; -import { Pill } from "components/Pill/Pill"; +import { WorkspaceTopbar } from "./WorkspaceTopbar/WorkspaceTopbar"; export type WorkspaceError = | "getBuildsError" @@ -168,141 +151,25 @@ export const Workspace: FC> = ({ return ( <> - - - - - - - -
- - - {workspace.name} - - - {workspace.template_display_name ?? workspace.template_name} - - - {workspace.outdated ? ( - - - - - - - ) : ( - - )} - - - - - - - {workspace.owner_name} - - - - - - - - -
- -
- - -
-
- {/* - - - {workspace.name} - -
- {workspace.name} - {workspace.owner_name} -
-
- - - - {canUpdateWorkspace && ( - - - - )} -
*/} + diff --git a/site/src/pages/WorkspacePage/WorkspaceStats.tsx b/site/src/pages/WorkspacePage/WorkspaceStats.tsx index c1d701d64d585..409c60fe93442 100644 --- a/site/src/pages/WorkspacePage/WorkspaceStats.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceStats.tsx @@ -7,7 +7,6 @@ import { getDisplayWorkspaceTemplateName } from "utils/workspace"; import type { Workspace } from "api/typesGenerated"; import { Stats, StatsItem } from "components/Stats/Stats"; import { WorkspaceStatusText } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"; -import { DormantDeletionStat } from "components/WorkspaceDeletion"; import { workspaceQuota } from "api/queries/workspaceQuota"; import { useQuery } from "react-query"; import _ from "lodash"; @@ -46,7 +45,6 @@ export const WorkspaceStats: FC = ({ label="Status" value={} /> - = ({ + workspace, +}) => { + const { entitlements, experiments } = useDashboard(); + const allowAdvancedScheduling = + entitlements.features["advanced_template_scheduling"].enabled; + // This check can be removed when https://github.com/coder/coder/milestone/19 + // is merged up + const allowWorkspaceActions = experiments.includes("workspace_actions"); + + if ( + !displayDormantDeletion( + workspace, + allowAdvancedScheduling, + allowWorkspaceActions, + ) + ) { + return null; + } + + return ( + + + + + + Deletion on {new Date(workspace.deleting_at!).toLocaleString()} + + + ); +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx new file mode 100644 index 0000000000000..81ebecbfeb84a --- /dev/null +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx @@ -0,0 +1,165 @@ +import { Link as RouterLink } from "react-router-dom"; +import type * as TypesGen from "api/typesGenerated"; +import { WorkspaceActions } from "pages/WorkspacePage/WorkspaceActions/WorkspaceActions"; +import { + Topbar, + TopbarAvatar, + TopbarData, + TopbarDivider, + TopbarIcon, + TopbarIconButton, +} from "components/FullPageLayout/Topbar"; +import Tooltip from "@mui/material/Tooltip"; +import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; +import PersonOutlineOutlined from "@mui/icons-material/PersonOutlineOutlined"; +import { WorkspaceOutdatedTooltipContent } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; +import { Popover, PopoverTrigger } from "components/Popover/Popover"; +import ScheduleOutlined from "@mui/icons-material/ScheduleOutlined"; +import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"; +import { Pill } from "components/Pill/Pill"; +import { WorkspaceScheduleControls } from "../WorkspaceScheduleControls"; +import { DormantTopbarData } from "./DormantTopbarData"; + +export type WorkspaceError = + | "getBuildsError" + | "buildError" + | "cancellationError"; + +export type WorkspaceErrors = Partial>; + +export interface WorkspaceProps { + handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; + handleStop: () => void; + handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; + handleDelete: () => void; + handleUpdate: () => void; + handleCancel: () => void; + handleSettings: () => void; + handleChangeVersion: () => void; + handleDormantActivate: () => void; + isUpdating: boolean; + isRestarting: boolean; + workspace: TypesGen.Workspace; + canUpdateWorkspace: boolean; + canChangeVersions: boolean; + canRetryDebugMode: boolean; + handleBuildRetry: () => void; + handleBuildRetryDebug: () => void; +} + +export const WorkspaceTopbar = (props: WorkspaceProps) => { + const { + handleStart, + handleStop, + handleRestart, + handleDelete, + handleUpdate, + handleCancel, + handleSettings, + handleChangeVersion, + handleDormantActivate, + workspace, + isUpdating, + isRestarting, + canUpdateWorkspace, + canChangeVersions, + canRetryDebugMode, + handleBuildRetry, + handleBuildRetryDebug, + } = props; + + return ( + + + + + + + +
+ + + {workspace.name} + + + {workspace.template_display_name ?? workspace.template_name} + + + {workspace.outdated ? ( + + + + + + + ) : ( + + )} + + + + + + + {workspace.owner_name} + + + + + + + + + + +
+ +
+ + +
+
+ ); +}; diff --git a/site/src/components/WorkspaceDeletion/utils.test.ts b/site/src/utils/dormant.test.ts similarity index 96% rename from site/src/components/WorkspaceDeletion/utils.test.ts rename to site/src/utils/dormant.test.ts index caca6c5661993..ae02ef017690c 100644 --- a/site/src/components/WorkspaceDeletion/utils.test.ts +++ b/site/src/utils/dormant.test.ts @@ -1,6 +1,6 @@ import * as TypesGen from "api/typesGenerated"; import * as Mocks from "testHelpers/entities"; -import { displayDormantDeletion } from "./utils"; +import { displayDormantDeletion } from "./dormant"; describe("displayDormantDeletion", () => { const today = new Date(); diff --git a/site/src/components/WorkspaceDeletion/utils.ts b/site/src/utils/dormant.ts similarity index 100% rename from site/src/components/WorkspaceDeletion/utils.ts rename to site/src/utils/dormant.ts From 90c3fc95a4eca72c63fd0c326df38cb6ad50e6b1 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 21 Dec 2023 14:14:45 +0000 Subject: [PATCH 08/17] Add quota and minor accessibility improvements --- site/src/components/FullPageLayout/Topbar.tsx | 34 +++++++++------- site/src/pages/WorkspacePage/Workspace.tsx | 1 - .../WorkspaceTopbar/WorkspaceTopbar.tsx | 39 +++++++++++++++++-- 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/site/src/components/FullPageLayout/Topbar.tsx b/site/src/components/FullPageLayout/Topbar.tsx index 54fb15ad299cf..b0ffab64b6e07 100644 --- a/site/src/components/FullPageLayout/Topbar.tsx +++ b/site/src/components/FullPageLayout/Topbar.tsx @@ -4,6 +4,7 @@ import IconButton, { IconButtonProps } from "@mui/material/IconButton"; import { useTheme } from "@mui/material/styles"; import { Avatar, AvatarProps } from "components/Avatar/Avatar"; import { + ForwardedRef, HTMLAttributes, PropsWithChildren, ReactElement, @@ -103,17 +104,24 @@ export const TopbarAvatar = (props: AvatarProps) => { ); }; -export const TopbarIcon = ({ - children, - ...props -}: PropsWithChildren>) => { - const theme = useTheme(); +type TopbarIconProps = PropsWithChildren>; - return cloneElement( - children as ReactElement>, - { - ...props, - className: css({ fontSize: 16, color: theme.palette.text.secondary }), - }, - ); -}; +export const TopbarIcon = forwardRef( + (props: TopbarIconProps, ref) => { + const { children, ...restProps } = props; + const theme = useTheme(); + + return cloneElement( + children as ReactElement< + HTMLAttributes & { + ref: ForwardedRef; + } + >, + { + ...restProps, + ref, + className: css({ fontSize: 16, color: theme.palette.text.secondary }), + }, + ); + }, +); diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index e61bfa130fb86..b7d966603e457 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -51,7 +51,6 @@ export interface WorkspaceProps { buildInfo?: TypesGen.BuildInfoResponse; sshPrefix?: string; template?: TypesGen.Template; - quotaBudget?: number; canRetryDebugMode: boolean; handleBuildRetry: () => void; handleBuildRetryDebug: () => void; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx index 81ebecbfeb84a..066e8ba567f69 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx @@ -19,6 +19,10 @@ import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceS import { Pill } from "components/Pill/Pill"; import { WorkspaceScheduleControls } from "../WorkspaceScheduleControls"; import { DormantTopbarData } from "./DormantTopbarData"; +import { workspaceQuota } from "api/queries/workspaceQuota"; +import { useQuery } from "react-query"; +import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined"; +import { useTheme } from "@mui/material/styles"; export type WorkspaceError = | "getBuildsError" @@ -67,6 +71,14 @@ export const WorkspaceTopbar = (props: WorkspaceProps) => { handleBuildRetry, handleBuildRetryDebug, } = props; + const theme = useTheme(); + + // Quota + const hasDailyCost = workspace.latest_build.daily_cost > 0; + const { data: quota } = useQuery({ + ...workspaceQuota(workspace.owner_name), + enabled: hasDailyCost, + }); return ( @@ -113,9 +125,11 @@ export const WorkspaceTopbar = (props: WorkspaceProps) => { - - - + + + + + {workspace.owner_name} @@ -123,13 +137,30 @@ export const WorkspaceTopbar = (props: WorkspaceProps) => { - + + + + + {quota && ( + + + + + + + {workspace.latest_build.daily_cost}{" "} + + credits of + {" "} + {quota.budget} + + )}
Date: Thu, 21 Dec 2023 14:31:53 +0000 Subject: [PATCH 09/17] Make template navigable --- .../WorkspaceTopbar/WorkspaceTopbar.tsx | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx index 066e8ba567f69..399612d11ed21 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx @@ -23,6 +23,8 @@ import { workspaceQuota } from "api/queries/workspaceQuota"; import { useQuery } from "react-query"; import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined"; import { useTheme } from "@mui/material/styles"; +import InfoOutlined from "@mui/icons-material/InfoOutlined"; +import Link from "@mui/material/Link"; export type WorkspaceError = | "getBuildsError" @@ -100,17 +102,36 @@ export const WorkspaceTopbar = (props: WorkspaceProps) => { {workspace.name} - + {workspace.template_display_name ?? workspace.template_name} - + {workspace.outdated ? ( - + {/* Added to give some bottom space from the popover content */} +
+ + } + text={ + + {workspace.latest_build.template_version_name} + + } + /> +
{ - {workspace.latest_build.daily_cost}{" "} - - credits of - {" "} - {quota.budget} + + {workspace.latest_build.daily_cost}{" "} + + credits of + {" "} + {quota.budget} + )}
From f37a88d2c251299571d0849e164f5d082a17a45f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 21 Dec 2023 14:44:52 +0000 Subject: [PATCH 10/17] Make visual adjustments --- site/src/components/FullPageLayout/Topbar.tsx | 2 +- .../WorkspaceActions/BuildParametersPopover.tsx | 2 +- .../WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/site/src/components/FullPageLayout/Topbar.tsx b/site/src/components/FullPageLayout/Topbar.tsx index b0ffab64b6e07..7ab025b99567b 100644 --- a/site/src/components/FullPageLayout/Topbar.tsx +++ b/site/src/components/FullPageLayout/Topbar.tsx @@ -19,7 +19,7 @@ export const Topbar = (props: HTMLAttributes) => {
= ({ data-testid="build-parameters-button" disabled={disabled} color="neutral" - css={{ paddingLeft: 0, paddingRight: 0 }} + css={{ paddingLeft: 0, paddingRight: 0, minWidth: "28px !important" }} > diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx index 399612d11ed21..6e68992632286 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx @@ -92,10 +92,13 @@ export const WorkspaceTopbar = (props: WorkspaceProps) => {
From 26d6c097bdcda54fcd7d8002e14ceda1aad51989 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 22 Dec 2023 11:40:16 +0000 Subject: [PATCH 11/17] Refactor icon colors --- site/src/components/FullPageLayout/Topbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/FullPageLayout/Topbar.tsx b/site/src/components/FullPageLayout/Topbar.tsx index 7ab025b99567b..a882825d199fc 100644 --- a/site/src/components/FullPageLayout/Topbar.tsx +++ b/site/src/components/FullPageLayout/Topbar.tsx @@ -120,7 +120,7 @@ export const TopbarIcon = forwardRef( { ...restProps, ref, - className: css({ fontSize: 16, color: theme.palette.text.secondary }), + className: css({ fontSize: 16, color: theme.palette.text.disabled }), }, ); }, From 651b800e2762b123e551d6b11bf21708b5156878 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 22 Dec 2023 13:30:21 +0000 Subject: [PATCH 12/17] Add storybook --- site/.storybook/preview.jsx | 41 +++--- site/src/@types/storybook.d.ts | 11 ++ site/src/api/queries/workspaceQuota.ts | 2 +- .../src/components/WorkspaceDeletion/index.ts | 1 - .../WorkspacePage/WorkspaceStats.stories.tsx | 53 ------- .../pages/WorkspacePage/WorkspaceStats.tsx | 137 ------------------ .../WorkspaceTopbar/DormantTopbarData.tsx | 49 ------- .../WorkspaceTopbar.stories.tsx | 82 +++++++++++ .../WorkspaceTopbar/WorkspaceTopbar.tsx | 64 ++++++-- site/src/testHelpers/storybook.tsx | 48 ++++++ 10 files changed, 214 insertions(+), 274 deletions(-) create mode 100644 site/src/@types/storybook.d.ts delete mode 100644 site/src/pages/WorkspacePage/WorkspaceStats.stories.tsx delete mode 100644 site/src/pages/WorkspacePage/WorkspaceStats.tsx delete mode 100644 site/src/pages/WorkspacePage/WorkspaceTopbar/DormantTopbarData.tsx create mode 100644 site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.stories.tsx create mode 100644 site/src/testHelpers/storybook.tsx diff --git a/site/.storybook/preview.jsx b/site/.storybook/preview.jsx index 05d5a340747c5..21d8d64e79ae7 100644 --- a/site/.storybook/preview.jsx +++ b/site/.storybook/preview.jsx @@ -17,7 +17,6 @@ export const decorators = [ (Story, context) => { const selectedTheme = DecoratorHelpers.pluckThemeFromContext(context); const { themeOverride } = DecoratorHelpers.useThemeParameters(); - const selected = themeOverride || selectedTheme || "dark"; return ( @@ -39,23 +38,7 @@ export const decorators = [ ); }, - (Story) => { - return ( - - - - ); - }, + withQuery, ]; export const parameters = { @@ -89,3 +72,25 @@ export const parameters = { }, }, }; + +function withQuery(Story, { parameters }) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: Infinity, + }, + }, + }); + + if (parameters.queries) { + parameters.queries.forEach((query) => { + queryClient.setQueryData(query.key, query.data); + }); + } + + return ( + + + + ); +} diff --git a/site/src/@types/storybook.d.ts b/site/src/@types/storybook.d.ts new file mode 100644 index 0000000000000..8a5b490987860 --- /dev/null +++ b/site/src/@types/storybook.d.ts @@ -0,0 +1,11 @@ +import * as _storybook_types from "@storybook/react"; +import { Experiments, FeatureName } from "api/typesGenerated"; +import { QueryKey } from "react-query"; + +declare module "@storybook/react" { + interface Parameters { + features?: FeatureName[]; + experiments?: Experiments; + queries?: { key: QueryKey; data: unknown }[]; + } +} diff --git a/site/src/api/queries/workspaceQuota.ts b/site/src/api/queries/workspaceQuota.ts index b8d627783838b..32c94eeb5ad39 100644 --- a/site/src/api/queries/workspaceQuota.ts +++ b/site/src/api/queries/workspaceQuota.ts @@ -1,6 +1,6 @@ import * as API from "api/api"; -const getWorkspaceQuotaQueryKey = (username: string) => [ +export const getWorkspaceQuotaQueryKey = (username: string) => [ username, "workspaceQuota", ]; diff --git a/site/src/components/WorkspaceDeletion/index.ts b/site/src/components/WorkspaceDeletion/index.ts index acc6e16192277..20c01e31d8f09 100644 --- a/site/src/components/WorkspaceDeletion/index.ts +++ b/site/src/components/WorkspaceDeletion/index.ts @@ -1,3 +1,2 @@ -export * from "../../pages/WorkspacePage/WorkspaceTopbar/DormantTopbarData"; export * from "./DormantDeletionText"; export * from "./DormantWorkspaceBanner"; diff --git a/site/src/pages/WorkspacePage/WorkspaceStats.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceStats.stories.tsx deleted file mode 100644 index de444ba38960f..0000000000000 --- a/site/src/pages/WorkspacePage/WorkspaceStats.stories.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; -import { - MockWorkspace, - MockAppearanceConfig, - MockBuildInfo, - MockEntitlementsWithScheduling, - MockExperiments, -} from "testHelpers/entities"; -import { WorkspaceStats } from "./WorkspaceStats"; -import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"; - -const MockedAppearance = { - config: MockAppearanceConfig, - isPreview: false, - setPreview: () => {}, -}; - -const meta: Meta = { - title: "pages/WorkspacePage/WorkspaceStats", - component: WorkspaceStats, - decorators: [ - (Story) => ( - - - - ), - ], -}; - -export default meta; -type Story = StoryObj; - -export const Example: Story = { - args: { - workspace: MockWorkspace, - }, -}; - -export const Outdated: Story = { - args: { - workspace: { - ...MockWorkspace, - outdated: true, - }, - }, -}; diff --git a/site/src/pages/WorkspacePage/WorkspaceStats.tsx b/site/src/pages/WorkspacePage/WorkspaceStats.tsx deleted file mode 100644 index 409c60fe93442..0000000000000 --- a/site/src/pages/WorkspacePage/WorkspaceStats.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { type Interpolation, type Theme } from "@emotion/react"; -import Link from "@mui/material/Link"; -import { WorkspaceOutdatedTooltip } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; -import { type FC } from "react"; -import { Link as RouterLink } from "react-router-dom"; -import { getDisplayWorkspaceTemplateName } from "utils/workspace"; -import type { Workspace } from "api/typesGenerated"; -import { Stats, StatsItem } from "components/Stats/Stats"; -import { WorkspaceStatusText } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"; -import { workspaceQuota } from "api/queries/workspaceQuota"; -import { useQuery } from "react-query"; -import _ from "lodash"; -import { - WorkspaceScheduleControls, - shouldDisplayScheduleControls, -} from "./WorkspaceScheduleControls"; - -const Language = { - workspaceDetails: "Workspace Details", - templateLabel: "Template", - costLabel: "Daily cost", - updatePolicy: "Update policy", -}; - -export interface WorkspaceStatsProps { - workspace: Workspace; - canUpdateWorkspace: boolean; - handleUpdate: () => void; -} - -export const WorkspaceStats: FC = ({ - workspace, - canUpdateWorkspace, - handleUpdate, -}) => { - const displayTemplateName = getDisplayWorkspaceTemplateName(workspace); - const quotaQuery = useQuery(workspaceQuota(workspace.owner_name)); - const quotaBudget = quotaQuery.data?.budget; - - return ( - <> - - } - /> - - {displayTemplateName} - - } - /> - - - - {workspace.latest_build.template_version_name} - - - {workspace.outdated && ( - - )} - - } - /> - - {shouldDisplayScheduleControls(workspace) && ( - - } - /> - )} - {workspace.latest_build.daily_cost > 0 && ( - - )} - - - ); -}; - -const styles = { - stats: (theme) => ({ - padding: 0, - border: 0, - gap: 48, - rowGap: 24, - flex: 1, - - [theme.breakpoints.down("md")]: { - display: "flex", - flexDirection: "column", - alignItems: "flex-start", - gap: 8, - }, - }), - - statsItem: { - flexDirection: "column", - gap: 0, - padding: 0, - - "& > span:first-of-type": { - fontSize: 12, - fontWeight: 500, - }, - }, -} satisfies Record>; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar/DormantTopbarData.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar/DormantTopbarData.tsx deleted file mode 100644 index 6665a5d4ff449..0000000000000 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar/DormantTopbarData.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import Link from "@mui/material/Link"; -import { type FC } from "react"; -import { Link as RouterLink } from "react-router-dom"; -import type { Workspace } from "api/typesGenerated"; -import { useDashboard } from "components/Dashboard/DashboardProvider"; -import { displayDormantDeletion } from "utils/dormant"; -import { TopbarData, TopbarIcon } from "components/FullPageLayout/Topbar"; -import DeleteOutline from "@mui/icons-material/DeleteOutline"; - -interface DormantTopbarDataProps { - workspace: Workspace; -} - -export const DormantTopbarData: FC = ({ - workspace, -}) => { - const { entitlements, experiments } = useDashboard(); - const allowAdvancedScheduling = - entitlements.features["advanced_template_scheduling"].enabled; - // This check can be removed when https://github.com/coder/coder/milestone/19 - // is merged up - const allowWorkspaceActions = experiments.includes("workspace_actions"); - - if ( - !displayDormantDeletion( - workspace, - allowAdvancedScheduling, - allowWorkspaceActions, - ) - ) { - return null; - } - - return ( - - - - - - Deletion on {new Date(workspace.deleting_at!).toLocaleString()} - - - ); -}; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.stories.tsx new file mode 100644 index 0000000000000..aec6c8fe2dc7b --- /dev/null +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.stories.tsx @@ -0,0 +1,82 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { MockUser, MockWorkspace } from "testHelpers/entities"; +import { WorkspaceTopbar } from "./WorkspaceTopbar"; +import { withDashboardProvider } from "testHelpers/storybook"; +import { addDays } from "date-fns"; +import { getWorkspaceQuotaQueryKey } from "api/queries/workspaceQuota"; + +// We want a workspace without a deadline to not pollute the screenshot +const baseWorkspace = { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + deadline: undefined, + }, +}; + +const meta: Meta = { + title: "pages/WorkspacePage/WorkspaceTopbar", + component: WorkspaceTopbar, + decorators: [withDashboardProvider], + args: { + workspace: baseWorkspace, + }, + parameters: { + layout: "fullscreen", + features: ["advanced_template_scheduling"], + experiments: ["workspace_actions"], + }, +}; + +export default meta; +type Story = StoryObj; + +export const Example: Story = {}; + +export const Outdated: Story = { + args: { + workspace: { + ...MockWorkspace, + outdated: true, + }, + }, +}; + +export const WithDeadline: Story = { + args: { + workspace: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + deadline: MockWorkspace.latest_build.deadline, + }, + }, + }, +}; + +export const Dormant: Story = { + args: { + workspace: { + ...baseWorkspace, + deleting_at: addDays(new Date(), 7).toISOString(), + latest_build: { + ...baseWorkspace.latest_build, + status: "failed", + }, + }, + }, +}; + +export const Quota: Story = { + parameters: { + queries: [ + { + key: getWorkspaceQuotaQueryKey(MockUser.username), + data: { + credits_consumed: 2, + budget: 40, + }, + }, + ], + }, +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx index 6e68992632286..02dc2dc3957f8 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx @@ -17,14 +17,19 @@ import { Popover, PopoverTrigger } from "components/Popover/Popover"; import ScheduleOutlined from "@mui/icons-material/ScheduleOutlined"; import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"; import { Pill } from "components/Pill/Pill"; -import { WorkspaceScheduleControls } from "../WorkspaceScheduleControls"; -import { DormantTopbarData } from "./DormantTopbarData"; +import { + WorkspaceScheduleControls, + shouldDisplayScheduleControls, +} from "../WorkspaceScheduleControls"; import { workspaceQuota } from "api/queries/workspaceQuota"; import { useQuery } from "react-query"; import MonetizationOnOutlined from "@mui/icons-material/MonetizationOnOutlined"; import { useTheme } from "@mui/material/styles"; import InfoOutlined from "@mui/icons-material/InfoOutlined"; import Link from "@mui/material/Link"; +import { useDashboard } from "components/Dashboard/DashboardProvider"; +import { displayDormantDeletion } from "utils/dormant"; +import DeleteOutline from "@mui/icons-material/DeleteOutline"; export type WorkspaceError = | "getBuildsError" @@ -82,6 +87,19 @@ export const WorkspaceTopbar = (props: WorkspaceProps) => { enabled: hasDailyCost, }); + // Dormant + const { entitlements, experiments } = useDashboard(); + const allowAdvancedScheduling = + entitlements.features["advanced_template_scheduling"].enabled; + // This check can be removed when https://github.com/coder/coder/milestone/19 + // is merged up + const allowWorkspaceActions = experiments.includes("workspace_actions"); + const shouldDisplayDormantData = displayDormantDeletion( + workspace, + allowAdvancedScheduling, + allowWorkspaceActions, + ); + return ( @@ -117,7 +135,7 @@ export const WorkspaceTopbar = (props: WorkspaceProps) => { {/* Added to give some bottom space from the popover content */} -
+
{ {workspace.owner_name} - + {shouldDisplayDormantData && ( + + + + + + Deletion on {new Date(workspace.deleting_at!).toLocaleString()} + + + )} - - - - - - - - + {shouldDisplayScheduleControls(workspace) && ( + + + + + + + + + )} {quota && ( diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx new file mode 100644 index 0000000000000..bd7d81a14f275 --- /dev/null +++ b/site/src/testHelpers/storybook.tsx @@ -0,0 +1,48 @@ +import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"; +import { + MockAppearanceConfig, + MockBuildInfo, + MockEntitlements, +} from "./entities"; +import { FC } from "react"; +import { StoryContext } from "@storybook/react"; +import * as _storybook_types from "@storybook/react"; +import { Entitlements } from "api/typesGenerated"; +import { withDefaultFeatures } from "api/api"; + +export const withDashboardProvider = ( + Story: FC, + { parameters }: StoryContext, +) => { + const { features = [], experiments = [] } = parameters; + + const entitlements: Entitlements = { + ...MockEntitlements, + features: withDefaultFeatures( + features.reduce( + (acc, feature) => { + acc[feature] = { enabled: true, entitlement: "entitled" }; + return acc; + }, + {} as Entitlements["features"], + ), + ), + }; + + return ( + {}, + }, + }} + > + + + ); +}; From d5510b2d907a9a812e9ded9b5a998f0584522db8 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Fri, 22 Dec 2023 13:32:42 +0000 Subject: [PATCH 13/17] Refactor storybook names --- .../WorkspaceTopbar.stories.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.stories.tsx index aec6c8fe2dc7b..5490bf2a3c096 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.stories.tsx @@ -42,32 +42,32 @@ export const Outdated: Story = { }, }; -export const WithDeadline: Story = { +export const Dormant: Story = { args: { workspace: { - ...MockWorkspace, + ...baseWorkspace, + deleting_at: addDays(new Date(), 7).toISOString(), latest_build: { - ...MockWorkspace.latest_build, - deadline: MockWorkspace.latest_build.deadline, + ...baseWorkspace.latest_build, + status: "failed", }, }, }, }; -export const Dormant: Story = { +export const WithDeadline: Story = { args: { workspace: { - ...baseWorkspace, - deleting_at: addDays(new Date(), 7).toISOString(), + ...MockWorkspace, latest_build: { - ...baseWorkspace.latest_build, - status: "failed", + ...MockWorkspace.latest_build, + deadline: MockWorkspace.latest_build.deadline, }, }, }, }; -export const Quota: Story = { +export const WithQuota: Story = { parameters: { queries: [ { From 19832ee79688981236f50c3c376b025a8e6a4231 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 2 Jan 2024 12:39:19 +0000 Subject: [PATCH 14/17] fix pill usage --- .../WorkspaceTopbar/WorkspaceTopbar.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx index 02dc2dc3957f8..f87eb238f7c8d 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar/WorkspaceTopbar.tsx @@ -146,12 +146,11 @@ export const WorkspaceTopbar = (props: WorkspaceProps) => { }} /> } - text={ - - {workspace.latest_build.template_version_name} - - } - /> + > + + {workspace.latest_build.template_version_name} + +
{ /> ) : ( - + {workspace.latest_build.template_version_name} )} From b8e80d00ff23bb3aeaf99fd801c55d16b94d4dd6 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 2 Jan 2024 13:02:33 +0000 Subject: [PATCH 15/17] add data-testid to workspace badge --- .../WorkspaceStatusBadge.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx index 73c53e72b84e4..798b70bfde592 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -47,13 +47,25 @@ export const WorkspaceStatusBadge: FC = ({ } placement="top" > - + {text} - + {text} From 30f2a8cda4b32de7aa3b4bcb6ee884fb427e3ffb Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 2 Jan 2024 13:35:12 +0000 Subject: [PATCH 16/17] Fix workspace status helper --- site/e2e/helpers.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index cd61b89305fd6..b25d76dc298eb 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -47,12 +47,9 @@ export const createWorkspace = async ( await expect(page).toHaveURL("/@admin/" + name); - await page.waitForSelector( - "span[data-testid='build-status'] >> text=Running", - { - state: "visible", - }, - ); + await page.waitForSelector("*[data-testid='build-status'] >> text=Running", { + state: "visible", + }); return name; }; From 446bb769dcc86be66d97f8318ecf9bae75abcaf9 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 2 Jan 2024 15:15:44 +0000 Subject: [PATCH 17/17] remove span depedency from build-status --- site/e2e/helpers.ts | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index b25d76dc298eb..77960b32234d0 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -194,12 +194,9 @@ export const stopWorkspace = async (page: Page, workspaceName: string) => { await page.getByTestId("workspace-stop-button").click(); - await page.waitForSelector( - "span[data-testid='build-status'] >> text=Stopped", - { - state: "visible", - }, - ); + await page.waitForSelector("*[data-testid='build-status'] >> text=Stopped", { + state: "visible", + }); }; export const buildWorkspaceWithParameters = async ( @@ -222,12 +219,9 @@ export const buildWorkspaceWithParameters = async ( await page.getByTestId("confirm-button").click(); } - await page.waitForSelector( - "span[data-testid='build-status'] >> text=Running", - { - state: "visible", - }, - ); + await page.waitForSelector("*[data-testid='build-status'] >> text=Running", { + state: "visible", + }); }; // startAgent runs the coder agent with the provided token. @@ -769,12 +763,9 @@ export const updateWorkspace = async ( await fillParameters(page, richParameters, buildParameters); await page.getByTestId("form-submit").click(); - await page.waitForSelector( - "span[data-testid='build-status'] >> text=Running", - { - state: "visible", - }, - ); + await page.waitForSelector("*[data-testid='build-status'] >> text=Running", { + state: "visible", + }); }; export const updateWorkspaceParameters = async ( @@ -793,10 +784,7 @@ export const updateWorkspaceParameters = async ( await fillParameters(page, richParameters, buildParameters); await page.getByTestId("form-submit").click(); - await page.waitForSelector( - "span[data-testid='build-status'] >> text=Running", - { - state: "visible", - }, - ); + await page.waitForSelector("*[data-testid='build-status'] >> text=Running", { + state: "visible", + }); };