Skip to content

Commit 039d0ba

Browse files
committed
Workspace batch actions
1 parent 091c00b commit 039d0ba

File tree

3 files changed

+158
-36
lines changed

3 files changed

+158
-36
lines changed

site/src/pages/WorkspacesPage/WorkspacesPage.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useSearchParams } from "react-router-dom"
1212
import { useFilter } from "components/Filter/filter"
1313
import { useUserFilterMenu } from "components/Filter/UserFilter"
1414
import { getWorkspaces } from "api/api"
15+
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
1516

1617
const WorkspacesPage: FC = () => {
1718
const [lockedWorkspaces, setLockedWorkspaces] = useState<Workspace[]>([])
@@ -55,8 +56,9 @@ const WorkspacesPage: FC = () => {
5556
setLockedWorkspaces([])
5657
}
5758
}, [experimentEnabled, data, filterProps.filter.query])
58-
5959
const updateWorkspace = useWorkspaceUpdate(queryKey)
60+
const [checkedWorkspaces, setCheckedWorkspaces] = useState<Workspace[]>([])
61+
const [isDeletingAll, setIsDeletingAll] = useState(false)
6062

6163
return (
6264
<>
@@ -65,6 +67,8 @@ const WorkspacesPage: FC = () => {
6567
</Helmet>
6668

6769
<WorkspacesPageView
70+
checkedWorkspaces={checkedWorkspaces}
71+
onCheckChange={setCheckedWorkspaces}
6872
workspaces={data?.workspaces}
6973
lockedWorkspaces={lockedWorkspaces}
7074
error={error}
@@ -76,6 +80,25 @@ const WorkspacesPage: FC = () => {
7680
onUpdateWorkspace={(workspace) => {
7781
updateWorkspace.mutate(workspace)
7882
}}
83+
onDeleteAll={() => {
84+
setIsDeletingAll(true)
85+
}}
86+
/>
87+
88+
<ConfirmDialog
89+
type="delete"
90+
title={`Delete ${checkedWorkspaces?.length} ${
91+
checkedWorkspaces.length === 1 ? "workspace" : "workspaces"
92+
}`}
93+
description="Deleting these workspaces is irreversible! Are you sure you want to proceed?"
94+
open={isDeletingAll}
95+
confirmLoading={false}
96+
onConfirm={() => {
97+
alert("DO IT!")
98+
}}
99+
onClose={() => {
100+
setIsDeletingAll(false)
101+
}}
79102
/>
80103
</>
81104
)

site/src/pages/WorkspacesPage/WorkspacesPageView.tsx

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"
1919
import { WorkspacesFilter } from "./filter/filter"
2020
import { hasError, isApiValidationError } from "api/errors"
2121
import { PaginationStatus } from "components/PaginationStatus/PaginationStatus"
22+
import Box from "@mui/material/Box"
23+
import Button from "@mui/material/Button"
24+
import DeleteOutlined from "@mui/icons-material/DeleteOutlined"
2225

2326
export const Language = {
2427
pageTitle: "Workspaces",
@@ -33,12 +36,15 @@ export interface WorkspacesPageViewProps {
3336
error: unknown
3437
workspaces?: Workspace[]
3538
lockedWorkspaces?: Workspace[]
39+
checkedWorkspaces: Workspace[]
3640
count?: number
3741
filterProps: ComponentProps<typeof WorkspacesFilter>
3842
page: number
3943
limit: number
4044
onPageChange: (page: number) => void
4145
onUpdateWorkspace: (workspace: Workspace) => void
46+
onCheckChange: (checkedWorkspaces: Workspace[]) => void
47+
onDeleteAll: () => void
4248
}
4349

4450
export const WorkspacesPageView: FC<
@@ -53,6 +59,9 @@ export const WorkspacesPageView: FC<
5359
onPageChange,
5460
onUpdateWorkspace,
5561
page,
62+
checkedWorkspaces,
63+
onCheckChange,
64+
onDeleteAll,
5665
}) => {
5766
const { saveLocal } = useLocalStorage()
5867

@@ -102,17 +111,50 @@ export const WorkspacesPageView: FC<
102111
<WorkspacesFilter error={error} {...filterProps} />
103112
</Stack>
104113

105-
<PaginationStatus
106-
isLoading={!workspaces && !error}
107-
showing={workspaces?.length ?? 0}
108-
total={count ?? 0}
109-
label="workspaces"
110-
/>
114+
{checkedWorkspaces.length > 0 ? (
115+
<Box
116+
sx={{
117+
position: "relative",
118+
display: "flex",
119+
alignItems: "center",
120+
fontSize: 13,
121+
mb: 2,
122+
mt: 1,
123+
color: (theme) => theme.palette.text.secondary,
124+
"& strong": { color: (theme) => theme.palette.text.primary },
125+
}}
126+
>
127+
<Box>
128+
Selected <strong>{checkedWorkspaces.length}</strong> of{" "}
129+
<strong>{workspaces?.length}</strong>{" "}
130+
{checkedWorkspaces.length === 1 ? "workspace" : "workspaces"}
131+
</Box>
132+
133+
<Box sx={{ position: "absolute", right: 0 }}>
134+
<Button
135+
size="small"
136+
startIcon={<DeleteOutlined />}
137+
onClick={onDeleteAll}
138+
>
139+
Delete all
140+
</Button>
141+
</Box>
142+
</Box>
143+
) : (
144+
<PaginationStatus
145+
isLoading={!workspaces && !error}
146+
showing={workspaces?.length ?? 0}
147+
total={count ?? 0}
148+
label="workspaces"
149+
/>
150+
)}
111151

112152
<WorkspacesTable
113153
workspaces={workspaces}
114154
isUsingFilter={filterProps.filter.used}
115155
onUpdateWorkspace={onUpdateWorkspace}
156+
checkedWorkspaces={checkedWorkspaces}
157+
onCheckChange={onCheckChange}
116158
/>
117159
{count !== undefined && (
118160
<PaginationWidgetBase

site/src/pages/WorkspacesPage/WorkspacesTable.tsx

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,23 @@ import { LastUsed } from "components/LastUsed/LastUsed"
3131
import { WorkspaceOutdatedTooltip } from "components/Tooltips"
3232
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
3333
import { getDisplayWorkspaceTemplateName } from "utils/workspace"
34+
import Checkbox from "@mui/material/Checkbox"
3435

3536
export interface WorkspacesTableProps {
3637
workspaces?: Workspace[]
38+
checkedWorkspaces: Workspace[]
39+
error?: unknown
3740
isUsingFilter: boolean
3841
onUpdateWorkspace: (workspace: Workspace) => void
39-
error?: unknown
42+
onCheckChange: (checkedWorkspaces: Workspace[]) => void
4043
}
4144

4245
export const WorkspacesTable: FC<WorkspacesTableProps> = ({
4346
workspaces,
47+
checkedWorkspaces,
4448
isUsingFilter,
4549
onUpdateWorkspace,
50+
onCheckChange,
4651
}) => {
4752
const { t } = useTranslation("workspacesPage")
4853
const styles = useStyles()
@@ -52,7 +57,31 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
5257
<Table>
5358
<TableHead>
5459
<TableRow>
55-
<TableCell width="40%">Name</TableCell>
60+
<TableCell
61+
width="40%"
62+
sx={{
63+
paddingLeft: (theme) => `${theme.spacing(1.5)} !important`,
64+
}}
65+
>
66+
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
67+
<Checkbox
68+
checked={checkedWorkspaces.length === workspaces?.length}
69+
size="small"
70+
onChange={(_, checked) => {
71+
if (!workspaces) {
72+
return
73+
}
74+
75+
if (!checked) {
76+
onCheckChange([])
77+
} else {
78+
onCheckChange(workspaces)
79+
}
80+
}}
81+
/>
82+
Name
83+
</Box>
84+
</TableCell>
5685
<TableCell width="25%">Template</TableCell>
5786
<TableCell width="20%">Last used</TableCell>
5887
<TableCell width="15%">Status</TableCell>
@@ -94,33 +123,61 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
94123
{workspaces &&
95124
workspaces.map((workspace) => (
96125
<WorkspacesRow workspace={workspace} key={workspace.id}>
97-
<TableCell>
98-
<AvatarData
99-
title={
100-
<Stack direction="row" spacing={0} alignItems="center">
101-
{workspace.name}
102-
{workspace.outdated && (
103-
<WorkspaceOutdatedTooltip
104-
templateName={workspace.template_name}
105-
templateId={workspace.template_id}
106-
onUpdateVersion={() => {
107-
onUpdateWorkspace(workspace)
108-
}}
109-
/>
110-
)}
111-
</Stack>
112-
}
113-
subtitle={workspace.owner_name}
114-
avatar={
115-
<Avatar
116-
src={workspace.template_icon}
117-
variant={workspace.template_icon ? "square" : undefined}
118-
fitImage={Boolean(workspace.template_icon)}
119-
>
120-
{workspace.name}
121-
</Avatar>
122-
}
123-
/>
126+
<TableCell
127+
sx={{
128+
paddingLeft: (theme) => `${theme.spacing(1.5)} !important`,
129+
}}
130+
>
131+
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
132+
<Checkbox
133+
size="small"
134+
checked={checkedWorkspaces.some(
135+
(w) => w.id === workspace.id,
136+
)}
137+
onClick={(e) => {
138+
e.stopPropagation()
139+
}}
140+
onChange={(e) => {
141+
if (e.currentTarget.checked) {
142+
onCheckChange([...checkedWorkspaces, workspace])
143+
} else {
144+
onCheckChange(
145+
checkedWorkspaces.filter(
146+
(w) => w.id !== workspace.id,
147+
),
148+
)
149+
}
150+
}}
151+
/>
152+
<AvatarData
153+
title={
154+
<Stack direction="row" spacing={0} alignItems="center">
155+
{workspace.name}
156+
{workspace.outdated && (
157+
<WorkspaceOutdatedTooltip
158+
templateName={workspace.template_name}
159+
templateId={workspace.template_id}
160+
onUpdateVersion={() => {
161+
onUpdateWorkspace(workspace)
162+
}}
163+
/>
164+
)}
165+
</Stack>
166+
}
167+
subtitle={workspace.owner_name}
168+
avatar={
169+
<Avatar
170+
src={workspace.template_icon}
171+
variant={
172+
workspace.template_icon ? "square" : undefined
173+
}
174+
fitImage={Boolean(workspace.template_icon)}
175+
>
176+
{workspace.name}
177+
</Avatar>
178+
}
179+
/>
180+
</Box>
124181
</TableCell>
125182

126183
<TableCell>

0 commit comments

Comments
 (0)