diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx index 4a36523c16a17..26565985e1ee6 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx +++ b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.stories.tsx @@ -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 = { @@ -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, diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx index 15d9c3773b3b5..80208aa4b4261 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx @@ -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 = ({ }) => { const isLoading = roles === undefined; const isEmpty = Boolean(roles && roles.length === 0); - return ( <> @@ -58,8 +58,8 @@ export const CustomRolesPageView: FC = ({ - Name - Permissions + Name + Permissions @@ -129,8 +129,8 @@ const RoleRow: FC = ({ role, onDelete, canAssignOrgRole }) => { {role.display_name || role.name} - - {role.organization_permissions.length} + + diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx b/site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx new file mode 100644 index 0000000000000..327d678e4d802 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx @@ -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 = { + title: "pages/OrganizationCustomRolesPage/PermissionPillsList", + component: PermissionPillsList, +}; + +export default meta; +type Story = StoryObj; + +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, + }, +}; diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.tsx b/site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.tsx new file mode 100644 index 0000000000000..e78e8baba15a1 --- /dev/null +++ b/site/src/pages/ManagementSettingsPage/CustomRolesPage/PermissionPillsList.tsx @@ -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 = ({ + permissions, +}) => { + const resourceTypes = getUniqueResourceTypes(permissions); + + return ( + + {permissions.length > 0 ? ( + + ) : ( +

None

+ )} + + {resourceTypes.length > 1 && ( + + )} +
+ ); +}; + +interface PermissionPillProps { + resource: string; + permissions: readonly Permission[]; +} + +const PermissionsPill: FC = ({ + resource, + permissions, +}) => { + const actions = permissions.filter( + (p) => resource === p.resource_type && p.action, + ); + + return ( + + {resource}: {actions.map((p) => p.action).join(", ")} + + ); +}; + +type OverflowPermissionPillProps = { + resources: string[]; + permissions: readonly Permission[]; +}; + +const OverflowPermissionPill: FC = ({ + resources, + permissions, +}) => { + const theme = useTheme(); + + return ( + + + + +{resources.length} more + + + + + {resources.map((resource) => ( + + ))} + + + ); +}; + +const styles = { + permissionPill: (theme) => ({ + backgroundColor: theme.experimental.pillDefault.background, + borderColor: theme.experimental.pillDefault.outline, + color: theme.experimental.pillDefault.text, + width: "fit-content", + }), +} satisfies Record>; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 08c567187366b..8d09460422251 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -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: [], }; diff --git a/site/src/theme/dark/experimental.ts b/site/src/theme/dark/experimental.ts index aedc1d17146d9..25074fb0106e0 100644 --- a/site/src/theme/dark/experimental.ts +++ b/site/src/theme/dark/experimental.ts @@ -43,4 +43,10 @@ export default { }, }, }, + + pillDefault: { + background: colors.zinc[800], + outline: colors.zinc[700], + text: colors.zinc[200], + }, } satisfies NewTheme; diff --git a/site/src/theme/darkBlue/experimental.ts b/site/src/theme/darkBlue/experimental.ts index 8cf9dafaf3fe6..d7e4b816a775e 100644 --- a/site/src/theme/darkBlue/experimental.ts +++ b/site/src/theme/darkBlue/experimental.ts @@ -43,4 +43,10 @@ export default { }, }, }, + + pillDefault: { + background: colors.gray[800], + outline: colors.gray[700], + text: colors.gray[200], + }, } satisfies NewTheme; diff --git a/site/src/theme/experimental.ts b/site/src/theme/experimental.ts index 66dce1b19558e..56ce8759bec7c 100644 --- a/site/src/theme/experimental.ts +++ b/site/src/theme/experimental.ts @@ -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; + }; } diff --git a/site/src/theme/light/experimental.ts b/site/src/theme/light/experimental.ts index e9c415be9ea1b..a816daab83ad5 100644 --- a/site/src/theme/light/experimental.ts +++ b/site/src/theme/light/experimental.ts @@ -43,4 +43,10 @@ export default { }, }, }, + + pillDefault: { + background: colors.zinc[200], + outline: colors.zinc[300], + text: colors.zinc[700], + }, } satisfies NewTheme;