Skip to content

Commit 5d7c6ab

Browse files
committed
feat: extract permissions pull list component and add tests
1 parent 32f7450 commit 5d7c6ab

File tree

5 files changed

+219
-123
lines changed

5 files changed

+219
-123
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Meta, StoryObj } from "@storybook/react";
22
import {
3-
MockRoleWithOrgPermissions,
43
MockOrganizationAuditorRole,
4+
MockRoleWithOrgPermissions,
55
} from "testHelpers/entities";
66
import { CustomRolesPageView } from "./CustomRolesPageView";
77

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

Lines changed: 4 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
1+
import type { Interpolation, Theme } from "@emotion/react";
22
import AddOutlined from "@mui/icons-material/AddOutlined";
33
import Button from "@mui/material/Button";
44
import Skeleton from "@mui/material/Skeleton";
5-
import Stack from "@mui/material/Stack";
65
import Table from "@mui/material/Table";
76
import TableBody from "@mui/material/TableBody";
87
import TableCell from "@mui/material/TableCell";
98
import TableContainer from "@mui/material/TableContainer";
109
import TableHead from "@mui/material/TableHead";
1110
import TableRow from "@mui/material/TableRow";
12-
import type { Permission, Role } from "api/typesGenerated";
11+
import type { Role } from "api/typesGenerated";
1312
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
1413
import { EmptyState } from "components/EmptyState/EmptyState";
1514
import {
@@ -20,19 +19,14 @@ import {
2019
ThreeDotsButton,
2120
} from "components/MoreMenu/MoreMenu";
2221
import { Paywall } from "components/Paywall/Paywall";
23-
import { Pill } from "components/Pill/Pill";
24-
import {
25-
Popover,
26-
PopoverContent,
27-
PopoverTrigger,
28-
} from "components/Popover/Popover";
2922
import {
3023
TableLoaderSkeleton,
3124
TableRowSkeleton,
3225
} from "components/TableLoader/TableLoader";
3326
import type { FC } from "react";
3427
import { Link as RouterLink, useNavigate } from "react-router-dom";
3528
import { docs } from "utils/docs";
29+
import { PermissionPillsList } from "./PermissionPillsList";
3630

3731
export type CustomRolesPageViewProps = {
3832
roles: Role[] | undefined;
@@ -122,10 +116,6 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
122116
);
123117
};
124118

