feat: add impending deletion banner to workspace page (#7634)

* add banner to workspace page

* fix prettier and lint

* color-code banner

* using warning instead

* improve prop name for clarity
This commit is contained in:
Kira Pilot
2023-05-25 10:55:41 -07:00
committed by GitHub
parent 5d2b2c577b
commit 0c665235ff
7 changed files with 72 additions and 48 deletions
+21 -4
View File
@@ -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<React.PropsWithChildren<WorkspaceProps>> = ({
const navigate = useNavigate()
const serverVersion = buildInfo?.version || ""
const { t } = useTranslation("workspacePage")
const { saveLocal, getLocal } = useLocalStorage()
const buildError = Boolean(workspaceErrors[WorkspaceErrors.BUILD_ERROR]) && (
<ErrorAlert
@@ -183,10 +187,23 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
{buildError}
{cancellationError}
<WorkspaceDeletedBanner
workspace={workspace}
handleClick={() => navigate(`/templates`)}
/>
<ChooseOne>
<Cond condition={workspace.latest_build.status === "deleted"}>
<WorkspaceDeletedBanner
handleClick={() => navigate(`/templates`)}
/>
</Cond>
<Cond>
{/* <ImpendingDeletionBanner/> determines its own visibility */}
<ImpendingDeletionBanner
workspace={workspace}
shouldRedisplayBanner={
getLocal("dismissedWorkspace") !== workspace.id
}
onDismiss={() => saveLocal("dismissedWorkspace", workspace.id)}
/>
</Cond>
</ChooseOne>
<TemplateVersionWarnings warnings={templateWarnings} />
@@ -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<WorkspaceDeletedBannerProps> = (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",
},
},
}
@@ -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<WorkspaceDeletedBannerProps>
> = ({ workspace, handleClick }) => {
> = ({ handleClick }) => {
const { t } = useTranslation("workspacePage")
const NewWorkspaceButton = (
@@ -22,10 +19,8 @@ export const WorkspaceDeletedBanner: FC<
)
return (
<Maybe condition={workspace.latest_build.status === "deleted"}>
<Alert severity="warning" actions={[NewWorkspaceButton]}>
{t("warningsAndErrors.workspaceDeletedWarning")}
</Alert>
</Maybe>
<Alert severity="warning" actions={[NewWorkspaceButton]}>
{t("warningsAndErrors.workspaceDeletedWarning")}
</Alert>
)
}
@@ -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 (
<Maybe
condition={Boolean(
workspace &&
displayImpendingDeletion(
workspace,
allowAdvancedScheduling,
allowWorkspaceActions,
) &&
displayImpendingDeletionBanner,
)}
<Alert
severity={daysUntilDelete <= 7 ? "warning" : "info"}
onDismiss={onDismiss}
dismissible
>
<Alert severity="info" onDismiss={onDismiss} dismissible>
You have workspaces that will be deleted soon.
</Alert>
</Maybe>
{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."}
</Alert>
)
}
@@ -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
},
},
)
@@ -61,7 +61,7 @@ describe("WorkspacesPage", () => {
renderWithAuth(<WorkspacesPage />)
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"))
@@ -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<
<Maybe condition={Boolean(error)}>
<ErrorAlert error={error} />
</Maybe>
{/* <ImpendingDeletionBanner/> determines its own visibility */}
<ImpendingDeletionBanner
workspace={workspaces?.find((workspace) => workspace.deleting_at)}
displayImpendingDeletionBanner={isNewWorkspacesImpendingDeletion()}
shouldRedisplayBanner={isNewWorkspacesImpendingDeletion()}
onDismiss={() =>
saveLocal(
"dismissedWorkspaceList",
JSON.stringify(workspaceIdsWithImpendingDeletions),
)
}
count={Count.Multiple}
/>
<SearchBarWithFilter