diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx index fe183f33fc..895c67d563 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx +++ b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx @@ -162,6 +162,7 @@ const DEFAULT_RESOURCES = [ "organization_member", "provisioner_daemon", "workspace", + "idpsync_settings", ]; const resources = new Set(DEFAULT_RESOURCES); diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx index beff71098f..c103c6b95e 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx +++ b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx @@ -1,5 +1,3 @@ -import AddIcon from "@mui/icons-material/AddOutlined"; -import Button from "@mui/material/Button"; import { getErrorMessage } from "api/errors"; import { organizationPermissions } from "api/queries/organizations"; import { deleteOrganizationRole, organizationRoles } from "api/queries/roles"; @@ -13,7 +11,7 @@ import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; -import { Link as RouterLink, useParams } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import { useOrganizationSettings } from "../ManagementSettingsLayout"; import CustomRolesPageView from "./CustomRolesPageView"; @@ -32,8 +30,11 @@ export const CustomRolesPage: FC = () => { ); const [roleToDelete, setRoleToDelete] = useState(); const organizationRolesQuery = useQuery(organizationRoles(organizationName)); - const filteredRoleData = organizationRolesQuery.data?.filter( - (role) => role.built_in === false, + const builtInRoles = organizationRolesQuery.data?.filter( + (role) => role.built_in, + ); + const customRoles = organizationRolesQuery.data?.filter( + (role) => !role.built_in, ); const permissions = permissionsQuery.data; @@ -64,18 +65,14 @@ export const CustomRolesPage: FC = () => { justifyContent="space-between" > - {permissions.assignOrgRole && isCustomRolesEnabled && ( - - )} ; export const NotEnabled: Story = { args: { - roles: [MockRoleWithOrgPermissions], + builtInRoles: [MockRoleWithOrgPermissions], + customRoles: [MockRoleWithOrgPermissions], canAssignOrgRole: true, isCustomRolesEnabled: false, }, @@ -23,7 +24,8 @@ export const NotEnabled: Story = { export const NotEnabledEmptyTable: Story = { args: { - roles: [], + builtInRoles: [MockRoleWithOrgPermissions], + customRoles: [], canAssignOrgRole: true, isCustomRolesEnabled: false, }, @@ -31,7 +33,8 @@ export const NotEnabledEmptyTable: Story = { export const Enabled: Story = { args: { - roles: [MockRoleWithOrgPermissions], + builtInRoles: [MockRoleWithOrgPermissions], + customRoles: [MockRoleWithOrgPermissions], canAssignOrgRole: true, isCustomRolesEnabled: true, }, @@ -39,7 +42,8 @@ export const Enabled: Story = { export const RoleWithoutPermissions: Story = { args: { - roles: [MockOrganizationAuditorRole], + builtInRoles: [MockOrganizationAuditorRole], + customRoles: [MockOrganizationAuditorRole], canAssignOrgRole: true, isCustomRolesEnabled: true, }, @@ -47,13 +51,14 @@ export const RoleWithoutPermissions: Story = { export const EmptyDisplayName: Story = { args: { - roles: [ + customRoles: [ { ...MockRoleWithOrgPermissions, name: "my-custom-role", display_name: "", }, ], + builtInRoles: [MockRoleWithOrgPermissions], canAssignOrgRole: true, isCustomRolesEnabled: true, }, @@ -61,7 +66,8 @@ export const EmptyDisplayName: Story = { export const EmptyTableUserWithoutPermission: Story = { args: { - roles: [], + builtInRoles: [MockRoleWithOrgPermissions], + customRoles: [], canAssignOrgRole: false, isCustomRolesEnabled: true, }, @@ -69,7 +75,8 @@ export const EmptyTableUserWithoutPermission: Story = { export const EmptyTableUserWithPermission: Story = { args: { - roles: [], + builtInRoles: [MockRoleWithOrgPermissions], + customRoles: [], canAssignOrgRole: true, isCustomRolesEnabled: true, }, diff --git a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx index 70ac152652..fb642c2225 100644 --- a/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx +++ b/site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx @@ -1,4 +1,5 @@ -import type { Interpolation, Theme } from "@emotion/react"; +import { type Interpolation, type Theme, useTheme } from "@emotion/react"; +import AddIcon from "@mui/icons-material/AddOutlined"; import AddOutlined from "@mui/icons-material/AddOutlined"; import Button from "@mui/material/Button"; import Skeleton from "@mui/material/Skeleton"; @@ -8,7 +9,7 @@ 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 { Role } from "api/typesGenerated"; +import type { AssignableRoles, Role } from "api/typesGenerated"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { EmptyState } from "components/EmptyState/EmptyState"; import { @@ -30,20 +31,21 @@ import { docs } from "utils/docs"; import { PermissionPillsList } from "./PermissionPillsList"; interface CustomRolesPageViewProps { - roles: Role[] | undefined; + builtInRoles: AssignableRoles[] | undefined; + customRoles: AssignableRoles[] | undefined; onDeleteRole: (role: Role) => void; canAssignOrgRole: boolean; isCustomRolesEnabled: boolean; } export const CustomRolesPageView: FC = ({ - roles, + builtInRoles, + customRoles, onDeleteRole, canAssignOrgRole, isCustomRolesEnabled, }) => { - const isLoading = roles === undefined; - const isEmpty = Boolean(roles && roles.length === 0); + const theme = useTheme(); return ( {!isCustomRolesEnabled && ( @@ -53,53 +55,112 @@ export const CustomRolesPageView: FC = ({ documentationLink={docs("/admin/groups")} /> )} - - - - - Name - Permissions - - - - - - - - + + +

Custom Roles

+ + Create custom roles to grant users a tailored set of granular + permissions. + +
+ {canAssignOrgRole && isCustomRolesEnabled && ( + + )} +
+ + +

Built-In Roles

+ + Built-in roles have predefined permissions. You cannot edit or delete + built-in roles. + +
+ + + ); +}; - - - - } - variant="contained" - > - Create custom role - - ) - } - /> - - - +interface RoleTableProps { + roles: AssignableRoles[] | undefined; + isCustomRolesEnabled: boolean; + canAssignOrgRole: boolean; + onDeleteRole: (role: Role) => void; +} - - {roles?.map((role) => ( +const RoleTable: FC = ({ + roles, + isCustomRolesEnabled, + onDeleteRole, + canAssignOrgRole, +}) => { + const isLoading = roles === undefined; + const isEmpty = Boolean(roles && roles.length === 0); + return ( + +
+ + + Name + Permissions + + + + + + + + + + + + + } + variant="contained" + > + Create custom role + + ) + } + /> + + + + + + {roles + ?.sort((a, b) => a.name.localeCompare(b.name)) + .map((role) => ( = ({ onDelete={() => onDeleteRole(role)} /> ))} - - - -
-
-
+ + + + + ); }; interface RoleRowProps { - role: Role; + role: AssignableRoles; onDelete: () => void; canAssignOrgRole: boolean; } @@ -134,25 +194,27 @@ const RoleRow: FC = ({ role, onDelete, canAssignOrgRole }) => { - - - - - - { - navigate(role.name); - }} - > - Edit - - {canAssignOrgRole && ( - - Delete… + {!role.built_in && ( + + + + + + { + navigate(role.name); + }} + > + Edit - )} - - + {canAssignOrgRole && ( + + Delete… + + )} + + + )} ); @@ -180,6 +242,15 @@ const styles = { secondary: (theme) => ({ color: theme.palette.text.secondary, }), + tableHeader: () => ({ + marginBottom: 0, + fontSize: 18, + }), + tableDescription: (theme) => ({ + fontSize: 14, + color: theme.palette.text.secondary, + lineHeight: "160%", + }), } satisfies Record>; export default CustomRolesPageView; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index b67ab46da5..ff4ba95f5f 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -318,19 +318,23 @@ export const MockOrganizationTemplateAdminRole: TypesGen.Role = { organization_id: MockOrganization.id, }; -export const MockOrganizationAuditorRole: TypesGen.Role = { +export const MockOrganizationAuditorRole: TypesGen.AssignableRoles = { name: "organization-auditor", display_name: "Organization Auditor", + assignable: true, + built_in: false, site_permissions: [], organization_permissions: [], user_permissions: [], organization_id: MockOrganization.id, }; -export const MockRoleWithOrgPermissions: TypesGen.Role = { +export const MockRoleWithOrgPermissions: TypesGen.AssignableRoles = { name: "my-role-1", display_name: "My Role 1", organization_id: MockOrganization.id, + assignable: true, + built_in: false, site_permissions: [], organization_permissions: [ {