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
This commit is contained in:
Jaayden Halko
2024-09-03 16:39:18 -04:00
committed by GitHub
parent 44210631cd
commit 093d243811
9 changed files with 254 additions and 8 deletions
@@ -1,5 +1,8 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MockRoleWithOrgPermissions } from "testHelpers/entities";
import {
MockOrganizationAuditorRole,
MockRoleWithOrgPermissions,
} from "testHelpers/entities";
import { CustomRolesPageView } from "./CustomRolesPageView";
const meta: Meta<typeof CustomRolesPageView> = {
@@ -26,6 +29,14 @@ export const Enabled: Story = {
},
};
export const RoleWithoutPermissions: Story = {
args: {
roles: [MockOrganizationAuditorRole],
canAssignOrgRole: true,
isCustomRolesEnabled: true,
},
};
export const EmptyDisplayName: Story = {
args: {
roles: [
@@ -40,7 +51,7 @@ export const EmptyDisplayName: Story = {
},
};
export const EmptyRoleWithoutPermission: Story = {
export const EmptyTableUserWithoutPermission: Story = {
args: {
roles: [],
canAssignOrgRole: false,
@@ -48,7 +59,7 @@ export const EmptyRoleWithoutPermission: Story = {
},
};
export const EmptyRoleWithPermission: Story = {
export const EmptyTableUserWithPermission: Story = {
args: {
roles: [],
canAssignOrgRole: true,
@@ -26,6 +26,7 @@ import {
import type { FC } from "react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { docs } from "utils/docs";
import { PermissionPillsList } from "./PermissionPillsList";
export type CustomRolesPageViewProps = {
roles: Role[] | undefined;
@@ -42,7 +43,6 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
}) => {
const isLoading = roles === undefined;
const isEmpty = Boolean(roles && roles.length === 0);
return (
<>
<ChooseOne>
@@ -58,8 +58,8 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
<Table>
<TableHead>
<TableRow>
<TableCell width="50%">Name</TableCell>
<TableCell width="49%">Permissions</TableCell>
<TableCell width="40%">Name</TableCell>
<TableCell width="59%">Permissions</TableCell>
<TableCell width="1%" />
</TableRow>
</TableHead>
@@ -129,8 +129,8 @@ const RoleRow: FC<RoleRowProps> = ({ role, onDelete, canAssignOrgRole }) => {
<TableRow data-testid={`role-${role.name}`}>
<TableCell>{role.display_name || role.name}</TableCell>
<TableCell css={styles.secondary}>
{role.organization_permissions.length}
<TableCell>
<PermissionPillsList permissions={role.organization_permissions} />
</TableCell>
<TableCell>
@@ -0,0 +1,52 @@
import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within } from "@storybook/test";
import { MockRoleWithOrgPermissions } from "testHelpers/entities";
import { PermissionPillsList } from "./PermissionPillsList";
const meta: Meta<typeof PermissionPillsList> = {
title: "pages/OrganizationCustomRolesPage/PermissionPillsList",
component: PermissionPillsList,
};
export default meta;
type Story = StoryObj<typeof PermissionPillsList>;
export const Default: Story = {
args: {
permissions: MockRoleWithOrgPermissions.organization_permissions,
},
};
export const SinglePermission: Story = {
args: {
permissions: [
{
negate: false,
resource_type: "organization_member",
action: "create",
},
],
},
};
export const NoPermissions: Story = {
args: {
permissions: [],
},
};
export const HoverOverflowPill: Story = {
args: {
permissions: MockRoleWithOrgPermissions.organization_permissions,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.hover(canvas.getByTestId("overflow-permissions-pill"));
},
};
export const ShowAllResources: Story = {
args: {
permissions: MockRoleWithOrgPermissions.organization_permissions,
},
};
@@ -0,0 +1,135 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import Stack from "@mui/material/Stack";
import type { Permission } from "api/typesGenerated";
import { Pill } from "components/Pill/Pill";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import type { FC } from "react";
function getUniqueResourceTypes(jsonObject: readonly Permission[]) {
const resourceTypes = jsonObject.map((item) => item.resource_type);
return [...new Set(resourceTypes)];
}
interface PermissionPillsListProps {
permissions: readonly Permission[];
}
export const PermissionPillsList: FC<PermissionPillsListProps> = ({
permissions,
}) => {
const resourceTypes = getUniqueResourceTypes(permissions);
return (
<Stack direction="row" spacing={1}>
{permissions.length > 0 ? (
<PermissionsPill
resource={resourceTypes[0]}
permissions={permissions}
/>
) : (
<p>None</p>
)}
{resourceTypes.length > 1 && (
<OverflowPermissionPill
resources={resourceTypes.slice(1)}
permissions={permissions.slice(1)}
/>
)}
</Stack>
);
};
interface PermissionPillProps {
resource: string;
permissions: readonly Permission[];
}
const PermissionsPill: FC<PermissionPillProps> = ({
resource,
permissions,
}) => {
const actions = permissions.filter(
(p) => resource === p.resource_type && p.action,
);
return (
<Pill css={styles.permissionPill}>
<b>{resource}</b>: {actions.map((p) => p.action).join(", ")}
</Pill>
);
};
type OverflowPermissionPillProps = {
resources: string[];
permissions: readonly Permission[];
};
const OverflowPermissionPill: FC<OverflowPermissionPillProps> = ({
resources,
permissions,
}) => {
const theme = useTheme();
return (
<Popover mode="hover">
<PopoverTrigger>
<Pill
css={{
backgroundColor: theme.palette.background.paper,
borderColor: theme.palette.divider,
}}
data-testid="overflow-permissions-pill"
>
+{resources.length} more
</Pill>
</PopoverTrigger>
<PopoverContent
disableRestoreFocus
disableScrollLock
css={{
".MuiPaper-root": {
display: "flex",
flexFlow: "column wrap",
columnGap: 8,
rowGap: 12,
padding: "12px 16px",
alignContent: "space-around",
minWidth: "auto",
backgroundColor: theme.palette.background.default,
},
}}
anchorOrigin={{
vertical: -4,
horizontal: "center",
}}
transformOrigin={{
vertical: "bottom",
horizontal: "center",
}}
>
{resources.map((resource) => (
<PermissionsPill
key={resource}
resource={resource}
permissions={permissions}
/>
))}
</PopoverContent>
</Popover>
);
};
const styles = {
permissionPill: (theme) => ({
backgroundColor: theme.experimental.pillDefault.background,
borderColor: theme.experimental.pillDefault.outline,
color: theme.experimental.pillDefault.text,
width: "fit-content",
}),
} satisfies Record<string, Interpolation<Theme>>;
+25
View File
@@ -388,6 +388,31 @@ export const MockRoleWithOrgPermissions: TypesGen.Role = {
resource_type: "audit_log",
action: "read",
},
{
negate: false,
resource_type: "group",
action: "create",
},
{
negate: false,
resource_type: "group",
action: "delete",
},
{
negate: false,
resource_type: "group",
action: "read",
},
{
negate: false,
resource_type: "group",
action: "update",
},
{
negate: false,
resource_type: "provisioner_daemon",
action: "create",
},
],
user_permissions: [],
};
+6
View File
@@ -43,4 +43,10 @@ export default {
},
},
},
pillDefault: {
background: colors.zinc[800],
outline: colors.zinc[700],
text: colors.zinc[200],
},
} satisfies NewTheme;
+6
View File
@@ -43,4 +43,10 @@ export default {
},
},
},
pillDefault: {
background: colors.gray[800],
outline: colors.gray[700],
text: colors.gray[200],
},
} satisfies NewTheme;
+5
View File
@@ -3,4 +3,9 @@ import type { InteractiveRole, Role } from "./roles";
export interface NewTheme {
l1: Role; // page background, things which sit at the "root level"
l2: InteractiveRole; // sidebars, table headers, navigation
pillDefault: {
background: string;
outline: string;
text: string;
};
}
+6
View File
@@ -43,4 +43,10 @@ export default {
},
},
},
pillDefault: {
background: colors.zinc[200],
outline: colors.zinc[300],
text: colors.zinc[700],
},
} satisfies NewTheme;