mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
+14
-3
@@ -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>>;
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
@@ -43,4 +43,10 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
pillDefault: {
|
||||
background: colors.zinc[800],
|
||||
outline: colors.zinc[700],
|
||||
text: colors.zinc[200],
|
||||
},
|
||||
} satisfies NewTheme;
|
||||
|
||||
@@ -43,4 +43,10 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
pillDefault: {
|
||||
background: colors.gray[800],
|
||||
outline: colors.gray[700],
|
||||
text: colors.gray[200],
|
||||
},
|
||||
} satisfies NewTheme;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,4 +43,10 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
pillDefault: {
|
||||
background: colors.zinc[200],
|
||||
outline: colors.zinc[300],
|
||||
text: colors.zinc[700],
|
||||
},
|
||||
} satisfies NewTheme;
|
||||
|
||||
Reference in New Issue
Block a user