Skip to content

feat: add custom roles #14069

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 28 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1b450da
feat: initial commit custom roles
jaaydenh Jul 31, 2024
e3647cb
feat: add page to create and edit custom roles
jaaydenh Jul 31, 2024
14f7bfc
feat: add assign org role permission
jaaydenh Aug 1, 2024
928de7e
feat: wip
jaaydenh Aug 2, 2024
8ebfee7
feat: cleanup
jaaydenh Aug 2, 2024
cb870e1
fix: role name is disabled when editing the role
jaaydenh Aug 2, 2024
6dff9ed
fix: assign role context menu falls back to name when no display_name
jaaydenh Aug 2, 2024
688ab79
feat: add helper text to let users know that role name is immutable
jaaydenh Aug 2, 2024
68eae17
fix: format
jaaydenh Aug 2, 2024
fbea545
feat: - hide custom roles tab if experiment is not enabled
jaaydenh Aug 2, 2024
b4a460f
fix: use custom TableLoader
jaaydenh Aug 2, 2024
832573f
fix: fix custom roles text
jaaydenh Aug 2, 2024
a5415c5
fix: use PatchRoleRequest
jaaydenh Aug 2, 2024
8db1da0
fix: use addIcon to create roles
jaaydenh Aug 2, 2024
4e05a33
feat: add cancel and save buttons to top of page
jaaydenh Aug 2, 2024
a17f579
fix: use nameValidator for name
jaaydenh Aug 4, 2024
e9af2f9
chore: cleanup
jaaydenh Aug 5, 2024
e40f0bf
feat: add show all permissions checkbox
jaaydenh Aug 6, 2024
add45fb
fix: update sidebar for roles
jaaydenh Aug 6, 2024
dfe82b1
fix: fix format
jaaydenh Aug 6, 2024
f623356
fix: custom roles is not needed outside orgs
jaaydenh Aug 6, 2024
f7860aa
fix: fix sidebar stories
jaaydenh Aug 6, 2024
764b15f
feat: add custom roles page stories
jaaydenh Aug 6, 2024
124090b
fix: use organization permissions
jaaydenh Aug 7, 2024
11db948
feat: add stories for CreateEditRolePageView
jaaydenh Aug 8, 2024
e18bf46
fix: design improvements for the create edit role form
jaaydenh Aug 8, 2024
d455c4e
feat: add show all resources checkbox to bottom of table
jaaydenh Aug 9, 2024
72f1bab
feat: improve spacing
jaaydenh Aug 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: wip
  • Loading branch information
jaaydenh committed Aug 6, 2024
commit 928de7e5b9fa5a45c7caa490b1100b1e4dc09efc
Original file line number Diff line number Diff line change
@@ -1,29 +1,47 @@
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQueryClient } from "react-query";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useNavigate, useParams } from "react-router-dom";
import { patchOrganizationRole } from "api/queries/roles";
import { getErrorMessage } from "api/errors";
import { patchOrganizationRole, organizationRoles } from "api/queries/roles";
import { displayError } from "components/GlobalSnackbar/utils";
import { pageTitle } from "utils/page";
import CreateEditRolePageView from "./CreateEditRolePageView";

export const CreateGroupPage: FC = () => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { organization } = useParams() as { organization: string };
const { organization, roleName } = useParams() as {
organization: string;
roleName: string;
};
const patchOrganizationRoleMutation = useMutation(
patchOrganizationRole(queryClient, organization ?? "default"),
);
const { data } = useQuery(organizationRoles(organization));
const role = data?.find((role) => role.name === roleName);
const pageTitleText =
role !== undefined ? "Edit Custom Role" : "Create Custom Role";

