feat: add workspaces banner for impending deletion (#7538)

* feat: add workspaces banner for impending deletion

* added storybook

* remove storybook - cannot add because of hook used in badge component
This commit is contained in:
Kira Pilot
2023-05-16 07:01:22 -07:00
committed by GitHub
parent 97b4743a47
commit dca77ba487
7 changed files with 164 additions and 25 deletions

View File

@ -37,7 +37,11 @@ export const AlertBannerCtas: FC<AlertBannerCtasProps> = ({
{/* close CTA */}
{dismissible && (
<Button size="small" onClick={() => setOpen(false)}>
<Button
size="small"
onClick={() => setOpen(false)}
data-testid="dismiss-banner-btn"
>
{t("ctas.dismissCta")}
</Button>
)}

11
site/src/hooks/index.ts Normal file
View File

@ -0,0 +1,11 @@
export * from "./useClickable"
export * from "./useClickableTableRow"
export * from "./useClipboard"
export * from "./useFeatureVisibility"
export * from "./useFilter"
export * from "./useLocalStorage"
export * from "./useMe"
export * from "./useOrganizationId"
export * from "./usePagination"
export * from "./usePermissions"
export * from "./useTab"

View File

@ -0,0 +1,25 @@
interface UseLocalStorage {
saveLocal: (arg0: string, arg1: string) => void
getLocal: (arg0: string) => string | undefined
clearLocal: (arg0: string) => void
}
export const useLocalStorage = (): UseLocalStorage => {
return {
saveLocal,
getLocal,
clearLocal,
}
}
const saveLocal = (itemKey: string, itemValue: string): void => {
window.localStorage.setItem(itemKey, itemValue)
}
const getLocal = (itemKey: string): string | undefined => {
return localStorage.getItem(itemKey) ?? undefined
}
const clearLocal = (itemKey: string): void => {
localStorage.removeItem(itemKey)
}

View File

@ -4,11 +4,15 @@ import * as CreateDayString from "utils/createDayString"
import {
MockWorkspace,
MockWorkspacesResponse,
} from "../../testHelpers/entities"
import { history, render } from "../../testHelpers/renderHelpers"
import { server } from "../../testHelpers/server"
MockEntitlementsWithScheduling,
MockWorkspacesResponseWithDeletions,
} from "testHelpers/entities"
import { history, renderWithAuth } from "testHelpers/renderHelpers"
import { server } from "testHelpers/server"
import WorkspacesPage from "./WorkspacesPage"
import { i18n } from "i18n"
import * as API from "api/api"
import userEvent from "@testing-library/user-event"
const { t } = i18n
@ -29,7 +33,7 @@ describe("WorkspacesPage", () => {
)
// When
render(<WorkspacesPage />)
renderWithAuth(<WorkspacesPage />)
// Then
const text = t("emptyCreateWorkspaceMessage", { ns: "workspacesPage" })
@ -37,11 +41,31 @@ describe("WorkspacesPage", () => {
})
it("renders a filled workspaces page", async () => {
render(<WorkspacesPage />)
renderWithAuth(<WorkspacesPage />)
await screen.findByText(`${MockWorkspace.name}1`)
const templateDisplayNames = await screen.findAllByText(
`${MockWorkspace.template_display_name}`,
)
expect(templateDisplayNames).toHaveLength(MockWorkspacesResponse.count)
})
it("displays banner for impending deletions", async () => {
jest
.spyOn(API, "getEntitlements")
.mockResolvedValue(MockEntitlementsWithScheduling)
jest
.spyOn(API, "getWorkspaces")
.mockResolvedValue(MockWorkspacesResponseWithDeletions)
renderWithAuth(<WorkspacesPage />)
const banner = await screen.findByText(
"You have workspaces that will be deleted soon.",
)
const user = userEvent.setup()
await user.click(screen.getByTestId("dismiss-banner-btn"))
expect(banner).toBeEmptyDOMElement
})
})

View File

@ -6,10 +6,18 @@ import { workspaceFilterQuery } from "utils/filters"
import { pageTitle } from "utils/page"
import { useWorkspacesData, useWorkspaceUpdate } from "./data"
import { WorkspacesPageView } from "./WorkspacesPageView"
import { useDashboard } from "components/Dashboard/DashboardProvider"
const WorkspacesPage: FC = () => {
const filter = useFilter(workspaceFilterQuery.me)
const pagination = usePagination()
const { entitlements, experiments } = useDashboard()
const allowAdvancedScheduling =
entitlements.features["advanced_template_scheduling"].enabled
// This check can be removed when https://github.com/coder/coder/milestone/19
// is merged up
const allowWorkspaceActions = experiments.includes("workspace_actions")
const { data, error, queryKey } = useWorkspacesData({
...pagination,
...filter,
@ -34,6 +42,8 @@ const WorkspacesPage: FC = () => {
onUpdateWorkspace={(workspace) => {
updateWorkspace.mutate(workspace)
}}
allowAdvancedScheduling={allowAdvancedScheduling}
allowWorkspaceActions={allowWorkspaceActions}
/>
</>
)

View File

@ -5,17 +5,19 @@ import { Maybe } from "components/Conditionals/Maybe"
import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase"
import { FC } from "react"
import { Link as RouterLink } from "react-router-dom"
import { Margins } from "../../components/Margins/Margins"
import { Margins } from "components/Margins/Margins"
import {
PageHeader,
PageHeaderSubtitle,
PageHeaderTitle,
} from "../../components/PageHeader/PageHeader"
import { SearchBarWithFilter } from "../../components/SearchBarWithFilter/SearchBarWithFilter"
import { Stack } from "../../components/Stack/Stack"
import { WorkspaceHelpTooltip } from "../../components/Tooltips"
import { WorkspacesTable } from "../../components/WorkspacesTable/WorkspacesTable"
import { workspaceFilterQuery } from "../../utils/filters"
} from "components/PageHeader/PageHeader"
import { SearchBarWithFilter } from "components/SearchBarWithFilter/SearchBarWithFilter"
import { Stack } from "components/Stack/Stack"
import { WorkspaceHelpTooltip } from "components/Tooltips"
import { WorkspacesTable } from "components/WorkspacesTable/WorkspacesTable"
import { workspaceFilterQuery } from "utils/filters"
import { useLocalStorage } from "hooks"
import difference from "lodash/difference"
export const Language = {
pageTitle: "Workspaces",
@ -26,6 +28,19 @@ export const Language = {
template: "Template",
}
const presetFilters = [
{ query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton },
{ query: workspaceFilterQuery.all, name: Language.allWorkspacesButton },
{
query: workspaceFilterQuery.running,
name: Language.runningWorkspacesButton,
},
{
query: workspaceFilterQuery.failed,
name: "Failed workspaces",
},
]
export interface WorkspacesPageViewProps {
error: unknown
workspaces?: Workspace[]
@ -36,6 +51,8 @@ export interface WorkspacesPageViewProps {
onPageChange: (page: number) => void
onFilter: (query: string) => void
onUpdateWorkspace: (workspace: Workspace) => void
allowAdvancedScheduling: boolean
allowWorkspaceActions: boolean
}
export const WorkspacesPageView: FC<
@ -50,19 +67,43 @@ export const WorkspacesPageView: FC<
onFilter,
onPageChange,
onUpdateWorkspace,
allowAdvancedScheduling,
allowWorkspaceActions,
}) => {
const presetFilters = [
{ query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton },
{ query: workspaceFilterQuery.all, name: Language.allWorkspacesButton },
{
query: workspaceFilterQuery.running,
name: Language.runningWorkspacesButton,
},
{
query: workspaceFilterQuery.failed,
name: "Failed workspaces",
},
]
const { saveLocal, getLocal } = useLocalStorage()
const workspaceIdsWithImpendingDeletions = workspaces
?.filter((workspace) => workspace.deleting_at)
.map((workspace) => workspace.id)
/**
* Returns a boolean indicating if there are workspaces that have been
* recently marked for deletion but are not in local storage.
* If there are, we want to alert the user so they can potentially take action
* before deletion takes place.
* @returns {boolean}
*/
const isNewWorkspacesImpendingDeletion = (): boolean => {
const dismissedList = getLocal("dismissedWorkspaceList")
if (!dismissedList) {
return true
}
const diff = difference(
workspaceIdsWithImpendingDeletions,
JSON.parse(dismissedList),
)
return diff && diff.length > 0
}
const displayImpendingDeletionBanner =
(allowAdvancedScheduling &&
allowWorkspaceActions &&
workspaceIdsWithImpendingDeletions &&
workspaceIdsWithImpendingDeletions.length > 0 &&
isNewWorkspacesImpendingDeletion()) ??
false
return (
<Margins>
@ -94,6 +135,19 @@ export const WorkspacesPageView: FC<
}
/>
</Maybe>
<Maybe condition={displayImpendingDeletionBanner}>
<AlertBanner
severity="info"
onDismiss={() =>
saveLocal(
"dismissedWorkspaceList",
JSON.stringify(workspaceIdsWithImpendingDeletions),
)
}
dismissible
text="You have workspaces that will be deleted soon."
/>
</Maybe>
<SearchBarWithFilter
filter={filter}

View File

@ -824,6 +824,12 @@ export const MockDeletingWorkspace: TypesGen.Workspace = {
status: "deleting",
},
}
export const MockWorkspaceWithDeletion = {
...MockWorkspace,
deleting_at: new Date().toISOString(),
}
export const MockDeletedWorkspace: TypesGen.Workspace = {
...MockWorkspace,
id: "test-deleted-workspace",
@ -857,6 +863,11 @@ export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = {
count: 26,
}
export const MockWorkspacesResponseWithDeletions = {
workspaces: [...MockWorkspacesResponse.workspaces, MockWorkspaceWithDeletion],
count: MockWorkspacesResponse.count + 1,
}
export const MockTemplateVersionParameter1: TypesGen.TemplateVersionParameter =
{
name: "first_parameter",