Skip to content

Commit 093d243

Browse files
authored
feat: add resource-action pills to custom roles table (#14354)
* feat: add resource-action pills to custom roles table * fix: remove permission from theme and change name to colorRoles * fix: revert name from colorRoles to roles * fix: format * fix: custom role with no permissions * feat: extract permissions pull list component and add tests * chore: undo color roles name change * feat: add experimental pill colors * fix: format * chore: update experiment name * chore: cleanup
1 parent 4421063 commit 093d243

File tree

9 files changed

+254
-8
lines changed

9 files changed

+254
-8
lines changed

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2-
import { MockRoleWithOrgPermissions } from "testHelpers/entities";
2+
import {
3+
MockOrganizationAuditorRole,
4+
MockRoleWithOrgPermissions,
5+
} from "testHelpers/entities";
36
import { CustomRolesPageView } from "./CustomRolesPageView";
47

58
const meta: Meta<typeof CustomRolesPageView> = {
@@ -26,6 +29,14 @@ export const Enabled: Story = {
2629
},
2730
};
2831

32+
export const RoleWithoutPermissions: Story = {
33+
args: {
34+
roles: [MockOrganizationAuditorRole],
35+
canAssignOrgRole: true,
36+
isCustomRolesEnabled: true,
37+
},
38+
};
39+
2940
export const EmptyDisplayName: Story = {
3041
args: {
3142
roles: [
@@ -40,15 +51,15 @@ export const EmptyDisplayName: Story = {
4051
},
4152
};
4253

43-
export const EmptyRoleWithoutPermission: Story = {
54+
export const EmptyTableUserWithoutPermission: Story = {
4455
args: {
4556
roles: [],
4657
canAssignOrgRole: false,
4758
isCustomRolesEnabled: true,
4859
},
4960
};
5061

51-
export const EmptyRoleWithPermission: Story = {
62+
export const EmptyTableUserWithPermission: Story = {
5263
args: {
5364
roles: [],
5465
canAssignOrgRole: true,

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
import type { FC } from "react";
2727
import { Link as RouterLink, useNavigate } from "react-router-dom";
2828
import { docs } from "utils/docs";
29+
import { PermissionPillsList } from "./PermissionPillsList";
2930

3031
export type CustomRolesPageViewProps = {
3132
roles: Role[] | undefined;
@@ -42,7 +43,6 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
4243
}) => {
4344
const isLoading = roles === undefined;
4445
const isEmpty = Boolean(roles && roles.length === 0);
45-
4646
return (
4747
<>
4848
<ChooseOne>
@@ -58,8 +58,8 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
5858
<Table>
5959
<TableHead>
6060
<TableRow>
61-
<TableCell width="50%">Name</TableCell>
62-
<TableCell width="49%">Permissions</TableCell>
61+
<TableCell width="40%">Name</TableCell>
62+
<TableCell width="59%">Permissions</TableCell>
6363
<TableCell width="1%" />
6464
</TableRow>
6565
</TableHead>
@@ -129,8 +129,8 @@ const RoleRow: FC<RoleRowProps> = ({ role, onDelete, canAssignOrgRole }) => {
129129
<TableRow data-testid={`role-${role.name}`}>
130130
<TableCell>{role.display_name || role.name}</TableCell>
131131

132-
<TableCell css={styles.secondary}>
133-
{role.organization_permissions.length}
132+
<TableCell>
133+
<PermissionPillsList permissions={role.organization_permissions} />
134134
</TableCell>
135135

136136
<TableCell>
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: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
2+
import Stack from "@mui/material/Stack";
3+
import type { Permission } 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 = 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(
57+
(p) => resource === p.resource_type && p.action,
58+
);
59+
60+
return (
61+
<Pill css={styles.permissionPill}>
62+
<b>{resource}</b>: {actions.map((p) => p.action).join(", ")}
63+
</Pill>
64+
);
65+
};
66+
67+
type OverflowPermissionPillProps = {
68+
resources: string[];
69+
permissions: readonly Permission[];
70+
};
71+
72+
const OverflowPermissionPill: FC<OverflowPermissionPillProps> = ({
73+
resources,
74+
permissions,
75+
}) => {
76+
const theme = useTheme();
77+
78+
return (
79+
<Popover mode="hover">
80+
<PopoverTrigger>
81+
<Pill
82+
css={{
83+
backgroundColor: theme.palette.background.paper,
84+
borderColor: theme.palette.divider,
85+
}}
86+
data-testid="overflow-permissions-pill"
87+
>
88+
+{resources.length} more
89+
</Pill>
90+
</PopoverTrigger>
91+
92+
<PopoverContent
93+
disableRestoreFocus
94+
disableScrollLock
95+
css={{
96+
".MuiPaper-root": {
97+
display: "flex",
98+
flexFlow: "column wrap",
99+
columnGap: 8,
100+
rowGap: 12,
101+
padding: "12px 16px",
102+
alignContent: "space-around",
103+
minWidth: "auto",
104+
backgroundColor: theme.palette.background.default,
105+
},
106+
}}
107+
anchorOrigin={{
108+
vertical: -4,
109+
horizontal: "center",
110+
}}
111+
transformOrigin={{
112+
vertical: "bottom",
113+
horizontal: "center",
114+
}}
115+
>
116+
{resources.map((resource) => (
117+
<PermissionsPill
118+
key={resource}
119+
resource={resource}
120+
permissions={permissions}
121+
/>
122+
))}
123+
</PopoverContent>
124+
</Popover>
125+
);
126+
};
127+
128+
const styles = {
129+
permissionPill: (theme) => ({
130+
backgroundColor: theme.experimental.pillDefault.background,
131+
borderColor: theme.experimental.pillDefault.outline,
132+
color: theme.experimental.pillDefault.text,
133+
width: "fit-content",
134+
}),
135+
} satisfies Record<string, Interpolation<Theme>>;

site/src/testHelpers/entities.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,31 @@ export const MockRoleWithOrgPermissions: TypesGen.Role = {
388388
resource_type: "audit_log",
389389
action: "read",
390390
},
391+
{
392+
negate: false,
393+
resource_type: "group",
394+
action: "create",
395+
},
396+
{
397+
negate: false,
398+
resource_type: "group",
399+
action: "delete",
400+
},
401+
{
402+
negate: false,
403+
resource_type: "group",
404+
action: "read",
405+
},
406+
{
407+
negate: false,
408+
resource_type: "group",
409+
action: "update",
410+
},
411+
{
412+
negate: false,
413+
resource_type: "provisioner_daemon",
414+
action: "create",
415+
},
391416
],
392417
user_permissions: [],
393418
};

site/src/theme/dark/experimental.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,10 @@ export default {
4343
},
4444
},
4545
},
46+
47+
pillDefault: {
48+
background: colors.zinc[800],
49+
outline: colors.zinc[700],
50+
text: colors.zinc[200],
51+
},
4652
} satisfies NewTheme;

site/src/theme/darkBlue/experimental.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,10 @@ export default {
4343
},
4444
},
4545
},
46+
47+
pillDefault: {
48+
background: colors.gray[800],
49+
outline: colors.gray[700],
50+
text: colors.gray[200],
51+
},
4652
} satisfies NewTheme;

site/src/theme/experimental.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@ import type { InteractiveRole, Role } from "./roles";
33
export interface NewTheme {
44
l1: Role; // page background, things which sit at the "root level"
55
l2: InteractiveRole; // sidebars, table headers, navigation
6+
pillDefault: {
7+
background: string;
8+
outline: string;
9+
text: string;
10+
};
611
}

site/src/theme/light/experimental.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,10 @@ export default {
4343
},
4444
},
4545
},
46+
47+
pillDefault: {
48+
background: colors.zinc[200],
49+
outline: colors.zinc[300],
50+
text: colors.zinc[700],
51+
},
4652
} satisfies NewTheme;

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy