Skip to content

Commit 4058f04

Browse files
feat(site): add batch actions to the workspaces page (#9091)
1 parent c2c9da7 commit 4058f04

File tree

17 files changed

+505
-147
lines changed

17 files changed

+505
-147
lines changed

coderd/apidoc/docs.go

+4-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+4-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codersdk/deployment.go

+4
Original file line numberDiff line numberDiff line change
@@ -1931,6 +1931,9 @@ const (
19311931
// Template parameters insights
19321932
ExperimentTemplateParametersInsights Experiment = "template_parameters_insights"
19331933

1934+
// Workspaces batch actions
1935+
ExperimentWorkspacesBatchActions Experiment = "workspaces_batch_actions"
1936+
19341937
// Add new experiments here!
19351938
// ExperimentExample Experiment = "example"
19361939
)
@@ -1942,6 +1945,7 @@ const (
19421945
var ExperimentsAll = Experiments{
19431946
ExperimentDeploymentHealthPage,
19441947
ExperimentTemplateParametersInsights,
1948+
ExperimentWorkspacesBatchActions,
19451949
}
19461950

19471951
// Experiments is a list of experiments that are enabled for the deployment.

docs/api/schemas.md

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/api/typesGenerated.ts

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/components/PaginationStatus/PaginationStatus.tsx

-45
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { styled } from "@mui/material/styles"
2+
import Box from "@mui/material/Box"
3+
import Skeleton from "@mui/material/Skeleton"
4+
5+
export const TableToolbar = styled(Box)(({ theme }) => ({
6+
fontSize: 13,
7+
marginBottom: theme.spacing(1),
8+
marginTop: theme.spacing(0),
9+
height: 36, // The size of a small button
10+
color: theme.palette.text.secondary,
11+
"& strong": { color: theme.palette.text.primary },
12+
display: "flex",
13+
alignItems: "center",
14+
}))
15+
16+
type BasePaginationStatusProps = {
17+
label: string
18+
isLoading: boolean
19+
showing?: number
20+
total?: number
21+
}
22+
23+
type LoadedPaginationStatusProps = BasePaginationStatusProps & {
24+
isLoading: false
25+
showing: number
26+
total: number
27+
}
28+
29+
export const PaginationStatus = ({
30+
isLoading,
31+
showing,
32+
total,
33+
label,
34+
}: BasePaginationStatusProps | LoadedPaginationStatusProps) => {
35+
if (isLoading) {
36+
return (
37+
<Box sx={{ height: 24, display: "flex", alignItems: "center" }}>
38+
<Skeleton variant="text" width={160} height={16} />
39+
</Box>
40+
)
41+
}
42+
return (
43+
<Box>
44+
Showing <strong>{showing}</strong> of{" "}
45+
<strong>{total?.toLocaleString()}</strong> {label}
46+
</Box>
47+
)
48+
}

site/src/pages/AuditPage/AuditPageView.tsx

+12-7
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import { ComponentProps, FC } from "react"
2121
import { useTranslation } from "react-i18next"
2222
import { AuditPaywall } from "./AuditPaywall"
2323
import { AuditFilter } from "./AuditFilter"
24-
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
24+
import {
25+
PaginationStatus,
26+
TableToolbar,
27+
} from "components/TableToolbar/TableToolbar"
2528
import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase"
2629

2730
export const Language = {
@@ -73,12 +76,14 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
7376
<Cond condition={isAuditLogVisible}>
7477
<AuditFilter {...filterProps} />
7578

76-
<PaginationStatus
77-
isLoading={Boolean(isLoading)}
78-
showing={auditLogs?.length ?? 0}
79-
total={count ?? 0}
80-
label="audit logs"
81-
/>
79+
<TableToolbar>
80+
<PaginationStatus
81+
isLoading={Boolean(isLoading)}
82+
showing={auditLogs?.length ?? 0}
83+
total={count ?? 0}
84+
label="audit logs"
85+
/>
86+
</TableToolbar>
8287

8388
<TableContainer>
8489
<Table>

site/src/pages/GroupsPage/GroupPage.tsx

+12-7
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ import { pageTitle } from "utils/page"
3333
import { groupMachine } from "xServices/groups/groupXService"
3434
import { Maybe } from "components/Conditionals/Maybe"
3535
import { makeStyles } from "@mui/styles"
36-
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
36+
import {
37+
PaginationStatus,
38+
TableToolbar,
39+
} from "components/TableToolbar/TableToolbar"
3740
import { UserAvatar } from "components/UserAvatar/UserAvatar"
3841

3942
const AddGroupMember: React.FC<{
@@ -155,12 +158,14 @@ export const GroupPage: React.FC = () => {
155158
}}
156159
/>
157160
</Maybe>
158-
<PaginationStatus
159-
isLoading={Boolean(isLoading)}
160-
showing={group?.members.length ?? 0}
161-
total={group?.members.length ?? 0}
162-
label="members"
163-
/>
161+
<TableToolbar>
162+
<PaginationStatus
163+
isLoading={Boolean(isLoading)}
164+
showing={group?.members.length ?? 0}
165+
total={group?.members.length ?? 0}
166+
label="members"
167+
/>
168+
</TableToolbar>
164169

165170
<TableContainer>
166171
<Table>

site/src/pages/UsersPage/UsersPageView.tsx

+12-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { PaginationMachineRef } from "xServices/pagination/paginationXService"
44
import * as TypesGen from "../../api/typesGenerated"
55
import { UsersTable } from "../../components/UsersTable/UsersTable"
66
import { UsersFilter } from "./UsersFilter"
7-
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
7+
import {
8+
PaginationStatus,
9+
TableToolbar,
10+
} from "components/TableToolbar/TableToolbar"
811

912
export const Language = {
1013
activeUsersFilterName: "Active users",
@@ -60,12 +63,14 @@ export const UsersPageView: FC<React.PropsWithChildren<UsersPageViewProps>> = ({
6063
<>
6164
<UsersFilter {...filterProps} />
6265

63-
<PaginationStatus
64-
isLoading={Boolean(isLoading)}
65-
showing={users?.length ?? 0}
66-
total={count ?? 0}
67-
label="users"
68-
/>
66+
<TableToolbar>
67+
<PaginationStatus
68+
isLoading={Boolean(isLoading)}
69+
showing={users?.length ?? 0}
70+
total={count ?? 0}
71+
label="users"
72+
/>
73+
</TableToolbar>
6974

7075
<UsersTable
7176
users={users}

site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx

+41-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
import { screen } from "@testing-library/react"
1+
import { screen, waitFor, within } from "@testing-library/react"
22
import { rest } from "msw"
33
import * as CreateDayString from "utils/createDayString"
44
import { MockWorkspace, MockWorkspacesResponse } from "testHelpers/entities"
5-
import { renderWithAuth } from "testHelpers/renderHelpers"
5+
import {
6+
renderWithAuth,
7+
waitForLoaderToBeRemoved,
8+
} from "testHelpers/renderHelpers"
69
import { server } from "testHelpers/server"
710
import WorkspacesPage from "./WorkspacesPage"
811
import { i18n } from "i18n"
12+
import userEvent from "@testing-library/user-event"
13+
import * as API from "api/api"
14+
import { Workspace } from "api/typesGenerated"
915

1016
const { t } = i18n
1117

@@ -40,4 +46,37 @@ describe("WorkspacesPage", () => {
4046
)
4147
expect(templateDisplayNames).toHaveLength(MockWorkspacesResponse.count)
4248
})
49+
50+
it("deletes only the selected workspaces", async () => {
51+
const workspaces = [
52+
{ ...MockWorkspace, id: "1" },
53+
{ ...MockWorkspace, id: "2" },
54+
{ ...MockWorkspace, id: "3" },
55+
]
56+
jest
57+
.spyOn(API, "getWorkspaces")
58+
.mockResolvedValue({ workspaces, count: workspaces.length })
59+
const deleteWorkspace = jest.spyOn(API, "deleteWorkspace")
60+
const user = userEvent.setup()
61+
renderWithAuth(<WorkspacesPage />)
62+
await waitForLoaderToBeRemoved()
63+
64+
await user.click(getWorkspaceCheckbox(workspaces[0]))
65+
await user.click(getWorkspaceCheckbox(workspaces[1]))
66+
await user.click(screen.getByRole("button", { name: /delete all/i }))
67+
await user.type(screen.getByLabelText(/type delete to confirm/i), "DELETE")
68+
await user.click(screen.getByTestId("confirm-button"))
69+
70+
await waitFor(() => {
71+
expect(deleteWorkspace).toHaveBeenCalledTimes(2)
72+
})
73+
expect(deleteWorkspace).toHaveBeenCalledWith(workspaces[0].id)
74+
expect(deleteWorkspace).toHaveBeenCalledWith(workspaces[1].id)
75+
})
4376
})
77+
78+
const getWorkspaceCheckbox = (workspace: Workspace) => {
79+
return within(screen.getByTestId(`checkbox-${workspace.id}`)).getByRole(
80+
"checkbox",
81+
)
82+
}

0 commit comments

Comments
 (0)