125-
function getUniqueResourceTypes(jsonObject: readonly Permission[]) {
126-
const resourceTypes = jsonObject.map((item) => item.resource_type);
127-
return [...new Set(resourceTypes)];
128-
}
129119
interface RoleRowProps {
130120
role: Role;
131121
onDelete: () => void;
@@ -135,32 +125,12 @@ interface RoleRowProps {
135125
const RoleRow: FC<RoleRowProps> = ({ role, onDelete, canAssignOrgRole }) => {
136126
const navigate = useNavigate();
137127

138-
const resourceTypes: string[] = getUniqueResourceTypes(
139-
role.organization_permissions,
140-
);
141-
142128
return (
143129
<TableRow data-testid={`role-${role.name}`}>
144130
<TableCell>{role.display_name || role.name}</TableCell>
145131

146132
<TableCell>
147-
<Stack direction="row" spacing={1}>
148-
{role.organization_permissions.length > 0 ? (
149-
<PermissionsPill
150-
resource={resourceTypes[0]}
151-
permissions={role.organization_permissions}
152-
/>
153-
) : (
154-
<p>None</p>
155-
)}
156-
157-
{resourceTypes.length > 1 && (
158-
<OverflowPermissionPill
159-
resources={resourceTypes.slice(1)}
160-
permissions={role.organization_permissions.slice(1)}
161-
/>
162-
)}
163-
</Stack>
133+
<PermissionPillsList permissions={role.organization_permissions} />
164134
</TableCell>
165135

166136
<TableCell>
@@ -206,98 +176,10 @@ const TableLoader = () => {
206176
);
207177
};
208178

209-
interface PermissionPillProps {
210-
resource: string;
211-
permissions: readonly Permission[];
212-
}
213-
214-
const PermissionsPill: FC<PermissionPillProps> = ({
215-
resource,
216-
permissions,
217-
}) => {
218-
const actions = permissions.filter((p) => {
219-
if (resource === p.resource_type) {
220-
return p.action;
221-
}
222-
});
223-
224-
return (
225-
<Pill css={styles.permissionPill}>
226-
<b>{resource}</b>: {actions.map((p) => p.action).join(", ")}
227-
</Pill>
228-
);
229-
};
230-
231-
type OverflowPermissionPillProps = {
232-
resources: string[];
233-
permissions: readonly Permission[];
234-
};
235-
236-
const OverflowPermissionPill: FC<OverflowPermissionPillProps> = ({
237-
resources,
238-
permissions,
239-
}) => {
240-
const theme = useTheme();
241-
242-
return (
243-
<Popover mode="hover">
244-
<PopoverTrigger>
245-
<Pill
246-
css={{
247-
backgroundColor: theme.palette.background.paper,
248-
borderColor: theme.palette.divider,
249-
}}
250-
>
251-
+{resources.length} more
252-
</Pill>
253-
</PopoverTrigger>
254-
255-
<PopoverContent
256-
disableRestoreFocus
257-
disableScrollLock
258-
css={{
259-
".MuiPaper-root": {
260-
display: "flex",
261-
flexFlow: "column wrap",
262-
columnGap: 8,
263-
rowGap: 12,
264-
padding: "12px 16px",
265-
alignContent: "space-around",
266-
minWidth: "auto",
267-
backgroundColor: theme.palette.background.default,
268-
},
269-
}}
270-
anchorOrigin={{
271-
vertical: -4,
272-
horizontal: "center",
273-
}}
274-
transformOrigin={{
275-
vertical: "bottom",
276-
horizontal: "center",
277-
}}
278-
>
279-
{resources.map((resource) => (
280-
<PermissionsPill
281-
key={resource}
282-
resource={resource}
283-
permissions={permissions}
284-
/>
285-
))}
286-
</PopoverContent>
287-
</Popover>
288-
);
289-
};
290-
291179
const styles = {
292180
secondary: (theme) => ({
293181
color: theme.palette.text.secondary,
294182
}),
295-
permissionPill: (theme) => ({
296-
backgroundColor: theme.roles.default.background,
297-
borderColor: theme.roles.default.outline,
298-
color: theme.roles.default.text,
299-
width: "fit-content",
300-
}),
301183
} satisfies Record<string, Interpolation<Theme>>;
302184

303185
export default CustomRolesPageView;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { userEvent, within } from "@storybook/test";
3+
import { MockRoleWithOrgPermissions } from "testHelpers/entities";
4+
import { PermissionPillsList } from "./PermissionPillsList";
5+
6+
const meta: Meta<typeof PermissionPillsList> = {
7+
title: "pages/OrganizationCustomRolesPage/PermissionPillsList",
8+
component: PermissionPillsList,
9+
};
10+
11+
export default meta;
12+
type Story = StoryObj<typeof PermissionPillsList>;
13+
14+
export const Default: Story = {
15+
args: {
16+
permissions: MockRoleWithOrgPermissions.organization_permissions,
17+
},
18+
};
19+
20+
export const SinglePermission: Story = {
21+
args: {
22+
permissions: [
23+
{
24+
negate: false,
25+
resource_type: "organization_member",
26+
action: "create",
27+
},
28+
],
29+
},
30+
};
31+
32+
export const NoPermissions: Story = {
33+
args: {
34+
permissions: [],
35+
},
36+
};
37+
38+
export const HoverOverflowPill: Story = {
39+
args: {
40+
permissions: MockRoleWithOrgPermissions.organization_permissions,
41+
},
42+
play: async ({ canvasElement }) => {
43+
const canvas = within(canvasElement);
44+
await userEvent.hover(canvas.getByTestId("overflow-permissions-pill"));
45+
},
46+
};
47+
48+
export const ShowAllResources: Story = {
49+
args: {
50+
permissions: MockRoleWithOrgPermissions.organization_permissions,
51+
},
52+
};
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
2+
import Stack from "@mui/material/Stack";
3+
import type { Permission, Role } from "api/typesGenerated";
4+
import { Pill } from "components/Pill/Pill";
5+
import {
6+
Popover,
7+
PopoverContent,
8+
PopoverTrigger,
9+
} from "components/Popover/Popover";
10+
import type { FC } from "react";
11+
12+
function getUniqueResourceTypes(jsonObject: readonly Permission[]) {
13+
const resourceTypes = jsonObject.map((item) => item.resource_type);
14+
return [...new Set(resourceTypes)];
15+
}
16+
17+
interface PermissionPillsListProps {
18+
permissions: readonly Permission[];
19+
}
20+
21+
export const PermissionPillsList: FC<PermissionPillsListProps> = ({
22+
permissions,
23+
}) => {
24+
const resourceTypes: string[] = getUniqueResourceTypes(permissions);
25+
26+
return (
27+
<Stack direction="row" spacing={1}>
28+
{permissions.length > 0 ? (
29+
<PermissionsPill
30+
resource={resourceTypes[0]}
31+
permissions={permissions}
32+
/>
33+
) : (
34+
<p>None</p>
35+
)}
36+
37+
{resourceTypes.length > 1 && (
38+
<OverflowPermissionPill
39+
resources={resourceTypes.slice(1)}
40+
permissions={permissions.slice(1)}
41+
/>
42+
)}
43+
</Stack>
44+
);
45+
};
46+
47+
interface PermissionPillProps {
48+
resource: string;
49+
permissions: readonly Permission[];
50+
}
51+
52+
const PermissionsPill: FC<PermissionPillProps> = ({
53+
resource,
54+
permissions,
55+
}) => {
56+
const actions = permissions.filter((p) => {
57+
if (resource === p.resource_type) {
58+
return p.action;
59+
}
60+
});
61+
62+
return (
63+
<Pill css={styles.permissionPill}>
64+
<b>{resource}</b>: {actions.map((p) => p.action).join(", ")}
65+
</Pill>
66+
);
67+
};
68+
69+
type OverflowPermissionPillProps = {
70+
resources: string[];
71+
permissions: readonly Permission[];
72+
};
73+
74+
const OverflowPermissionPill: FC<OverflowPermissionPillProps> = ({
75+
resources,
76+
permissions,
77+
}) => {
78+
const theme = useTheme();
79+
80+
return (
81+
<Popover mode="hover">
82+
<PopoverTrigger>
83+
<Pill
84+
css={{
85+
backgroundColor: theme.palette.background.paper,
86+
borderColor: theme.palette.divider,
87+
}}
88+
data-testid="overflow-permissions-pill"
89+
>
90+
+{resources.length} more
91+
</Pill>
92+
</PopoverTrigger>
93+
94+
<PopoverContent
95+
disableRestoreFocus
96+
disableScrollLock
97+
css={{
98+
".MuiPaper-root": {
99+
display: "flex",
100+
flexFlow: "column wrap",
101+
columnGap: 8,
102+
rowGap: 12,
103+
padding: "12px 16px",
104+
alignContent: "space-around",
105+
minWidth: "auto",
106+
backgroundColor: theme.palette.background.default,
107+
},
108+
}}
109+
anchorOrigin={{
110+
vertical: -4,
111+
horizontal: "center",
112+
}}
113+
transformOrigin={{
114+
vertical: "bottom",
115+
horizontal: "center",
116+
}}
117+
>
118+
{resources.map((resource) => (
119+
<PermissionsPill
120+
key={resource}
121+
resource={resource}
122+
permissions={permissions}
123+
/>
124+
))}
125+
</PopoverContent>
126+
</Popover>
127+
);
128+
};
129+
130+
const styles = {
131+
permissionPill: (theme) => ({
132+
backgroundColor: theme.roles.default.background,
133+
borderColor: theme.roles.default.outline,
134+
color: theme.roles.default.text,
135+
width: "fit-content",
136+
}),
137+
} satisfies Record<string, Interpolation<Theme>>;

0 commit comments

Comments
 (0)