mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user