Skip to content

Commit 800a49e

Browse files
committed
feat: add parent checkbox for grouped resource permissions
1 parent 49f9157 commit 800a49e

File tree

1 file changed

+123
-40
lines changed

1 file changed

+123
-40
lines changed

site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx

Lines changed: 123 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,6 @@ export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
146146
);
147147
};
148148

149-
interface ActionCheckboxesProps {
150-
permissions: readonly Permission[] | undefined;
151-
form: ReturnType<typeof useFormik<Role>> & { values: Role };
152-
allResources: boolean;
153-
}
154-
155149
const ResourceActionComparator = (
156150
p: Permission,
157151
resource: string,
@@ -160,6 +154,7 @@ const ResourceActionComparator = (
160154
p.resource_type === resource &&
161155
(p.action.toString() === "*" || p.action === action);
162156

157+
// the subset of resources that are useful for most users
163158
const DEFAULT_RESOURCES = [
164159
"audit_log",
165160
"group",
@@ -177,6 +172,12 @@ const filteredRBACResourceActions = Object.fromEntries(
177172
),
178173
);
179174

175+
interface ActionCheckboxesProps {
176+
permissions: readonly Permission[];
177+
form: ReturnType<typeof useFormik<Role>> & { values: Role };
178+
allResources: boolean;
179+
}
180+
180181
const ActionCheckboxes: FC<ActionCheckboxesProps> = ({
181182
permissions,
182183
form,
@@ -185,6 +186,10 @@ const ActionCheckboxes: FC<ActionCheckboxesProps> = ({
185186
const [checkedActions, setCheckActions] = useState(permissions);
186187
const [showAllResources, setShowAllResources] = useState(allResources);
187188

189+
const resourceActions = showAllResources
190+
? RBACResourceActions
191+
: filteredRBACResourceActions;
192+
188193
const handleActionCheckChange = async (
189194
e: ChangeEvent<HTMLInputElement>,
190195
form: ReturnType<typeof useFormik<Role>> & { values: Role },
@@ -194,7 +199,7 @@ const ActionCheckboxes: FC<ActionCheckboxesProps> = ({
194199

195200
const newPermissions = checked
196201
? [
197-
...(checkedActions ?? []),
202+
...checkedActions,
198203
{
199204
negate: false,
200205
resource_type: resource_type as RBACResource,
@@ -209,9 +214,36 @@ const ActionCheckboxes: FC<ActionCheckboxesProps> = ({
209214
await form.setFieldValue("organization_permissions", newPermissions);
210215
};
211216

212-
const resourceActions = showAllResources
213-
? RBACResourceActions
214-
: filteredRBACResourceActions;
217+
const handleResourceCheckChange = async (
218+
e: ChangeEvent<HTMLInputElement>,
219+
form: ReturnType<typeof useFormik<Role>> & { values: Role },
220+
indeterminate: boolean,
221+
) => {
222+
const { name, checked } = e.currentTarget;
223+
const resource = name as RBACResource;
224+
225+
const resourceActionsForResource = resourceActions[resource] || {};
226+
227+
const newCheckedActions =
228+
!checked || indeterminate
229+
? checkedActions?.filter((p) => p.resource_type !== resource)
230+
: checkedActions;
231+
232+
const newPermissions =
233+
checked || indeterminate
234+
? [
235+
...newCheckedActions,
236+
...Object.keys(resourceActionsForResource).map((resourceKey) => ({
237+
negate: false,
238+
resource_type: resource as RBACResource,
239+
action: resourceKey as RBACAction,
240+
})),
241+
]
242+
: [...newCheckedActions];
243+
244+
setCheckActions(newPermissions);
245+
await form.setFieldValue("organization_permissions", newPermissions);
246+
};
215247

216248
return (
217249
<TableContainer>
@@ -233,36 +265,17 @@ const ActionCheckboxes: FC<ActionCheckboxesProps> = ({
233265
<TableBody>
234266
{Object.entries(resourceActions).map(([resourceKey, value]) => {
235267
return (
236-
<TableRow key={resourceKey}>
237-
<TableCell sx={{ paddingLeft: 2 }} colSpan={2}>
238-
<li key={resourceKey} css={styles.checkBoxes}>
239-
{resourceKey}
240-
<ul css={styles.checkBoxes}>
241-
{Object.entries(value).map(([actionKey, value]) => (
242-
<li key={actionKey}>
243-
<span css={styles.actionText}>
244-
<Checkbox
245-
size="small"
246-
name={`${resourceKey}:${actionKey}`}
247-
checked={checkedActions?.some((p) =>
248-
ResourceActionComparator(
249-
p,
250-
resourceKey,
251-
actionKey,
252-
),
253-
)}
254-
onChange={(e) => handleActionCheckChange(e, form)}
255-
/>
256-
{actionKey}
257-
</span>{" "}
258-
&ndash;{" "}
259-
<span css={styles.actionDescription}>{value}</span>
260-
</li>
261-
))}
262-
</ul>
263-
</li>
264-
</TableCell>
265-
</TableRow>
268+
<PermissionCheckboxGroup
269+
key={resourceKey}
270+
checkedActions={checkedActions?.filter(
271+
(a) => a.resource_type === resourceKey,
272+
)}
273+
resourceKey={resourceKey}
274+
value={value}
275+
form={form}
276+
handleActionCheckChange={handleActionCheckChange}
277+
handleResourceCheckChange={handleResourceCheckChange}
278+
/>
266279
);
267280
})}
268281
</TableBody>
@@ -285,6 +298,76 @@ const ActionCheckboxes: FC<ActionCheckboxesProps> = ({
285298
);
286299
};
287300

301+
interface PermissionCheckboxGroupProps {
302+
checkedActions: readonly Permission[];
303+
resourceKey: string;
304+
value: Partial<Record<RBACAction, string>>;
305+
form: ReturnType<typeof useFormik<Role>> & { values: Role };
306+
handleActionCheckChange: (
307+
e: ChangeEvent<HTMLInputElement>,
308+
form: ReturnType<typeof useFormik<Role>> & { values: Role },
309+
) => Promise<void>;
310+
handleResourceCheckChange: (
311+
e: ChangeEvent<HTMLInputElement>,
312+
form: ReturnType<typeof useFormik<Role>> & { values: Role },
313+
indeterminate: boolean,
314+
) => Promise<void>;
315+
}
316+
317+
const PermissionCheckboxGroup: FC<PermissionCheckboxGroupProps> = ({
318+
checkedActions,
319+
resourceKey,
320+
value,
321+
form,
322+
handleActionCheckChange,
323+
handleResourceCheckChange,
324+
}) => {
325+
return (
326+
<TableRow key={resourceKey}>
327+
<TableCell sx={{ paddingLeft: 2 }} colSpan={2}>
328+
<li key={resourceKey} css={styles.checkBoxes}>
329+
<Checkbox
330+
size="small"
331+
name={`${resourceKey}`}
332+
checked={checkedActions.length === Object.keys(value).length}
333+
indeterminate={
334+
checkedActions.length > 0 &&
335+
checkedActions.length < Object.keys(value).length
336+
}
337+
onChange={(e) =>
338+
handleResourceCheckChange(
339+
e,
340+
form,
341+
checkedActions.length > 0 &&
342+
checkedActions.length < Object.keys(value).length,
343+
)
344+
}
345+
/>
346+
{resourceKey}
347+
<ul css={styles.checkBoxes}>
348+
{Object.entries(value).map(([actionKey, value]) => (
349+
<li key={actionKey}>
350+
<span css={styles.actionText}>
351+
<Checkbox
352+
size="small"
353+
name={`${resourceKey}:${actionKey}`}
354+
checked={checkedActions.some((p) =>
355+
ResourceActionComparator(p, resourceKey, actionKey),
356+
)}
357+
onChange={(e) => handleActionCheckChange(e, form)}
358+
/>
359+
{actionKey}
360+
</span>{" "}
361+
&ndash; <span css={styles.actionDescription}>{value}</span>
362+
</li>
363+
))}
364+
</ul>
365+
</li>
366+
</TableCell>
367+
</TableRow>
368+
);
369+
};
370+
288371
interface ShowAllResourcesCheckboxProps {
289372
showAllResources: boolean;
290373
setShowAllResources: React.Dispatch<React.SetStateAction<boolean>>;

0 commit comments

Comments
 (0)