From 0b286284ebda1e5a88b289695d9b59b41abce031 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 9 Jan 2024 12:12:03 +0000 Subject: [PATCH 01/22] Refactor outdated warning --- site/src/api/queries/workspaceQuota.ts | 2 +- .../pages/WorkspacePage/Workspace.stories.tsx | 14 ++++- site/src/pages/WorkspacePage/Workspace.tsx | 37 +++---------- .../WorkspacePage/WorkspaceNotifications.tsx | 52 +++++++++++++++++++ .../WorkspacePage/WorkspaceReadyPage.tsx | 10 +--- 5 files changed, 74 insertions(+), 41 deletions(-) create mode 100644 site/src/pages/WorkspacePage/WorkspaceNotifications.tsx diff --git a/site/src/api/queries/workspaceQuota.ts b/site/src/api/queries/workspaceQuota.ts index 32c94eeb5ad39..f43adf616688e 100644 --- a/site/src/api/queries/workspaceQuota.ts +++ b/site/src/api/queries/workspaceQuota.ts @@ -12,7 +12,7 @@ export const workspaceQuota = (username: string) => { }; }; -const getWorkspaceResolveAutostartQueryKey = (workspaceId: string) => [ +export const getWorkspaceResolveAutostartQueryKey = (workspaceId: string) => [ workspaceId, "workspaceResolveAutostart", ]; diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index b156daeb4c6f6..35744ef74c590 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -9,6 +9,7 @@ import EventSource from "eventsourcemock"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"; import { WorkspaceBuildLogsSection } from "pages/WorkspacePage/WorkspaceBuildLogsSection"; +import { getWorkspaceResolveAutostartQueryKey } from "api/queries/workspaceQuota"; const MockedAppearance = { config: Mocks.MockAppearanceConfig, @@ -196,9 +197,20 @@ export const Outdated: Story = { export const CantAutostart: Story = { args: { ...Running.args, - canAutostart: false, workspace: Mocks.MockOutdatedRunningWorkspaceRequireActiveVersion, }, + parameters: { + queries: [ + { + key: getWorkspaceResolveAutostartQueryKey( + Mocks.MockOutdatedRunningWorkspaceRequireActiveVersion.id, + ), + data: { + parameter_mismatch: false, + }, + }, + ], + }, }; export const GetBuildsError: Story = { diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index e19f485aa903a..ad530d4fd4aad 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -26,6 +26,7 @@ import { SidebarIconButton } from "components/FullPageLayout/Sidebar"; import HubOutlined from "@mui/icons-material/HubOutlined"; import { ResourcesSidebar } from "./ResourcesSidebar"; import { ResourceCard } from "components/Resources/ResourceCard"; +import { WorkspaceNotifications } from "./WorkspaceNotifications"; export type WorkspaceError = | "getBuildsError" @@ -48,7 +49,6 @@ export interface WorkspaceProps { isRestarting: boolean; workspace: TypesGen.Workspace; canUpdateWorkspace: boolean; - updateMessage?: string; canChangeVersions: boolean; hideSSHButton?: boolean; hideVSCodeDesktopButton?: boolean; @@ -60,7 +60,7 @@ export interface WorkspaceProps { handleBuildRetry: () => void; handleBuildRetryDebug: () => void; buildLogs?: React.ReactNode; - canAutostart: boolean; + latestVersion?: TypesGen.TemplateVersion; } /** @@ -80,7 +80,6 @@ export const Workspace: FC = ({ isUpdating, isRestarting, canUpdateWorkspace, - updateMessage, canChangeVersions, workspaceErrors, hideSSHButton, @@ -92,7 +91,7 @@ export const Workspace: FC = ({ handleBuildRetry, handleBuildRetryDebug, buildLogs, - canAutostart, + latestVersion, }) => { const navigate = useNavigate(); const { saveLocal, getLocal } = useLocalStorage(); @@ -135,13 +134,6 @@ export const Workspace: FC = ({ }; }, [workspace, now, showAlertPendingInQueue]); - const updateRequired = - (workspace.template_require_active_version || - workspace.automatic_updates === "always") && - workspace.outdated; - const autoStartFailing = workspace.autostart_schedule && !canAutostart; - const requiresManualUpdate = updateRequired && autoStartFailing; - const transitionStats = template !== undefined ? ActiveTransition(template, workspace) : undefined; @@ -244,25 +236,10 @@ export const Workspace: FC = ({
- {workspace.outdated && - (requiresManualUpdate ? ( - - - Autostart has been disabled for your workspace. - - - Autostart is unable to automatically update your workspace. - Manually update your workspace to reenable Autostart. - - - ) : ( - - - An update is available for your workspace - - {updateMessage && {updateMessage}} - - ))} + {Boolean(workspaceErrors.buildError) && ( diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx new file mode 100644 index 0000000000000..46e7371175e4c --- /dev/null +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx @@ -0,0 +1,52 @@ +import AlertTitle from "@mui/material/AlertTitle"; +import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; +import { TemplateVersion, Workspace } from "api/typesGenerated"; +import { Alert, AlertDetail } from "components/Alert/Alert"; +import { FC } from "react"; +import { useQuery } from "react-query"; + +type WorkspaceNotificationsProps = { + workspace: Workspace; + latestVersion?: TemplateVersion; +}; + +export const WorkspaceNotifications: FC = ( + props, +) => { + const { workspace, latestVersion } = props; + + // Outdated + const canAutostartResponse = useQuery( + workspaceResolveAutostart(workspace.id), + ); + const canAutostart = !canAutostartResponse.data?.parameter_mismatch ?? false; + const updateRequired = + (workspace.template_require_active_version || + workspace.automatic_updates === "always") && + workspace.outdated; + const autoStartFailing = workspace.autostart_schedule && !canAutostart; + const requiresManualUpdate = updateRequired && autoStartFailing; + + return ( + <> + {workspace.outdated && + latestVersion && + (requiresManualUpdate ? ( + + + Autostart has been disabled for your workspace. + + + Autostart is unable to automatically update your workspace. + Manually update your workspace to reenable Autostart. + + + ) : ( + + An update is available for your workspace + {latestVersion.message} + + ))} + + ); +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index f267d1689f1fd..ea3adf928ddb0 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -33,7 +33,6 @@ import { getErrorMessage } from "api/errors"; import { displayError } from "components/GlobalSnackbar/utils"; import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment"; import { WorkspacePermissions } from "./permissions"; -import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; import dayjs from "dayjs"; @@ -83,12 +82,6 @@ export const WorkspaceReadyPage = ({ mutationFn: restartWorkspace, }); - // Auto start - const canAutostartResponse = useQuery( - workspaceResolveAutostart(workspace.id), - ); - const canAutostart = !canAutostartResponse.data?.parameter_mismatch ?? false; - // SSH Prefix const sshPrefixQuery = useQuery(deploymentSSHConfig()); @@ -225,7 +218,7 @@ export const WorkspaceReadyPage = ({ } }} canUpdateWorkspace={canUpdateWorkspace} - updateMessage={latestVersion?.message} + latestVersion={latestVersion} canChangeVersions={canChangeVersions} hideSSHButton={featureVisibility["browser_only"]} hideVSCodeDesktopButton={featureVisibility["browser_only"]} @@ -246,7 +239,6 @@ export const WorkspaceReadyPage = ({ ) } - canAutostart={canAutostart} /> Date: Tue, 9 Jan 2024 12:23:29 +0000 Subject: [PATCH 02/22] Move unhealthy warning --- .../pages/WorkspacePage/Workspace.stories.tsx | 15 ++++++- site/src/pages/WorkspacePage/Workspace.tsx | 43 ++++--------------- .../WorkspacePage/WorkspaceNotifications.tsx | 33 +++++++++++++- .../WorkspacePage/WorkspaceReadyPage.tsx | 9 ++-- 4 files changed, 58 insertions(+), 42 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 35744ef74c590..5c2dcc8e2f389 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -10,6 +10,7 @@ import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"; import { WorkspaceBuildLogsSection } from "pages/WorkspacePage/WorkspaceBuildLogsSection"; import { getWorkspaceResolveAutostartQueryKey } from "api/queries/workspaceQuota"; +import { WorkspacePermissions } from "./permissions"; const MockedAppearance = { config: Mocks.MockAppearanceConfig, @@ -17,8 +18,16 @@ const MockedAppearance = { setPreview: () => {}, }; +const permissions: WorkspacePermissions = { + readWorkspace: true, + updateWorkspace: true, + updateTemplate: true, + viewDeploymentValues: true, +}; + const meta: Meta = { title: "pages/WorkspacePage/Workspace", + args: { permissions }, component: Workspace, decorators: [ (Story) => ( @@ -69,7 +78,6 @@ export const Running: Story = { workspace: Mocks.MockWorkspace, handleStart: action("start"), handleStop: action("stop"), - canUpdateWorkspace: true, workspaceErrors: {}, buildInfo: Mocks.MockBuildInfo, template: Mocks.MockTemplate, @@ -79,7 +87,10 @@ export const Running: Story = { export const WithoutUpdateAccess: Story = { args: { ...Running.args, - canUpdateWorkspace: false, + permissions: { + ...permissions, + updateWorkspace: false, + }, }, }; diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index ad530d4fd4aad..db6e3b9498c53 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -27,6 +27,7 @@ import HubOutlined from "@mui/icons-material/HubOutlined"; import { ResourcesSidebar } from "./ResourcesSidebar"; import { ResourceCard } from "components/Resources/ResourceCard"; import { WorkspaceNotifications } from "./WorkspaceNotifications"; +import { WorkspacePermissions } from "./permissions"; export type WorkspaceError = | "getBuildsError" @@ -48,7 +49,6 @@ export interface WorkspaceProps { isUpdating: boolean; isRestarting: boolean; workspace: TypesGen.Workspace; - canUpdateWorkspace: boolean; canChangeVersions: boolean; hideSSHButton?: boolean; hideVSCodeDesktopButton?: boolean; @@ -61,6 +61,7 @@ export interface WorkspaceProps { handleBuildRetryDebug: () => void; buildLogs?: React.ReactNode; latestVersion?: TypesGen.TemplateVersion; + permissions: WorkspacePermissions; } /** @@ -79,7 +80,6 @@ export const Workspace: FC = ({ workspace, isUpdating, isRestarting, - canUpdateWorkspace, canChangeVersions, workspaceErrors, hideSSHButton, @@ -92,6 +92,7 @@ export const Workspace: FC = ({ handleBuildRetryDebug, buildLogs, latestVersion, + permissions, }) => { const navigate = useNavigate(); const { saveLocal, getLocal } = useLocalStorage(); @@ -190,7 +191,7 @@ export const Workspace: FC = ({ canChangeVersions={canChangeVersions} isUpdating={isUpdating} isRestarting={isRestarting} - canUpdateWorkspace={canUpdateWorkspace} + canUpdateWorkspace={permissions.updateWorkspace} />
= ({ {Boolean(workspaceErrors.buildError) && ( @@ -252,40 +255,12 @@ export const Workspace: FC = ({ /> )} - {workspace.latest_build.status === "running" && - !workspace.health.healthy && ( - { - handleRestart(); - }} - > - Restart - - ) - } - > - Workspace is unhealthy - - Your workspace is running but{" "} - {workspace.health.failing_agents.length > 1 - ? `${workspace.health.failing_agents.length} agents are unhealthy` - : `1 agent is unhealthy`} - . - - - )} - {workspace.latest_build.status === "deleted" && ( navigate(`/templates`)} /> )} + {/* determines its own visibility */} = ({ agent={agent} workspace={workspace} sshPrefix={sshPrefix} - showApps={canUpdateWorkspace} - showBuiltinApps={canUpdateWorkspace} + showApps={permissions.updateWorkspace} + showBuiltinApps={permissions.updateWorkspace} hideSSHButton={hideSSHButton} hideVSCodeDesktopButton={hideVSCodeDesktopButton} serverVersion={buildInfo?.version || ""} diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx index 46e7371175e4c..4e77f1c876a74 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx @@ -1,19 +1,23 @@ import AlertTitle from "@mui/material/AlertTitle"; +import Button from "@mui/material/Button"; import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; import { TemplateVersion, Workspace } from "api/typesGenerated"; import { Alert, AlertDetail } from "components/Alert/Alert"; import { FC } from "react"; import { useQuery } from "react-query"; +import { WorkspacePermissions } from "./permissions"; type WorkspaceNotificationsProps = { workspace: Workspace; + permissions: WorkspacePermissions; + onRestartWorkspace: () => void; latestVersion?: TemplateVersion; }; export const WorkspaceNotifications: FC = ( props, ) => { - const { workspace, latestVersion } = props; + const { workspace, latestVersion, permissions, onRestartWorkspace } = props; // Outdated const canAutostartResponse = useQuery( @@ -47,6 +51,33 @@ export const WorkspaceNotifications: FC = ( {latestVersion.message} ))} + + {workspace.latest_build.status === "running" && + !workspace.health.healthy && ( + + Restart + + ) + } + > + Workspace is unhealthy + + Your workspace is running but{" "} + {workspace.health.failing_agents.length > 1 + ? `${workspace.health.failing_agents.length} agents are unhealthy` + : `1 agent is unhealthy`} + . + + + )} ); }; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index ea3adf928ddb0..efa6ea4ffb47c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -58,7 +58,7 @@ export const WorkspaceReadyPage = ({ // Debug mode const { data: deploymentValues } = useQuery({ ...deploymentConfig(), - enabled: permissions?.viewDeploymentValues, + enabled: permissions.viewDeploymentValues, }); // Build logs @@ -99,7 +99,7 @@ export const WorkspaceReadyPage = ({ }, []); // Change version - const canChangeVersions = Boolean(permissions?.updateTemplate); + const canChangeVersions = Boolean(permissions.updateTemplate); const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); const changeVersionMutation = useMutation( changeVersion(workspace, queryClient), @@ -116,7 +116,6 @@ export const WorkspaceReadyPage = ({ }); // Update workspace - const canUpdateWorkspace = Boolean(permissions?.updateWorkspace); const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); const updateWorkspaceMutation = useMutation( updateWorkspace(workspace, queryClient), @@ -124,7 +123,7 @@ export const WorkspaceReadyPage = ({ // If a user can update the template then they can force a delete // (via orphan). - const canUpdateTemplate = Boolean(permissions?.updateTemplate); + const canUpdateTemplate = Boolean(permissions.updateTemplate); const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); const deleteWorkspaceMutation = useMutation( deleteWorkspace(workspace, queryClient), @@ -181,6 +180,7 @@ export const WorkspaceReadyPage = ({ Date: Tue, 9 Jan 2024 12:30:36 +0000 Subject: [PATCH 03/22] Move dormant notification --- .../src/components/WorkspaceDeletion/index.ts | 2 -- .../DormantDeletionText.tsx | 0 .../WorkspaceStatusBadge.tsx | 2 +- .../WorkspacePage}/DormantWorkspaceBanner.tsx | 20 +++++++++---------- site/src/pages/WorkspacePage/Workspace.tsx | 13 +----------- .../WorkspacePage/WorkspaceNotifications.tsx | 3 +++ 6 files changed, 15 insertions(+), 25 deletions(-) delete mode 100644 site/src/components/WorkspaceDeletion/index.ts rename site/src/components/{WorkspaceDeletion => WorkspaceStatusBadge}/DormantDeletionText.tsx (100%) rename site/src/{components/WorkspaceDeletion => pages/WorkspacePage}/DormantWorkspaceBanner.tsx (86%) diff --git a/site/src/components/WorkspaceDeletion/index.ts b/site/src/components/WorkspaceDeletion/index.ts deleted file mode 100644 index 20c01e31d8f09..0000000000000 --- a/site/src/components/WorkspaceDeletion/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./DormantDeletionText"; -export * from "./DormantWorkspaceBanner"; diff --git a/site/src/components/WorkspaceDeletion/DormantDeletionText.tsx b/site/src/components/WorkspaceStatusBadge/DormantDeletionText.tsx similarity index 100% rename from site/src/components/WorkspaceDeletion/DormantDeletionText.tsx rename to site/src/components/WorkspaceStatusBadge/DormantDeletionText.tsx diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx index 798b70bfde592..1b40462adcf6d 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -9,7 +9,7 @@ import { type FC, type ReactNode } from "react"; import type { Workspace } from "api/typesGenerated"; import { Pill } from "components/Pill/Pill"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; -import { DormantDeletionText } from "components/WorkspaceDeletion"; +import { DormantDeletionText } from "./DormantDeletionText"; import { getDisplayWorkspaceStatus } from "utils/workspace"; import { useClassName } from "hooks/useClassName"; import { formatDistanceToNow } from "date-fns"; diff --git a/site/src/components/WorkspaceDeletion/DormantWorkspaceBanner.tsx b/site/src/pages/WorkspacePage/DormantWorkspaceBanner.tsx similarity index 86% rename from site/src/components/WorkspaceDeletion/DormantWorkspaceBanner.tsx rename to site/src/pages/WorkspacePage/DormantWorkspaceBanner.tsx index 6c03ccdbad74a..df036fcb3db56 100644 --- a/site/src/components/WorkspaceDeletion/DormantWorkspaceBanner.tsx +++ b/site/src/pages/WorkspacePage/DormantWorkspaceBanner.tsx @@ -3,24 +3,18 @@ import { ReactNode, type FC } from "react"; import type { Workspace } from "api/typesGenerated"; import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider"; import { Alert } from "components/Alert/Alert"; - -export enum Count { - Singular, - Multiple, -} +import { useLocalStorage } from "hooks"; interface DormantWorkspaceBannerProps { workspace: Workspace; - onDismiss: () => void; - shouldRedisplayBanner: boolean; } export const DormantWorkspaceBanner: FC = ({ workspace, - onDismiss, - shouldRedisplayBanner, }) => { const experimentEnabled = useIsWorkspaceActionsEnabled(); + const { saveLocal, getLocal } = useLocalStorage(); + const shouldRedisplayBanner = getLocal("dismissedWorkspace") !== workspace.id; if ( // Only show this if the experiment is included. @@ -69,7 +63,13 @@ export const DormantWorkspaceBanner: FC = ({ }; return ( - + { + saveLocal("dismissedWorkspace", workspace.id); + }} + dismissible + > {alertText()} ); diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index db6e3b9498c53..37509e97dd60b 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -8,9 +8,8 @@ import type * as TypesGen from "api/typesGenerated"; import { Alert, AlertDetail } from "components/Alert/Alert"; import { Stack } from "components/Stack/Stack"; import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { DormantWorkspaceBanner } from "components/WorkspaceDeletion"; import { AgentRow } from "components/Resources/AgentRow"; -import { useLocalStorage, useTab } from "hooks"; +import { useTab } from "hooks"; import { ActiveTransition, WorkspaceBuildProgress, @@ -95,7 +94,6 @@ export const Workspace: FC = ({ permissions, }) => { const navigate = useNavigate(); - const { saveLocal, getLocal } = useLocalStorage(); const theme = useTheme(); const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false); @@ -261,15 +259,6 @@ export const Workspace: FC = ({ /> )} - {/* determines its own visibility */} - saveLocal("dismissedWorkspace", workspace.id)} - /> - {showAlertPendingInQueue && ( Workspace build is pending diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx index 4e77f1c876a74..a14dd87ca6b69 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx @@ -6,6 +6,7 @@ import { Alert, AlertDetail } from "components/Alert/Alert"; import { FC } from "react"; import { useQuery } from "react-query"; import { WorkspacePermissions } from "./permissions"; +import { DormantWorkspaceBanner } from "./DormantWorkspaceBanner"; type WorkspaceNotificationsProps = { workspace: Workspace; @@ -78,6 +79,8 @@ export const WorkspaceNotifications: FC = ( )} + + ); }; From 9ec5952f452fc2cebe3b8d7eac8dc4300d900dff Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 9 Jan 2024 12:33:47 +0000 Subject: [PATCH 04/22] Remove mutation errors from alerts --- .../pages/WorkspacePage/Workspace.stories.tsx | 36 ------------------- site/src/pages/WorkspacePage/Workspace.tsx | 21 ----------- .../WorkspacePage/WorkspaceReadyPage.tsx | 20 +++-------- 3 files changed, 4 insertions(+), 73 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 5c2dcc8e2f389..80cdeac2f8557 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -78,7 +78,6 @@ export const Running: Story = { workspace: Mocks.MockWorkspace, handleStart: action("start"), handleStop: action("stop"), - workspaceErrors: {}, buildInfo: Mocks.MockBuildInfo, template: Mocks.MockTemplate, }, @@ -122,18 +121,6 @@ export const Stopping: Story = { }, }; -export const Failed: Story = { - args: { - ...Running.args, - workspace: Mocks.MockFailedWorkspace, - workspaceErrors: { - buildError: Mocks.mockApiError({ - message: "A workspace build is already active.", - }), - }, - }, -}; - export const FailedWithLogs: Story = { args: { ...Running.args, @@ -224,29 +211,6 @@ export const CantAutostart: Story = { }, }; -export const GetBuildsError: Story = { - args: { - ...Running.args, - workspaceErrors: { - getBuildsError: Mocks.mockApiError({ - message: "There is a problem fetching builds.", - }), - }, - }, -}; - -export const CancellationError: Story = { - args: { - ...Failed.args, - workspaceErrors: { - cancellationError: Mocks.mockApiError({ - message: "Job could not be canceled.", - }), - }, - buildLogs: , - }, -}; - export const Deprecated: Story = { args: { ...Running.args, diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 37509e97dd60b..33d42674cf405 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -7,7 +7,6 @@ import dayjs from "dayjs"; import type * as TypesGen from "api/typesGenerated"; import { Alert, AlertDetail } from "components/Alert/Alert"; import { Stack } from "components/Stack/Stack"; -import { ErrorAlert } from "components/Alert/ErrorAlert"; import { AgentRow } from "components/Resources/AgentRow"; import { useTab } from "hooks"; import { @@ -28,13 +27,6 @@ import { ResourceCard } from "components/Resources/ResourceCard"; import { WorkspaceNotifications } from "./WorkspaceNotifications"; import { WorkspacePermissions } from "./permissions"; -export type WorkspaceError = - | "getBuildsError" - | "buildError" - | "cancellationError"; - -export type WorkspaceErrors = Partial>; - export interface WorkspaceProps { handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; handleStop: () => void; @@ -51,7 +43,6 @@ export interface WorkspaceProps { canChangeVersions: boolean; hideSSHButton?: boolean; hideVSCodeDesktopButton?: boolean; - workspaceErrors: WorkspaceErrors; buildInfo?: TypesGen.BuildInfoResponse; sshPrefix?: string; template?: TypesGen.Template; @@ -80,7 +71,6 @@ export const Workspace: FC = ({ isUpdating, isRestarting, canChangeVersions, - workspaceErrors, hideSSHButton, hideVSCodeDesktopButton, buildInfo, @@ -242,17 +232,6 @@ export const Workspace: FC = ({ onRestartWorkspace={handleRestart} /> - {Boolean(workspaceErrors.buildError) && ( - - )} - - {Boolean(workspaceErrors.cancellationError) && ( - - )} - {workspace.latest_build.status === "deleted" && ( navigate(`/templates`)} diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index efa6ea4ffb47c..d4eba23d42cf4 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -74,13 +74,10 @@ export const WorkspaceReadyPage = ({ open: boolean; buildParameters?: TypesGen.WorkspaceBuildParameter[]; }>({ open: false }); - const { - mutate: mutateRestartWorkspace, - error: restartBuildError, - isLoading: isRestarting, - } = useMutation({ - mutationFn: restartWorkspace, - }); + const { mutate: mutateRestartWorkspace, isLoading: isRestarting } = + useMutation({ + mutationFn: restartWorkspace, + }); // SSH Prefix const sshPrefixQuery = useQuery(deploymentSSHConfig()); @@ -221,15 +218,6 @@ export const WorkspaceReadyPage = ({ canChangeVersions={canChangeVersions} hideSSHButton={featureVisibility["browser_only"]} hideVSCodeDesktopButton={featureVisibility["browser_only"]} - workspaceErrors={{ - buildError: - restartBuildError ?? - startWorkspaceMutation.error ?? - stopWorkspaceMutation.error ?? - deleteWorkspaceMutation.error ?? - updateWorkspaceMutation.error, - cancellationError: cancelBuildMutation.error, - }} buildInfo={buildInfo} sshPrefix={sshPrefixQuery.data?.hostname_prefix} template={template} From 17ecf0ea1388df3193fae84b1905fa204154fcb2 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 9 Jan 2024 12:40:04 +0000 Subject: [PATCH 05/22] Move pending in queue notification --- site/src/pages/WorkspacePage/Workspace.tsx | 62 +------------------ .../WorkspacePage/WorkspaceNotifications.tsx | 57 ++++++++++++++++- 2 files changed, 57 insertions(+), 62 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 33d42674cf405..12bdb0e9e6cc5 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -1,9 +1,8 @@ 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 { type FC, useEffect } from "react"; import { useNavigate } from "react-router-dom"; -import dayjs from "dayjs"; import type * as TypesGen from "api/typesGenerated"; import { Alert, AlertDetail } from "components/Alert/Alert"; import { Stack } from "components/Stack/Stack"; @@ -86,43 +85,6 @@ export const Workspace: FC = ({ const navigate = useNavigate(); const theme = useTheme(); - const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false); - - // 2023-11-15 - MES - This effect will be called every single render because - // "now" will always change and invalidate the dependency array. Need to - // figure out if this effect really should run every render (possibly meaning - // no dependency array at all), or how to get the array stabilized (ideal) - const now = dayjs(); - useEffect(() => { - if ( - workspace.latest_build.status !== "pending" || - workspace.latest_build.job.queue_size === 0 - ) { - if (!showAlertPendingInQueue) { - return; - } - - const hideTimer = setTimeout(() => { - setShowAlertPendingInQueue(false); - }, 250); - return () => { - clearTimeout(hideTimer); - }; - } - - const t = Math.max( - 0, - 5000 - dayjs().diff(dayjs(workspace.latest_build.created_at)), - ); - const showTimer = setTimeout(() => { - setShowAlertPendingInQueue(true); - }, t); - - return () => { - clearTimeout(showTimer); - }; - }, [workspace, now, showAlertPendingInQueue]); - const transitionStats = template !== undefined ? ActiveTransition(template, workspace) : undefined; @@ -238,24 +200,6 @@ export const Workspace: FC = ({ /> )} - {showAlertPendingInQueue && ( - - Workspace build is pending - -
- This workspace build job is waiting for a provisioner to - become available. If you have been waiting for an extended - period of time, please contact your administrator for - assistance. -
-
- Position in queue:{" "} - {workspace.latest_build.job.queue_position} -
-
-
- )} - {workspace.latest_build.job.error && ( >; diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx index a14dd87ca6b69..0e231b785bb61 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx @@ -3,10 +3,11 @@ import Button from "@mui/material/Button"; import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; import { TemplateVersion, Workspace } from "api/typesGenerated"; import { Alert, AlertDetail } from "components/Alert/Alert"; -import { FC } from "react"; +import { FC, useEffect, useState } from "react"; import { useQuery } from "react-query"; import { WorkspacePermissions } from "./permissions"; import { DormantWorkspaceBanner } from "./DormantWorkspaceBanner"; +import dayjs from "dayjs"; type WorkspaceNotificationsProps = { workspace: Workspace; @@ -32,6 +33,43 @@ export const WorkspaceNotifications: FC = ( const autoStartFailing = workspace.autostart_schedule && !canAutostart; const requiresManualUpdate = updateRequired && autoStartFailing; + // Pending in Queue + const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false); + // 2023-11-15 - MES - This effect will be called every single render because + // "now" will always change and invalidate the dependency array. Need to + // figure out if this effect really should run every render (possibly meaning + // no dependency array at all), or how to get the array stabilized (ideal) + const now = dayjs(); + useEffect(() => { + if ( + workspace.latest_build.status !== "pending" || + workspace.latest_build.job.queue_size === 0 + ) { + if (!showAlertPendingInQueue) { + return; + } + + const hideTimer = setTimeout(() => { + setShowAlertPendingInQueue(false); + }, 250); + return () => { + clearTimeout(hideTimer); + }; + } + + const t = Math.max( + 0, + 5000 - dayjs().diff(dayjs(workspace.latest_build.created_at)), + ); + const showTimer = setTimeout(() => { + setShowAlertPendingInQueue(true); + }, t); + + return () => { + clearTimeout(showTimer); + }; + }, [workspace, now, showAlertPendingInQueue]); + return ( <> {workspace.outdated && @@ -81,6 +119,23 @@ export const WorkspaceNotifications: FC = ( )} + + {showAlertPendingInQueue && ( + + Workspace build is pending + +
+ This workspace build job is waiting for a provisioner to become + available. If you have been waiting for an extended period of + time, please contact your administrator for assistance. +
+
+ Position in queue:{" "} + {workspace.latest_build.job.queue_position} +
+
+
+ )} ); }; From c969df502e7c55ee086eb50103163490be220ec2 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 9 Jan 2024 12:43:08 +0000 Subject: [PATCH 06/22] Move deprecated notification --- site/src/pages/WorkspacePage/Workspace.tsx | 10 ++-------- .../WorkspacePage/WorkspaceNotifications.tsx | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 12bdb0e9e6cc5..ca822d1582062 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -44,7 +44,7 @@ export interface WorkspaceProps { hideVSCodeDesktopButton?: boolean; buildInfo?: TypesGen.BuildInfoResponse; sshPrefix?: string; - template?: TypesGen.Template; + template: TypesGen.Template; canRetryDebugMode: boolean; handleBuildRetry: () => void; handleBuildRetryDebug: () => void; @@ -189,6 +189,7 @@ export const Workspace: FC = ({ = ({
)} - {template?.deprecated && ( - - Workspace using deprecated template - {template?.deprecation_message} - - )} - {transitionStats !== undefined && ( void; latestVersion?: TemplateVersion; @@ -19,7 +20,13 @@ type WorkspaceNotificationsProps = { export const WorkspaceNotifications: FC = ( props, ) => { - const { workspace, latestVersion, permissions, onRestartWorkspace } = props; + const { + workspace, + template, + latestVersion, + permissions, + onRestartWorkspace, + } = props; // Outdated const canAutostartResponse = useQuery( @@ -136,6 +143,13 @@ export const WorkspaceNotifications: FC = (
)} + + {template.deprecated && ( + + Workspace using deprecated template + {template.deprecation_message} + + )} ); }; From 8f6040d45ac760f67dc0c46307bde5934c854adf Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 9 Jan 2024 12:52:19 +0000 Subject: [PATCH 07/22] Move outdated to notifications array --- .../WorkspacePage/WorkspaceNotifications.tsx | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx index 38dcf5aabe53c..6007c262584a8 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx @@ -2,13 +2,20 @@ import AlertTitle from "@mui/material/AlertTitle"; import Button from "@mui/material/Button"; import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; import { Template, TemplateVersion, Workspace } from "api/typesGenerated"; -import { Alert, AlertDetail } from "components/Alert/Alert"; +import { Alert, AlertDetail, AlertProps } from "components/Alert/Alert"; import { FC, useEffect, useState } from "react"; import { useQuery } from "react-query"; import { WorkspacePermissions } from "./permissions"; import { DormantWorkspaceBanner } from "./DormantWorkspaceBanner"; import dayjs from "dayjs"; +type Notification = { + title: string; + severity: AlertProps["severity"]; + detail?: string; + actions?: { label: string; onClick: () => void }[]; +}; + type WorkspaceNotificationsProps = { workspace: Workspace; template: Template; @@ -27,6 +34,7 @@ export const WorkspaceNotifications: FC = ( permissions, onRestartWorkspace, } = props; + const notifications: Notification[] = []; // Outdated const canAutostartResponse = useQuery( @@ -40,6 +48,23 @@ export const WorkspaceNotifications: FC = ( const autoStartFailing = workspace.autostart_schedule && !canAutostart; const requiresManualUpdate = updateRequired && autoStartFailing; + if (workspace.outdated && latestVersion) { + if (requiresManualUpdate) { + notifications.push({ + title: "Autostart has been disabled for your workspace.", + severity: "warning", + detail: + "Autostart is unable to automatically update your workspace. Manually update your workspace to reenable Autostart.", + }); + } else { + notifications.push({ + title: "An update is available for your workspace", + severity: "info", + detail: latestVersion.message, + }); + } + } + // Pending in Queue const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false); // 2023-11-15 - MES - This effect will be called every single render because @@ -79,25 +104,6 @@ export const WorkspaceNotifications: FC = ( return ( <> - {workspace.outdated && - latestVersion && - (requiresManualUpdate ? ( - - - Autostart has been disabled for your workspace. - - - Autostart is unable to automatically update your workspace. - Manually update your workspace to reenable Autostart. - - - ) : ( - - An update is available for your workspace - {latestVersion.message} - - ))} - {workspace.latest_build.status === "running" && !workspace.health.healthy && ( Date: Tue, 9 Jan 2024 12:53:52 +0000 Subject: [PATCH 08/22] Move unhealthy to notifications array --- .../WorkspacePage/WorkspaceNotifications.tsx | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx index 6007c262584a8..82a6337783f10 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx @@ -65,6 +65,28 @@ export const WorkspaceNotifications: FC = ( } } + // Unhealthy + if ( + workspace.latest_build.status === "running" && + !workspace.health.healthy + ) { + notifications.push({ + title: "Workspace is unhealthy", + severity: "warning", + detail: `Your workspace is running but ${ + workspace.health.failing_agents.length > 1 + ? `${workspace.health.failing_agents.length} agents are unhealthy` + : `1 agent is unhealthy` + }.`, + actions: [ + { + label: "Restart", + onClick: onRestartWorkspace, + }, + ], + }); + } + // Pending in Queue const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false); // 2023-11-15 - MES - This effect will be called every single render because @@ -104,33 +126,6 @@ export const WorkspaceNotifications: FC = ( return ( <> - {workspace.latest_build.status === "running" && - !workspace.health.healthy && ( - - Restart - - ) - } - > - Workspace is unhealthy - - Your workspace is running but{" "} - {workspace.health.failing_agents.length > 1 - ? `${workspace.health.failing_agents.length} agents are unhealthy` - : `1 agent is unhealthy`} - . - - - )} - {showAlertPendingInQueue && ( From 706bbd512d6c00acf0042e6b5ef0d21b576e2895 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 9 Jan 2024 12:54:58 +0000 Subject: [PATCH 09/22] Fix permissions for restart workspace --- .../WorkspacePage/WorkspaceNotifications.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx index 82a6337783f10..6ff0d45aa2c43 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx @@ -1,5 +1,4 @@ import AlertTitle from "@mui/material/AlertTitle"; -import Button from "@mui/material/Button"; import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; import { Template, TemplateVersion, Workspace } from "api/typesGenerated"; import { Alert, AlertDetail, AlertProps } from "components/Alert/Alert"; @@ -78,12 +77,14 @@ export const WorkspaceNotifications: FC = ( ? `${workspace.health.failing_agents.length} agents are unhealthy` : `1 agent is unhealthy` }.`, - actions: [ - { - label: "Restart", - onClick: onRestartWorkspace, - }, - ], + actions: permissions.updateWorkspace + ? [ + { + label: "Restart", + onClick: onRestartWorkspace, + }, + ] + : undefined, }); } From d3e68fb8bd4b5f4a5eb936a47de334597ef99c60 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 9 Jan 2024 13:03:33 +0000 Subject: [PATCH 10/22] Move dormant notifications to notifications array --- .../WorkspacePage/DormantWorkspaceBanner.tsx | 76 ------------------- .../WorkspacePage/WorkspaceNotifications.tsx | 59 +++++++++++--- 2 files changed, 49 insertions(+), 86 deletions(-) delete mode 100644 site/src/pages/WorkspacePage/DormantWorkspaceBanner.tsx diff --git a/site/src/pages/WorkspacePage/DormantWorkspaceBanner.tsx b/site/src/pages/WorkspacePage/DormantWorkspaceBanner.tsx deleted file mode 100644 index df036fcb3db56..0000000000000 --- a/site/src/pages/WorkspacePage/DormantWorkspaceBanner.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { formatDistanceToNow } from "date-fns"; -import { ReactNode, type FC } from "react"; -import type { Workspace } from "api/typesGenerated"; -import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider"; -import { Alert } from "components/Alert/Alert"; -import { useLocalStorage } from "hooks"; - -interface DormantWorkspaceBannerProps { - workspace: Workspace; -} - -export const DormantWorkspaceBanner: FC = ({ - workspace, -}) => { - const experimentEnabled = useIsWorkspaceActionsEnabled(); - const { saveLocal, getLocal } = useLocalStorage(); - const shouldRedisplayBanner = getLocal("dismissedWorkspace") !== workspace.id; - - if ( - // Only show this if the experiment is included. - !experimentEnabled || - !workspace.dormant_at || - // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion - !shouldRedisplayBanner - ) { - return null; - } - - const formatDate = (dateStr: string, timestamp: boolean): string => { - const date = new Date(dateStr); - return date.toLocaleDateString(undefined, { - month: "long", - day: "numeric", - year: "numeric", - ...(timestamp ? { hour: "numeric", minute: "numeric" } : {}), - }); - }; - - const alertText = (): ReactNode => { - if (workspace.deleting_at && workspace.dormant_at) { - return ( - <> - This workspace has not been used for{" "} - {formatDistanceToNow(Date.parse(workspace.last_used_at))} and was - marked dormant on {formatDate(workspace.dormant_at, false)}. It is - scheduled to be deleted on {formatDate(workspace.deleting_at, true)}. - To keep it you must activate the workspace. - - ); - } else if (workspace.dormant_at) { - return ( - <> - This workspace has not been used for{" "} - {formatDistanceToNow(Date.parse(workspace.last_used_at))} and was - marked dormant on {formatDate(workspace.dormant_at, false)}. It is not - scheduled for auto-deletion but will become a candidate if - auto-deletion is enabled on this template. To keep it you must - activate the workspace. - - ); - } - return ""; - }; - - return ( - { - saveLocal("dismissedWorkspace", workspace.id); - }} - dismissible - > - {alertText()} - - ); -}; diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx index 6ff0d45aa2c43..a449ea58d0397 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx @@ -2,16 +2,17 @@ import AlertTitle from "@mui/material/AlertTitle"; import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; import { Template, TemplateVersion, Workspace } from "api/typesGenerated"; import { Alert, AlertDetail, AlertProps } from "components/Alert/Alert"; -import { FC, useEffect, useState } from "react"; +import { FC, ReactNode, useEffect, useState } from "react"; import { useQuery } from "react-query"; import { WorkspacePermissions } from "./permissions"; -import { DormantWorkspaceBanner } from "./DormantWorkspaceBanner"; import dayjs from "dayjs"; +import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider"; +import formatDistanceToNow from "date-fns/formatDistanceToNow"; type Notification = { title: string; severity: AlertProps["severity"]; - detail?: string; + detail?: ReactNode; actions?: { label: string; onClick: () => void }[]; }; @@ -72,11 +73,15 @@ export const WorkspaceNotifications: FC = ( notifications.push({ title: "Workspace is unhealthy", severity: "warning", - detail: `Your workspace is running but ${ - workspace.health.failing_agents.length > 1 - ? `${workspace.health.failing_agents.length} agents are unhealthy` - : `1 agent is unhealthy` - }.`, + detail: ( + <> + Your workspace is running but{" "} + {workspace.health.failing_agents.length > 1 + ? `${workspace.health.failing_agents.length} agents are unhealthy` + : `1 agent is unhealthy`} + . + + ), actions: permissions.updateWorkspace ? [ { @@ -88,6 +93,42 @@ export const WorkspaceNotifications: FC = ( }); } + // Dormant + const areActionsEnabled = useIsWorkspaceActionsEnabled(); + if (areActionsEnabled && workspace.dormant_at) { + const formatDate = (dateStr: string, timestamp: boolean): string => { + const date = new Date(dateStr); + return date.toLocaleDateString(undefined, { + month: "long", + day: "numeric", + year: "numeric", + ...(timestamp ? { hour: "numeric", minute: "numeric" } : {}), + }); + }; + notifications.push({ + title: "Workspace is dormant", + severity: "warning", + detail: workspace.deleting_at ? ( + <> + This workspace has not been used for{" "} + {formatDistanceToNow(Date.parse(workspace.last_used_at))} and was + marked dormant on {formatDate(workspace.dormant_at, false)}. It is + scheduled to be deleted on {formatDate(workspace.deleting_at, true)}. + To keep it you must activate the workspace. + + ) : ( + <> + This workspace has not been used for{" "} + {formatDistanceToNow(Date.parse(workspace.last_used_at))} and was + marked dormant on {formatDate(workspace.dormant_at, false)}. It is not + scheduled for auto-deletion but will become a candidate if + auto-deletion is enabled on this template. To keep it you must + activate the workspace. + + ), + }); + } + // Pending in Queue const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false); // 2023-11-15 - MES - This effect will be called every single render because @@ -127,8 +168,6 @@ export const WorkspaceNotifications: FC = ( return ( <> - - {showAlertPendingInQueue && ( Workspace build is pending From 2f1ac839a2c4eb45469043cd0660721f34b1c611 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 9 Jan 2024 13:12:45 +0000 Subject: [PATCH 11/22] Structure notification pills --- .../WorkspacePage/WorkspaceNotifications.tsx | 78 ++++++++++++------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx index a449ea58d0397..455462534005e 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx @@ -1,13 +1,15 @@ -import AlertTitle from "@mui/material/AlertTitle"; import { workspaceResolveAutostart } from "api/queries/workspaceQuota"; import { Template, TemplateVersion, Workspace } from "api/typesGenerated"; -import { Alert, AlertDetail, AlertProps } from "components/Alert/Alert"; +import { AlertProps } from "components/Alert/Alert"; import { FC, ReactNode, useEffect, useState } from "react"; import { useQuery } from "react-query"; import { WorkspacePermissions } from "./permissions"; import dayjs from "dayjs"; import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider"; import formatDistanceToNow from "date-fns/formatDistanceToNow"; +import { Pill } from "components/Pill/Pill"; +import InfoOutlined from "@mui/icons-material/InfoOutlined"; +import ErrorOutline from "@mui/icons-material/ErrorOutline"; type Notification = { title: string; @@ -166,31 +168,53 @@ export const WorkspaceNotifications: FC = ( }; }, [workspace, now, showAlertPendingInQueue]); - return ( - <> - {showAlertPendingInQueue && ( - - Workspace build is pending - -
- This workspace build job is waiting for a provisioner to become - available. If you have been waiting for an extended period of - time, please contact your administrator for assistance. -
-
- Position in queue:{" "} - {workspace.latest_build.job.queue_position} -
-
-
- )} + if (showAlertPendingInQueue) { + notifications.push({ + title: "Workspace build is pending", + severity: "info", + detail: ( + <> +
+ This workspace build job is waiting for a provisioner to become + available. If you have been waiting for an extended period of time, + please contact your administrator for assistance. +
+
+ Position in queue:{" "} + {workspace.latest_build.job.queue_position} +
+ + ), + }); + } - {template.deprecated && ( - - Workspace using deprecated template - {template.deprecation_message} - - )} - + // Deprecated + if (template.deprecated) { + notifications.push({ + title: "Workspace using deprecated template", + severity: "warning", + detail: template.deprecation_message, + }); + } + + return ( +
+ }> + 2 + + }> + 4 + +
); }; From c334031d39f3ef5a7ad689a3ee5f651716445572 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 9 Jan 2024 13:23:08 +0000 Subject: [PATCH 12/22] Minor style adjustments --- site/src/pages/WorkspacePage/Workspace.tsx | 11 +++-------- .../pages/WorkspacePage/WorkspaceNotifications.tsx | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index ca822d1582062..cd2fbd8e5e42a 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -5,7 +5,6 @@ import { type FC, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import type * as TypesGen from "api/typesGenerated"; import { Alert, AlertDetail } from "components/Alert/Alert"; -import { Stack } from "components/Stack/Stack"; import { AgentRow } from "components/Resources/AgentRow"; import { useTab } from "hooks"; import { @@ -186,7 +185,7 @@ export const Workspace: FC = ({
- +
= ({ )} /> )} - +
@@ -272,7 +271,7 @@ const styles = { dotBackground: (theme) => ({ minHeight: "100%", - padding: 24, + padding: 23, "--d": "1px", background: ` radial-gradient( @@ -292,8 +291,4 @@ const styles = { flexDirection: "column", }, }), - - firstColumnSpacer: { - flex: 2, - }, } satisfies Record>; diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx index 455462534005e..18e83919ef6e2 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx @@ -204,8 +204,8 @@ export const WorkspaceNotifications: FC = ( alignItems: "center", gap: 8, position: "fixed", - bottom: 24, - right: 24, + bottom: 48, + right: 48, zIndex: 10, }} > From a78d4cf77bc242cb714d86c4b3f9c4342f97654e Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 9 Jan 2024 16:34:27 +0000 Subject: [PATCH 13/22] Remove bottom bar from workspace page --- site/src/AppRouter.tsx | 2 +- site/src/pages/WorkspacePage/Workspace.tsx | 4 -- .../src/pages/WorkspacePage/WorkspacePage.tsx | 40 +++++++++---------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index d06068100b268..400ce1e19c733 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -370,7 +370,6 @@ export const AppRouter: FC = () => { {/* In order for the 404 page to work properly the routes that start with top level parameter must be fully qualified. */} - } /> } @@ -413,6 +412,7 @@ export const AppRouter: FC = () => { {/* Pages that don't have the dashboard layout */} + } /> } diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index cd2fbd8e5e42a..55fb7596c4004 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -14,8 +14,6 @@ import { import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner"; import { WorkspaceTopbar } from "./WorkspaceTopbar"; import { HistorySidebar } from "./HistorySidebar"; -import { dashboardContentBottomPadding, navHeight } from "theme/constants"; -import { bannerHeight } from "components/Dashboard/DeploymentBanner/DeploymentBannerView"; import HistoryOutlined from "@mui/icons-material/HistoryOutlined"; import { useTheme } from "@mui/material/styles"; import { SidebarIconButton } from "components/FullPageLayout/Sidebar"; @@ -119,8 +117,6 @@ export const Workspace: FC = ({ "topbar topbar topbar" auto "leftbar sidebar content" 1fr / auto auto 1fr `, - maxHeight: `calc(100vh - ${navHeight + bannerHeight}px)`, - marginBottom: `-${dashboardContentBottomPadding}px`, }} > { const queryClient = useQueryClient(); @@ -102,27 +103,26 @@ export const WorkspacePage: FC = () => { workspaceQuery.error ?? templateQuery.error ?? permissionsQuery.error; const isLoading = !workspace || !template || !permissions; - if (pageError) { - return ( - - - - ); - } - - if (isLoading) { - return ; - } - return ( - +
+ + {pageError ? ( + + + + ) : isLoading ? ( + + ) : ( + + )} +
); }; From 24c7552e086977be5d5717c7356eb3438b193def Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 9 Jan 2024 17:13:25 +0000 Subject: [PATCH 14/22] Display notifications under the pills --- site/src/pages/WorkspacePage/Workspace.tsx | 2 + .../WorkspacePage/WorkspaceNotifications.tsx | 157 ++++++++++++++++-- .../src/pages/WorkspacePage/WorkspacePage.tsx | 2 +- 3 files changed, 143 insertions(+), 18 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 55fb7596c4004..01483a1d4a038 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -188,6 +188,8 @@ export const Workspace: FC = ({ latestVersion={latestVersion} permissions={permissions} onRestartWorkspace={handleRestart} + onUpdateWorkspace={handleUpdate} + onActivateWorkspace={handleDormantActivate} /> {workspace.latest_build.status === "deleted" && ( diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx index 18e83919ef6e2..f3a6da6716830 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications.tsx @@ -9,13 +9,21 @@ import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProv import formatDistanceToNow from "date-fns/formatDistanceToNow"; import { Pill } from "components/Pill/Pill"; import InfoOutlined from "@mui/icons-material/InfoOutlined"; -import ErrorOutline from "@mui/icons-material/ErrorOutline"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; +import { Interpolation, Theme, useTheme } from "@emotion/react"; +import Button, { ButtonProps } from "@mui/material/Button"; +import { ThemeRole } from "theme/experimental"; +import WarningRounded from "@mui/icons-material/WarningRounded"; type Notification = { title: string; severity: AlertProps["severity"]; detail?: ReactNode; - actions?: { label: string; onClick: () => void }[]; + actions?: ReactNode; }; type WorkspaceNotificationsProps = { @@ -23,6 +31,8 @@ type WorkspaceNotificationsProps = { template: Template; permissions: WorkspacePermissions; onRestartWorkspace: () => void; + onUpdateWorkspace: () => void; + onActivateWorkspace: () => void; latestVersion?: TemplateVersion; }; @@ -35,6 +45,8 @@ export const WorkspaceNotifications: FC = ( latestVersion, permissions, onRestartWorkspace, + onUpdateWorkspace, + onActivateWorkspace, } = props; const notifications: Notification[] = []; @@ -51,18 +63,26 @@ export const WorkspaceNotifications: FC = ( const requiresManualUpdate = updateRequired && autoStartFailing; if (workspace.outdated && latestVersion) { + const actions = ( + + Update + + ); if (requiresManualUpdate) { notifications.push({ title: "Autostart has been disabled for your workspace.", severity: "warning", detail: "Autostart is unable to automatically update your workspace. Manually update your workspace to reenable Autostart.", + + actions, }); } else { notifications.push({ title: "An update is available for your workspace", severity: "info", detail: latestVersion.message, + actions, }); } } @@ -84,14 +104,11 @@ export const WorkspaceNotifications: FC = ( . ), - actions: permissions.updateWorkspace - ? [ - { - label: "Restart", - onClick: onRestartWorkspace, - }, - ] - : undefined, + actions: permissions.updateWorkspace ? ( + + Restart + + ) : undefined, }); } @@ -107,7 +124,13 @@ export const WorkspaceNotifications: FC = ( ...(timestamp ? { hour: "numeric", minute: "numeric" } : {}), }); }; + const actions = ( + + Activate + + ); notifications.push({ + actions, title: "Workspace is dormant", severity: "warning", detail: workspace.deleting_at ? ( @@ -197,24 +220,124 @@ export const WorkspaceNotifications: FC = ( }); } + const infoNotifications = notifications.filter((n) => n.severity === "info"); + const warningNotifications = notifications.filter( + (n) => n.severity === "warning", + ); + return (
- }> - 2 - - }> - 4 - + {infoNotifications.length > 0 && ( + } + /> + )} + + {warningNotifications.length > 0 && ( + } + /> + )}
); }; + +type NotificationPillProps = { + notifications: Notification[]; + type: ThemeRole; + icon: ReactNode; +}; + +const NotificationPill: FC = (props) => { + const { notifications, type, icon } = props; + const theme = useTheme(); + + return ( + + +
+ + {notifications.length} + +
+
+ + {notifications.map((n) => ( + + ))} + +
+ ); +}; + +const NotificationItem: FC<{ notification: Notification }> = (props) => { + const { notification } = props; + const theme = useTheme(); + + return ( +
+

{notification.title}

+ {notification.detail && ( +

+ {notification.detail} +

+ )} +
{notification.actions}
+
+ ); +}; + +const NotificationActionButton: FC = (props) => { + return ( +