return (
<>
<Helmet>
<title>{pageTitle("Create Custom Role")}</title>
<title>{pageTitle(pageTitleText)}</title>
</Helmet>
<CreateEditRolePageView
role={role}
organization={organization}
onSubmit={async (data) => {
const newRole = await patchOrganizationRoleMutation.mutateAsync(data);
console.log({ newRole });
navigate(`/organizations/${organization}/roles`);
try {
console.log({ data });
await patchOrganizationRoleMutation.mutateAsync(data);
navigate(`/organizations/${organization}/roles`);
} catch (error) {
console.log({ error });
displayError(
getErrorMessage(error, "Failed to update custom role"),
);
}
}}
error={patchOrganizationRoleMutation.error}
isLoading={patchOrganizationRoleMutation.isLoading}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import type { Interpolation, Theme } from "@emotion/react";
import Checkbox from "@mui/material/Checkbox";
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 TableRow from "@mui/material/TableRow";
import TextField from "@mui/material/TextField";
import { useFormik } from "formik";
import type { FC } from "react";
import { type FormikValues, useFormik } from "formik";
import { type ChangeEvent, useState, type FC, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import * as Yup from "yup";
import { isApiValidationError } from "api/errors";
import { RBACResourceActions } from "api/rbacresources_gen";
import type { Role } from "api/typesGenerated";
import type {
Role,
Permission,
AssignableRoles,
RBACResource,
RBACAction,
} from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import {
FormFields,
Expand All @@ -27,24 +34,29 @@ const validationSchema = Yup.object({
});

export type CreateEditRolePageViewProps = {
role: AssignableRoles | undefined;
organization: string;
onSubmit: (data: Role) => void;
error?: unknown;
isLoading: boolean;
};

export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
role,
organization,
onSubmit,
error,
isLoading,
}) => {
const navigate = useNavigate();
const form = useFormik<Role>({
initialValues: {
name: "",
display_name: "",
site_permissions: [],
organization_permissions: [],
user_permissions: [],
name: role?.name || "",
organization_id: role?.organization_id || organization,
display_name: role?.display_name || "",
site_permissions: role?.site_permissions || [],
organization_permissions: role?.organization_permissions || [],
user_permissions: role?.user_permissions || [],
},
validationSchema,
onSubmit,
Expand All @@ -55,12 +67,14 @@ export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
return (
<>
<PageHeader css={{ paddingTop: 8 }}>
<PageHeaderTitle>Create custom role</PageHeaderTitle>
<PageHeaderTitle>
{role ? "Edit" : "Create"} custom role
</PageHeaderTitle>
</PageHeader>
<HorizontalForm onSubmit={form.handleSubmit}>
<FormSection
title="Role settings"
description="Set a name for this role."
description="Set a name and permissions for this role."
>
<FormFields>
{Boolean(error) && !isApiValidationError(error) && (
Expand All @@ -80,7 +94,10 @@ export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
fullWidth
label="Display Name"
/>
<ActionCheckboxes permissions={[]}></ActionCheckboxes>
<ActionCheckboxes
permissions={role?.organization_permissions || []}
form={form}
/>
</FormFields>
</FormSection>
<FormFooter onCancel={onCancel} isLoading={isLoading} />
Expand All @@ -90,26 +107,69 @@ export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
};

interface ActionCheckboxesProps {
permissions: Permissions[];
permissions: readonly Permission[] | undefined;
form: ReturnType<typeof useFormik<Role>> & { values: Role };
}

const ActionCheckboxes: FC<ActionCheckboxesProps> = ({ permissions }) => {
const ActionCheckboxes: FC<ActionCheckboxesProps> = ({ permissions, form }) => {
const [checkedActions, setIsChecked] = useState(permissions);

const handleCheckChange = async (
e: ChangeEvent<HTMLInputElement>,
form: ReturnType<typeof useFormik<Role>> & { values: Role },
) => {
const { name, checked } = e.currentTarget;
const [resource_type, action] = name.split(":");

const newPermissions = checked
? [
...(checkedActions ?? []),
{
negate: false,
resource_type: resource_type as RBACResource,
action: action as RBACAction,
},
]
: checkedActions?.filter(
(p) => p.resource_type !== resource_type || p.action !== action,
);

setIsChecked(newPermissions);
await form.setFieldValue("organization_permissions", checkedActions);
};

// useEffect(() => {
// setIsChecked(permissions);
// }, [permissions]);

return (
<TableContainer>
<Table>
<TableBody>
{Object.entries(RBACResourceActions).map(([key, value]) => {
{Object.entries(RBACResourceActions).map(([resourceKey, value]) => {
return (
<TableRow key={key}>
<TableRow key={resourceKey}>
<TableCell>
<li key={key} css={styles.checkBoxes}>
<input type="checkbox" /> {key}
<li key={resourceKey} css={styles.checkBoxes}>
{resourceKey}
<ul css={styles.checkBoxes}>
{Object.entries(value).map(([key, value]) => {
{Object.entries(value).map(([actionKey, value]) => {
return (
<li key={key}>
<li key={actionKey}>
<span css={styles.actionText}>
<input type="checkbox" /> {key}
<Checkbox
name={`${resourceKey}:${actionKey}`}
checked={
checkedActions?.some(
(p) =>
p.resource_type === resourceKey &&
(p.action.toString() === "*" ||
p.action === actionKey),
) || false
}
onChange={(e) => handleCheckChange(e, form)}
/>
{actionKey}
</span>{" "}
-{" "}
<span css={styles.actionDescription}>{value}</span>
Expand All @@ -129,9 +189,6 @@ const ActionCheckboxes: FC<ActionCheckboxesProps> = ({ permissions }) => {
};

const styles = {
rolesDropdown: {
marginBottom: 20,
},
checkBoxes: {
margin: 0,
listStyleType: "none",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { Interpolation, Theme } from "@emotion/react";
import AddOutlined from "@mui/icons-material/AddOutlined";
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
import { LoadingButton } from "@mui/lab";
import { createFilterOptions } from "@mui/material/Autocomplete";
import Button from "@mui/material/Button";
import Skeleton from "@mui/material/Skeleton";
import Table from "@mui/material/Table";
Expand All @@ -11,19 +9,17 @@ 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 { useState, type FC } from "react";
import type { FC } from "react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import type { Role } from "api/typesGenerated";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Paywall } from "components/Paywall/Paywall";
import { Stack } from "components/Stack/Stack";
import {
TableLoader,
TableLoaderSkeleton,
TableRowSkeleton,
} from "components/TableLoader/TableLoader";
import { permissionsToCheck } from "contexts/auth/permissions";
import { useClickableTableRow } from "hooks";
import { docs } from "utils/docs";

Expand All @@ -33,17 +29,14 @@ export type CustomRolesPageViewProps = {
isCustomRolesEnabled: boolean;
};

// const filter = createFilterOptions<Role>();

export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
roles,
canAssignOrgRole,
isCustomRolesEnabled,
}) => {
const isLoading = Boolean(roles === undefined);
const isEmpty = Boolean(roles && roles.length === 0);
// const [selectedRole, setSelectedRole] = useState<Role | null>(null);
console.log({ roles });

return (
<>
<ChooseOne>
Expand All @@ -59,9 +52,8 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
<Table>
<TableHead>
<TableRow>
<TableCell width="33%">Name</TableCell>
<TableCell width="33%">Display Name</TableCell>
<TableCell width="33%">Permissions</TableCell>
<TableCell width="50%">Name</TableCell>
<TableCell width="49%">Permissions</TableCell>
<TableCell width="1%"></TableCell>
</TableRow>
</TableHead>
Expand Down Expand Up @@ -125,9 +117,7 @@ const RoleRow: FC<RoleRowProps> = ({ role }) => {

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

<TableCell css={styles.secondary}>{role.display_name}</TableCell>
<TableCell>{role.display_name || role.name}</TableCell>

<TableCell css={styles.secondary}>
{role.organization_permissions.length}
Expand Down