diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index a67e946fe57e5..77509e079fd78 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`)} + /> + + + {/* determines its own visibility */} + 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..dd110e255b361 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, differenceInDays } from "date-fns" + +export enum Count { + Singular, + Multiple, +} 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() const allowAdvancedScheduling = @@ -20,21 +27,36 @@ export const ImpendingDeletionBanner = ({ // is merged up const allowWorkspaceActions = experiments.includes("workspace_actions") + if ( + !workspace || + !displayImpendingDeletion( + workspace, + allowAdvancedScheduling, + allowWorkspaceActions, + ) || + // Banners should be redisplayed after dismissal when additional workspaces are newly scheduled for deletion + !shouldRedisplayBanner + ) { + 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 ( - - - 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/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")) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 8d9e2dbfaffcd..c6778e85ae1d5 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 = { @@ -117,15 +117,17 @@ export const WorkspacesPageView: FC< + {/* determines its own visibility */} workspace.deleting_at)} - displayImpendingDeletionBanner={isNewWorkspacesImpendingDeletion()} + shouldRedisplayBanner={isNewWorkspacesImpendingDeletion()} onDismiss={() => saveLocal( "dismissedWorkspaceList", JSON.stringify(workspaceIdsWithImpendingDeletions), ) } + count={Count.Multiple} />