Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 15d74a1

Browse files
authored
feat: improve custom roles create/edit page (#14456)
* fix: improve show/hide checkbox text * feat: add parent checkbox for grouped resource permissions * fix: align action list item to a grid * chore: add additional tests * fix: format
1 parent f3ea740 commit 15d74a1

File tree

3 files changed

+177
-41
lines changed

3 files changed

+177
-41
lines changed

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

+26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2+
import { expect, userEvent, within } from "@storybook/test";
23
import {
4+
MockRole2WithOrgPermissions,
35
MockRoleWithOrgPermissions,
46
assignableRole,
57
mockApiError,
@@ -25,6 +27,16 @@ export const Default: Story = {
2527
},
2628
};
2729

30+
export const CheckboxIndeterminate: Story = {
31+
args: {
32+
role: assignableRole(MockRole2WithOrgPermissions, true),
33+
onSubmit: () => null,
34+
isLoading: false,
35+
organizationName: "my-org",
36+
canAssignOrgRole: true,
37+
},
38+
};
39+
2840
export const WithError: Story = {
2941
args: {
3042
role: assignableRole(MockRoleWithOrgPermissions, true),
@@ -61,3 +73,17 @@ export const ShowAllResources: Story = {
6173
allResources: true,
6274
},
6375
};
76+
77+
export const ToggleParentCheckbox: Story = {
78+
play: async ({ canvasElement }) => {
79+
const user = userEvent.setup();
80+
const canvas = within(canvasElement);
81+
const checkbox = await canvas
82+
.getByTestId("audit_log")
83+
.getElementsByTagName("input")[0];
84+
await user.click(checkbox);
85+
await expect(checkbox).toBeChecked();
86+
await user.click(checkbox);
87+
await expect(checkbox).not.toBeChecked();
88+
},
89+
};

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

+136-41
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,77 @@ 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+
data-testid={`${resourceKey}`}
338+
onChange={(e) =>
339+
handleResourceCheckChange(
340+
e,
341+
form,
342+
checkedActions.length > 0 &&
343+
checkedActions.length < Object.keys(value).length,
344+
)
345+
}
346+
/>
347+
{resourceKey}
348+
<ul css={styles.checkBoxes}>
349+
{Object.entries(value).map(([actionKey, value]) => (
350+
<li key={actionKey} css={styles.actionItem}>
351+
<span css={styles.actionText}>
352+
<Checkbox
353+
size="small"
354+
name={`${resourceKey}:${actionKey}`}
355+
checked={checkedActions.some((p) =>
356+
ResourceActionComparator(p, resourceKey, actionKey),
357+
)}
358+
onChange={(e) => handleActionCheckChange(e, form)}
359+
/>
360+
{actionKey}
361+
</span>
362+
<span css={styles.actionDescription}>{value}</span>
363+
</li>
364+
))}
365+
</ul>
366+
</li>
367+
</TableCell>
368+
</TableRow>
369+
);
370+
};
371+
288372
interface ShowAllResourcesCheckboxProps {
289373
showAllResources: boolean;
290374
setShowAllResources: React.Dispatch<React.SetStateAction<boolean>>;
@@ -308,7 +392,13 @@ const ShowAllResourcesCheckbox: FC<ShowAllResourcesCheckboxProps> = ({
308392
icon={<VisibilityOffOutlinedIcon />}
309393
/>
310394
}
311-
label={<span style={{ fontSize: 12 }}>Show all permissions</span>}
395+
label={
396+
<span style={{ fontSize: 12 }}>
397+
{showAllResources
398+
? "Hide advanced permissions"
399+
: "Show advanced permissions"}
400+
</span>
401+
}
312402
/>
313403
);
314404
};
@@ -323,7 +413,12 @@ const styles = {
323413
}),
324414
actionDescription: (theme) => ({
325415
color: theme.palette.text.secondary,
416+
paddingTop: 6,
326417
}),
418+
actionItem: {
419+
display: "grid",
420+
gridTemplateColumns: "270px 1fr",
421+
},
327422
} satisfies Record<string, Interpolation<Theme>>;
328423

329424
export default CreateEditRolePageView;

site/src/testHelpers/entities.ts

+15
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,21 @@ export const MockRoleWithOrgPermissions: TypesGen.Role = {
392392
user_permissions: [],
393393
};
394394

395+
export const MockRole2WithOrgPermissions: TypesGen.Role = {
396+
name: "my-role-1",
397+
display_name: "My Role 1",
398+
organization_id: MockOrganization.id,
399+
site_permissions: [],
400+
organization_permissions: [
401+
{
402+
negate: false,
403+
resource_type: "audit_log",
404+
action: "create",
405+
},
406+
],
407+
user_permissions: [],
408+
};
409+
395410
// assignableRole takes a role and a boolean. The boolean implies if the
396411
// actor can assign (add/remove) the role from other users.
397412
export function assignableRole(

0 commit comments

Comments
 (0)