feat: display builtin roles alongside custom roles (#14843)

Since its currently not possible to update or delete built-in roles. The
purpose of this PR is to display the built-in roles so that users know
they exist and what permissions each role contains.


<img width="1185" alt="Screenshot 2024-09-26 at 9 18 05 PM"
src="https://github.com/user-attachments/assets/017a51d7-ec98-409c-9c8e-b66ac7abb948">
This commit is contained in:
Jaayden Halko
2024-09-27 14:07:57 -04:00
committed by GitHub
parent 5cc5bbea04
commit 339eebacae
5 changed files with 178 additions and 98 deletions
@@ -162,6 +162,7 @@ const DEFAULT_RESOURCES = [
"organization_member",
"provisioner_daemon",
"workspace",
"idpsync_settings",
];
const resources = new Set(DEFAULT_RESOURCES);
@@ -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<Role>();
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"
>
<SettingsHeader
title="Custom Roles"
description="Manage custom roles for this organization."
title="Roles"
description="Manage roles for this organization."
/>
{permissions.assignOrgRole && isCustomRolesEnabled && (
<Button component={RouterLink} startIcon={<AddIcon />} to="create">
Create custom role
</Button>
)}
</Stack>
<CustomRolesPageView
roles={filteredRoleData}
builtInRoles={builtInRoles}
customRoles={customRoles}
onDeleteRole={setRoleToDelete}
canAssignOrgRole={permissions.assignOrgRole}
isCustomRolesEnabled={isCustomRolesEnabled}
@@ -15,7 +15,8 @@ type Story = StoryObj<typeof CustomRolesPageView>;
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,
},
@@ -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<CustomRolesPageViewProps> = ({
roles,
builtInRoles,
customRoles,
onDeleteRole,
canAssignOrgRole,
isCustomRolesEnabled,
}) => {
const isLoading = roles === undefined;
const isEmpty = Boolean(roles && roles.length === 0);
const theme = useTheme();
return (
<Stack spacing={4}>
{!isCustomRolesEnabled && (
@@ -53,53 +55,112 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
documentationLink={docs("/admin/groups")}
/>
)}
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="40%">Name</TableCell>
<TableCell width="59%">Permissions</TableCell>
<TableCell width="1%" />
</TableRow>
</TableHead>
<TableBody>
<ChooseOne>
<Cond condition={isLoading}>
<TableLoader />
</Cond>
<Stack
alignItems="baseline"
direction="row"
justifyContent="space-between"
>
<span>
<h2 css={styles.tableHeader}>Custom Roles</h2>
<span css={styles.tableDescription}>
Create custom roles to grant users a tailored set of granular
permissions.
</span>
</span>
{canAssignOrgRole && isCustomRolesEnabled && (
<Button component={RouterLink} startIcon={<AddIcon />} to="create">
Create custom role
</Button>
)}
</Stack>
<RoleTable
roles={customRoles}
isCustomRolesEnabled={isCustomRolesEnabled}
canAssignOrgRole={canAssignOrgRole}
onDeleteRole={onDeleteRole}
/>
<span>
<h2 css={styles.tableHeader}>Built-In Roles</h2>
<span css={styles.tableDescription}>
Built-in roles have predefined permissions. You cannot edit or delete
built-in roles.
</span>
</span>
<RoleTable
roles={builtInRoles}
isCustomRolesEnabled={isCustomRolesEnabled}
canAssignOrgRole={canAssignOrgRole}
onDeleteRole={onDeleteRole}
/>
</Stack>
);
};
<Cond condition={isEmpty}>
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No custom roles yet"
description={
canAssignOrgRole && isCustomRolesEnabled
? "Create your first custom role"
: !isCustomRolesEnabled
? "Upgrade to a premium license to create a custom role"
: "You don't have permission to create a custom role"
}
cta={
canAssignOrgRole &&
isCustomRolesEnabled && (
<Button
component={RouterLink}
to="create"
startIcon={<AddOutlined />}
variant="contained"
>
Create custom role
</Button>
)
}
/>
</TableCell>
</TableRow>
</Cond>
interface RoleTableProps {
roles: AssignableRoles[] | undefined;
isCustomRolesEnabled: boolean;
canAssignOrgRole: boolean;
onDeleteRole: (role: Role) => void;
}
<Cond>
{roles?.map((role) => (
const RoleTable: FC<RoleTableProps> = ({
roles,
isCustomRolesEnabled,
onDeleteRole,
canAssignOrgRole,
}) => {
const isLoading = roles === undefined;
const isEmpty = Boolean(roles && roles.length === 0);
return (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="40%">Name</TableCell>
<TableCell width="59%">Permissions</TableCell>
<TableCell width="1%" />
</TableRow>
</TableHead>
<TableBody>
<ChooseOne>
<Cond condition={isLoading}>
<TableLoader />
</Cond>
<Cond condition={isEmpty}>
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No custom roles yet"
description={
canAssignOrgRole && isCustomRolesEnabled
? "Create your first custom role"
: !isCustomRolesEnabled
? "Upgrade to a premium license to create a custom role"
: "You don't have permission to create a custom role"
}
cta={
canAssignOrgRole &&
isCustomRolesEnabled && (
<Button
component={RouterLink}
to="create"
startIcon={<AddOutlined />}
variant="contained"
>
Create custom role
</Button>
)
}
/>
</TableCell>
</TableRow>
</Cond>
<Cond>
{roles
?.sort((a, b) => a.name.localeCompare(b.name))
.map((role) => (
<RoleRow
key={role.name}
role={role}
@@ -107,17 +168,16 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
onDelete={() => onDeleteRole(role)}
/>
))}
</Cond>
</ChooseOne>
</TableBody>
</Table>
</TableContainer>
</Stack>
</Cond>
</ChooseOne>
</TableBody>
</Table>
</TableContainer>
);
};
interface RoleRowProps {
role: Role;
role: AssignableRoles;
onDelete: () => void;
canAssignOrgRole: boolean;
}
@@ -134,25 +194,27 @@ const RoleRow: FC<RoleRowProps> = ({ role, onDelete, canAssignOrgRole }) => {
</TableCell>
<TableCell>
<MoreMenu>
<MoreMenuTrigger>
<ThreeDotsButton />
</MoreMenuTrigger>
<MoreMenuContent>
<MoreMenuItem
onClick={() => {
navigate(role.name);
}}
>
Edit
</MoreMenuItem>
{canAssignOrgRole && (
<MoreMenuItem danger onClick={onDelete}>
Delete&hellip;
{!role.built_in && (
<MoreMenu>
<MoreMenuTrigger>
<ThreeDotsButton />
</MoreMenuTrigger>
<MoreMenuContent>
<MoreMenuItem
onClick={() => {
navigate(role.name);
}}
>
Edit
</MoreMenuItem>
)}
</MoreMenuContent>
</MoreMenu>
{canAssignOrgRole && (
<MoreMenuItem danger onClick={onDelete}>
Delete&hellip;
</MoreMenuItem>
)}
</MoreMenuContent>
</MoreMenu>
)}
</TableCell>
</TableRow>
);
@@ -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<string, Interpolation<Theme>>;
export default CustomRolesPageView;
+6 -2
View File
@@ -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: [
{