Skip to content

feat: add resource-action pills to custom roles table #14354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Sep 3, 2024
Original file line number Diff line number Diff line change
@@ -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> = {
Expand All @@ -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: [
Expand All @@ -40,15 +51,15 @@ export const EmptyDisplayName: Story = {
},
};

export const EmptyRoleWithoutPermission: Story = {
export const EmptyTableUserWithoutPermission: Story = {
args: {
roles: [],
canAssignOrgRole: false,
isCustomRolesEnabled: true,
},
};

export const EmptyRoleWithPermission: Story = {
export const EmptyTableUserWithPermission: Story = {
args: {
roles: [],
canAssignOrgRole: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,7 +43,6 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
}) => {
const isLoading = roles === undefined;
const isEmpty = Boolean(roles && roles.length === 0);

return (
<>
<ChooseOne>
Expand All @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
},
};
Original file line number Diff line number Diff line change
@@ -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 changes: 25 additions & 0 deletions site/src/testHelpers/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
};
Expand Down
6 changes: 6 additions & 0 deletions site/src/theme/dark/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@ export default {
},
},
},

pillDefault: {
background: colors.zinc[800],
outline: colors.zinc[700],
text: colors.zinc[200],
},
} satisfies NewTheme;
6 changes: 6 additions & 0 deletions site/src/theme/darkBlue/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@ export default {
},
},
},

pillDefault: {
background: colors.gray[800],
outline: colors.gray[700],
text: colors.gray[200],
},
} satisfies NewTheme;
5 changes: 5 additions & 0 deletions site/src/theme/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 6 additions & 0 deletions site/src/theme/light/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,10 @@ export default {
},
},
},

pillDefault: {
background: colors.zinc[200],
outline: colors.zinc[300],
text: colors.zinc[700],
},
} satisfies NewTheme;
Loading