From 30ef863929675c080fd01616e268d1d3f26b500f Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 22 May 2023 19:09:39 +0000 Subject: [PATCH 1/5] add banner to workspace page --- site/src/components/Workspace/Workspace.tsx | 24 +++++++++++--- .../WorkspaceDeletedBanner.stories.tsx | 13 -------- .../WorkspaceDeletedBanner.tsx | 13 +++----- .../ImpendingDeletionBanner.tsx | 33 ++++++++++++------- .../TemplateSchedulePage.tsx | 5 +-- .../WorkspacesPage/WorkspacesPageView.tsx | 3 +- 6 files changed, 50 insertions(+), 41 deletions(-) diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index a67e946fe57e5..cfd55cb869415 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -28,6 +28,9 @@ import { } from "components/PageHeader/FullWidthPageHeader" import { TemplateVersionWarnings } from "components/TemplateVersionWarnings/TemplateVersionWarnings" import { ErrorAlert } from "components/Alert/ErrorAlert" +import { ImpendingDeletionBanner } from "components/WorkspaceDeletion" +import { useLocalStorage } from "hooks" +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" export enum WorkspaceErrors { GET_BUILDS_ERROR = "getBuildsError", @@ -105,6 +108,7 @@ export const Workspace: FC> = ({ const navigate = useNavigate() const serverVersion = buildInfo?.version || "" const { t } = useTranslation("workspacePage") + const { saveLocal, getLocal } = useLocalStorage() const buildError = Boolean(workspaceErrors[WorkspaceErrors.BUILD_ERROR]) && ( > = ({ {buildError} {cancellationError} - navigate(`/templates`)} - /> + + + navigate(`/templates`)} + /> + + + saveLocal("dismissedWorkspace", workspace.id)} + /> + + diff --git a/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.stories.tsx b/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.stories.tsx index d90036067bb32..fde4f1d3097a3 100644 --- a/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.stories.tsx +++ b/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.stories.tsx @@ -1,6 +1,5 @@ import { action } from "@storybook/addon-actions" import { Story } from "@storybook/react" -import * as Mocks from "../../testHelpers/entities" import { WorkspaceDeletedBanner, WorkspaceDeletedBannerProps, @@ -18,16 +17,4 @@ const Template: Story = (args) => ( export const Example = Template.bind({}) Example.args = { handleClick: action("extend"), - workspace: { - ...Mocks.MockWorkspace, - - latest_build: { - ...Mocks.MockWorkspaceBuild, - job: { - ...Mocks.MockProvisionerJob, - status: "succeeded", - }, - transition: "delete", - }, - }, } diff --git a/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.tsx b/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.tsx index 6a9de18026ccb..66b68f28758d1 100644 --- a/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.tsx +++ b/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.tsx @@ -1,18 +1,15 @@ import Button from "@mui/material/Button" import { FC } from "react" -import * as TypesGen from "api/typesGenerated" import { Alert } from "components/Alert/Alert" import { useTranslation } from "react-i18next" -import { Maybe } from "components/Conditionals/Maybe" export interface WorkspaceDeletedBannerProps { - workspace: TypesGen.Workspace handleClick: () => void } export const WorkspaceDeletedBanner: FC< React.PropsWithChildren -> = ({ workspace, handleClick }) => { +> = ({ handleClick }) => { const { t } = useTranslation("workspacePage") const NewWorkspaceButton = ( @@ -22,10 +19,8 @@ export const WorkspaceDeletedBanner: FC< ) return ( - - - {t("warningsAndErrors.workspaceDeletedWarning")} - - + + {t("warningsAndErrors.workspaceDeletedWarning")} + ) } diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index e75294945a048..7b80f3a6e6e56 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -1,17 +1,24 @@ import { Workspace } from "api/typesGenerated" import { displayImpendingDeletion } from "./utils" import { useDashboard } from "components/Dashboard/DashboardProvider" -import { Maybe } from "components/Conditionals/Maybe" import { Alert } from "components/Alert/Alert" +import { formatDistanceToNow } from "date-fns" + +export enum Count { + Singular, + Multiple, +} export const ImpendingDeletionBanner = ({ workspace, onDismiss, displayImpendingDeletionBanner, + count = Count.Singular, }: { workspace?: Workspace onDismiss: () => void displayImpendingDeletionBanner: boolean + count?: Count, }): JSX.Element | null => { const { entitlements, experiments } = useDashboard() const allowAdvancedScheduling = @@ -20,21 +27,23 @@ export const ImpendingDeletionBanner = ({ // is merged up const allowWorkspaceActions = experiments.includes("workspace_actions") - return ( - + ) || + !displayImpendingDeletionBanner + ) { + return null + } + + return ( - You have workspaces that will be deleted soon. + {count === Count.Singular + ? `This workspace has been unused for ${formatDistanceToNow(Date.parse(workspace.last_used_at))} and is scheduled for deletion. To keep it, connect via SSH or the web terminal.` + : "You have workspaces that will be deleted soon due to inactivity. To keep these workspaces, connect to them via SSH or the web terminal."} - ) } diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx index 8bc0d9256fb18..2145099a6713c 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.tsx @@ -32,8 +32,9 @@ const TemplateSchedulePage: FC = () => { { onSuccess: () => { displaySuccess("Template updated successfully") - // clear browser-stored list of workspaces impending deletion - clearLocal("dismissedWorkspaceList") + // clear browser storage of workspaces impending deletion + clearLocal("dismissedWorkspaceList") // workspaces page + clearLocal("dismissedWorkspace") // workspace page }, }, ) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 8d9e2dbfaffcd..dd4b7b2cd7712 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -17,7 +17,7 @@ import { WorkspacesTable } from "components/WorkspacesTable/WorkspacesTable" import { workspaceFilterQuery } from "utils/filters" import { useLocalStorage } from "hooks" import difference from "lodash/difference" -import { ImpendingDeletionBanner } from "components/WorkspaceDeletion" +import { ImpendingDeletionBanner, Count } from "components/WorkspaceDeletion" import { ErrorAlert } from "components/Alert/ErrorAlert" export const Language = { @@ -126,6 +126,7 @@ export const WorkspacesPageView: FC< JSON.stringify(workspaceIdsWithImpendingDeletions), ) } + count={Count.Multiple} /> Date: Mon, 22 May 2023 19:53:36 +0000 Subject: [PATCH 2/5] fix prettier and lint --- .../ImpendingDeletionBanner.tsx | 36 ++++++++++--------- .../WorkspacesPage/WorkspacesPage.test.tsx | 2 +- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index 7b80f3a6e6e56..f6fb7eea375ce 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -18,7 +18,7 @@ export const ImpendingDeletionBanner = ({ workspace?: Workspace onDismiss: () => void displayImpendingDeletionBanner: boolean - count?: Count, + count?: Count }): JSX.Element | null => { const { entitlements, experiments } = useDashboard() const allowAdvancedScheduling = @@ -27,23 +27,25 @@ export const ImpendingDeletionBanner = ({ // is merged up const allowWorkspaceActions = experiments.includes("workspace_actions") - if ( - !workspace || - !displayImpendingDeletion( - workspace, - allowAdvancedScheduling, - allowWorkspaceActions, - ) || - !displayImpendingDeletionBanner - ) { - return null - } + if ( + !workspace || + !displayImpendingDeletion( + workspace, + allowAdvancedScheduling, + allowWorkspaceActions, + ) || + !displayImpendingDeletionBanner + ) { + return null + } return ( - - {count === Count.Singular - ? `This workspace has been unused for ${formatDistanceToNow(Date.parse(workspace.last_used_at))} and is scheduled for deletion. To keep it, connect via SSH or the web terminal.` - : "You have workspaces that will be deleted soon due to inactivity. To keep these workspaces, connect to them via SSH or the web terminal."} - + + {count === Count.Singular + ? `This workspace has been unused for ${formatDistanceToNow( + Date.parse(workspace.last_used_at), + )} and is scheduled for deletion. To keep it, connect via SSH or the web terminal.` + : "You have workspaces that will be deleted soon due to inactivity. To keep these workspaces, connect to them via SSH or the web terminal."} + ) } diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx index 8a9ddeda680a5..1d6e1ace85c47 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx @@ -61,7 +61,7 @@ describe("WorkspacesPage", () => { renderWithAuth() const banner = await screen.findByText( - "You have workspaces that will be deleted soon.", + "You have workspaces that will be deleted soon due to inactivity. To keep these workspaces, connect to them via SSH or the web terminal.", ) const user = userEvent.setup() await user.click(screen.getByTestId("dismiss-banner-btn")) From 467efae7bca00b9853d0b13247ef896029da8e5c Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 24 May 2023 16:51:26 +0000 Subject: [PATCH 3/5] color-code banner --- .../WorkspaceDeletion/ImpendingDeletionBanner.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index f6fb7eea375ce..b6ee8d06a325d 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -2,7 +2,7 @@ import { Workspace } from "api/typesGenerated" import { displayImpendingDeletion } from "./utils" import { useDashboard } from "components/Dashboard/DashboardProvider" import { Alert } from "components/Alert/Alert" -import { formatDistanceToNow } from "date-fns" +import { formatDistanceToNow, differenceInDays } from "date-fns" export enum Count { Singular, @@ -39,8 +39,18 @@ export const ImpendingDeletionBanner = ({ return null } + // if deleting_at is 7 days away or less, display an 'error' banner to convey urgency to user + const daysUntilDelete = differenceInDays( + Date.parse(workspace.last_used_at), + new Date(), + ) + return ( - + {count === Count.Singular ? `This workspace has been unused for ${formatDistanceToNow( Date.parse(workspace.last_used_at), From ef3702ffa2e683e43ace0ca019c3499955ad9e21 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 24 May 2023 16:54:34 +0000 Subject: [PATCH 4/5] using warning instead --- .../components/WorkspaceDeletion/ImpendingDeletionBanner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index b6ee8d06a325d..7287f5f304571 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -47,7 +47,7 @@ export const ImpendingDeletionBanner = ({ return ( From 9c2dc40d19afc7b4b9639cef62a2bf971bba1790 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 25 May 2023 17:44:40 +0000 Subject: [PATCH 5/5] improve prop name for clarity --- site/src/components/Workspace/Workspace.tsx | 3 ++- .../WorkspaceDeletion/ImpendingDeletionBanner.tsx | 7 ++++--- site/src/pages/WorkspacesPage/WorkspacesPageView.tsx | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index cfd55cb869415..77509e079fd78 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -194,9 +194,10 @@ export const Workspace: FC> = ({ /> + {/* determines its own visibility */} saveLocal("dismissedWorkspace", workspace.id)} diff --git a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx index 7287f5f304571..dd110e255b361 100644 --- a/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx +++ b/site/src/components/WorkspaceDeletion/ImpendingDeletionBanner.tsx @@ -12,12 +12,12 @@ export enum Count { export const ImpendingDeletionBanner = ({ workspace, onDismiss, - displayImpendingDeletionBanner, + shouldRedisplayBanner, count = Count.Singular, }: { workspace?: Workspace onDismiss: () => void - displayImpendingDeletionBanner: boolean + shouldRedisplayBanner: boolean count?: Count }): JSX.Element | null => { const { entitlements, experiments } = useDashboard() @@ -34,7 +34,8 @@ export const ImpendingDeletionBanner = ({ allowAdvancedScheduling, allowWorkspaceActions, ) || - !displayImpendingDeletionBanner + // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion + !shouldRedisplayBanner ) { return null } diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index dd4b7b2cd7712..c6778e85ae1d5 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -117,9 +117,10 @@ export const WorkspacesPageView: FC< + {/* determines its own visibility */} workspace.deleting_at)} - displayImpendingDeletionBanner={isNewWorkspacesImpendingDeletion()} + shouldRedisplayBanner={isNewWorkspacesImpendingDeletion()} onDismiss={() => saveLocal( "dismissedWorkspaceList",