8000 feat: add resource-action pills to custom roles table by jaaydenh · Pull Request #14354 · coder/coder · GitHub
[go: up one dir, main page]

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
Prev Previous commit
Next Next commit
feat: extract permissions pull list component and add tests
  • Loading branch information
jaaydenh committed Sep 3, 2024
commit 9514f3000772b3ed4e21776cdaf9c1466eab5d5c
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
MockRoleWithOrgPermissions,
MockOrganizationAuditorRole,
MockRoleWithOrgPermissions,
} from "testHelpers/entities";
import { CustomRolesPageView } from "./CustomRolesPageView";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import type { Interpolation, Theme } from "@emotion/react";
import AddOutlined from "@mui/icons-material/AddOutlined";
import Button from "@mui/material/Button";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import type { Permission, Role } from "api/typesGenerated";
import type { Role } from "api/typesGenerated";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
import {
Expand All @@ -20,19 +19,14 @@ import {
ThreeDotsButton,
} from "components/MoreMenu/MoreMenu";
import { Paywall } from "components/Paywall/Paywall";
import { Pill } from "components/Pill/Pill";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "components/TableLoader/TableLoader";
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 Down Expand Up @@ -122,10 +116,6 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
);
};

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

const resourceTypes: string[] = getUniqueResourceTypes(
role.organization_permissions,
);

return (
<TableRow data-testid={`role-${role.name}`}>
<TableCell>{role.display_name || role.name}</TableCell>

<TableCell>
<Stack direction="row" spacing={1}>
{role.organization_permissions.length > 0 ? (
<PermissionsPill
resource={resourceTypes[0]}
permissions={role.organization_permissions}
/>
) : (
<p>None</p>
)}

{resourceTypes.length > 1 && (
<OverflowPermissionPill
resources={resourceTypes.slice(1)}
permissions={role.organization_permissions.slice(1)}
/>
)}
</Stack>
<PermissionPillsList permissions={role.organization_permissions} />
</TableCell>

<TableCell>
Expand Down Expand Up @@ -206,98 +176,10 @@ const TableLoader = () => {
);
};

interface PermissionPillProps {
resource: string;
permissions: readonly Permission[];
}

const PermissionsPill: FC<PermissionPillProps> = ({
resource,
permissions,
}) => {
const actions = permissions.filter((p) => {
if (resource === p.resource_type) {
return 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,
}}
>
+{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 = {
secondary: (theme) => ({
color: theme.palette.text.secondary,
}),
permissionPill: (theme) => ({
backgroundColor: theme.roles.default.background,
borderColor: theme.roles.default.outline,
color: theme.roles.default.text,
width: "fit-content",
}),
} satisfies Record<string, Interpolation<Theme>>;

export default CustomRolesPageView;
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,137 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import Stack from "@mui/material/Stack";
import type { Permission, Role } 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: string[] = getUniqueResourceTypes(permissions);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this type annotation here still necessary? maybe if TypeScript is having trouble inferring the type here you could add : string[] to the function declaration

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not necessary, just leftover from implementation work.

7D4E

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) => {
if (resource === p.resource_type) {
return 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.roles.default.background,
borderColor: theme.roles.default.outline,
color: theme.roles.default.text,
width: "fit-content",
}),
} satisfies Record<string, Interpolation<Theme>>;
Loading
0