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: add stories for CreateEditRolePageView
  • Loading branch information
jaaydenh committed Aug 8, 2024
commit 11db948e34055a14fdbba7a1296c7af568608557
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import Button from "@mui/material/Button";
import { useFormik } from "formik";
import type { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useNavigate, useParams } from "react-router-dom";
import * as Yup from "yup";
import { getErrorMessage } from "api/errors";
import { organizationPermissions } from "api/queries/organizations";
import { patchOrganizationRole, organizationRoles } from "api/queries/roles";
import type { PatchRoleRequest } from "api/typesGenerated";
import { displayError } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { nameValidator } from "utils/formUtils";
import { pageTitle } from "utils/page";
import { useOrganizationSettings } from "../ManagementSettingsLayout";
import CreateEditRolePageView from "./CreateEditRolePageView";
Expand All @@ -36,32 +31,7 @@ export const CreateEditRolePage: FC = () => {
const role = roleData?.find((role) => role.name === roleName);
const permissions = permissionsQuery.data;

const validationSchema = Yup.object({
name: nameValidator("Name"),
});

const onSubmit = async (data: PatchRoleRequest) => {
try {
await patchOrganizationRoleMutation.mutateAsync(data);
navigate(`/organizations/${organizationName}/roles`);
} catch (error) {
displayError(getErrorMessage(error, "Failed to update custom role"));
}
};

const form = useFormik<PatchRoleRequest>({
initialValues: {
name: role?.name || "",
display_name: role?.display_name || "",
site_permissions: role?.site_permissions || [],
organization_permissions: role?.organization_permissions || [],
user_permissions: role?.user_permissions || [],
},
validationSchema,
onSubmit,
});

if (isLoading) {
if (isLoading || !permissions) {
return <Loader />;
}

Expand All @@ -75,41 +45,22 @@ export const CreateEditRolePage: FC = () => {
</title>
</Helmet>

<PageHeader
actions={
permissions &&
permissions.assignOrgRole && (
<>
<Button
onClick={() => {
navigate(`/organizations/${organizationName}/roles`);
}}
>
Cancel
</Button>
<Button
variant="contained"
color="primary"
onClick={() => {
form.handleSubmit();
}}
>
{role !== undefined ? "Save" : "Create Role"}
</Button>
</>
)
}
>
<PageHeaderTitle>
{role ? "Edit" : "Create"} custom role
</PageHeaderTitle>
</PageHeader>

<CreateEditRolePageView
role={role}
form={form}
onSubmit={async (data: PatchRoleRequest) => {
try {
await patchOrganizationRoleMutation.mutateAsync(data);
navigate(`/organizations/${organizationName}/roles`);
} catch (error) {
displayError(
getErrorMessage(error, "Failed to update custom role"),
);
}
}}
error={patchOrganizationRoleMutation.error}
isLoading={patchOrganizationRoleMutation.isLoading}
organizationName={organizationName}
canAssignOrgRole={permissions.assignOrgRole}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
mockApiError,
MockRoleWithOrgPermissions,
assignableRole,
} from "testHelpers/entities";
import { CreateEditRolePageView } from "./CreateEditRolePageView";

const meta: Meta<typeof CreateEditRolePageView> = {
title: "pages/OrganizationCreateEditRolePage",
component: CreateEditRolePageView,
};

export default meta;
type Story = StoryObj<typeof CreateEditRolePageView>;

export const Default: Story = {
args: {
role: assignableRole(MockRoleWithOrgPermissions, true),
onSubmit: () => null,
error: undefined,
isLoading: false,
organizationName: "my-org",
canAssignOrgRole: true,
},
};

export const WithError: Story = {
args: {
role: assignableRole(MockRoleWithOrgPermissions, true),
onSubmit: () => null,
error: mockApiError({
message: "A role named new-role already exists.",
validations: [{ field: "name", detail: "Role names must be unique" }],
}),
isLoading: false,
organizationName: "my-org",
canAssignOrgRole: true,
},
};

export const CannotEdit: Story = {
args: {
role: assignableRole(MockRoleWithOrgPermissions, true),
onSubmit: () => null,
error: undefined,
isLoading: false,
organizationName: "my-org",
canAssignOrgRole: false,
},
};

export const ShowAllResources: Story = {
args: {
role: assignableRole(MockRoleWithOrgPermissions, true),
onSubmit: () => null,
error: undefined,
isLoading: false,
organizationName: "my-org",
canAssignOrgRole: true,
allResources: true,
},
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Interpolation, Theme } from "@emotion/react";
import Button from "@mui/material/Button";
import Checkbox from "@mui/material/Checkbox";
import FormControlLabel from "@mui/material/FormControlLabel";
import Table from "@mui/material/Table";
Expand All @@ -8,9 +9,10 @@ import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import TextField from "@mui/material/TextField";
import type { useFormik } from "formik";
import { useFormik } from "formik";
import { type ChangeEvent, useState, type FC } 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 {
Expand All @@ -28,71 +30,129 @@ import {
FormSection,
HorizontalForm,
} from "components/Form/Form";
import { getFormHelpers } from "utils/formUtils";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { getFormHelpers, nameValidator } from "utils/formUtils";

const validationSchema = Yup.object({
name: nameValidator("Name"),
});

export type CreateEditRolePageViewProps = {
role: AssignableRoles | undefined;
form: ReturnType<typeof useFormik<PatchRoleRequest>>;
onSubmit: (data: PatchRoleRequest) => void;
error?: unknown;
isLoading: boolean;
organizationName: string;
canAssignOrgRole: boolean;
allResources?: boolean;
};

export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
role,
form,
onSubmit,
error,
isLoading,
organizationName,
canAssignOrgRole,
allResources = false,
}) => {
const navigate = useNavigate();
const getFieldHelpers = getFormHelpers<Role>(form, error);
const onCancel = () => navigate(-1);

const form = useFormik<PatchRoleRequest>({
initialValues: {
name: role?.name || "",
display_name: role?.display_name || "",
site_permissions: role?.site_permissions || [],
organization_permissions: role?.organization_permissions || [],
user_permissions: role?.user_permissions || [],
},
validationSchema,
onSubmit,
});

const getFieldHelpers = getFormHelpers<Role>(form, error);

return (
<HorizontalForm onSubmit={form.handleSubmit}>
<FormSection
title="Role settings"
description="Set a name and permissions for this role."
<>
<PageHeader
actions={
canAssignOrgRole && (
<>
<Button
onClick={() => {
navigate(`/organizations/${organizationName}/roles`);
}}
>
Cancel
</Button>
<Button
variant="contained"
color="primary"
onClick={() => {
form.handleSubmit();
}}
>
{role !== undefined ? "Save" : "Create Role"}
</Button>
</>
)
}
>
<FormFields>
{Boolean(error) && !isApiValidationError(error) && (
<ErrorAlert error={error} />
)}

<TextField
{...getFieldHelpers("name", {
helperText:
"The role name cannot be modified after the role is created.",
})}
autoFocus
fullWidth
disabled={role !== undefined}
label="Name"
/>
<TextField
{...getFieldHelpers("display_name", {
helperText: "Optional: keep empty to default to the name.",
})}
fullWidth
label="Display Name"
/>
<ActionCheckboxes
permissions={role?.organization_permissions || []}
form={form}
<PageHeaderTitle>
{role ? "Edit" : "Create"} custom role
</PageHeaderTitle>
</PageHeader>
<HorizontalForm onSubmit={form.handleSubmit}>
<FormSection
title="Role settings"
description="Set a name and permissions for this role."
>
<FormFields>
{Boolean(error) && !isApiValidationError(error) && (
<ErrorAlert error={error} />
)}

<TextField
{...getFieldHelpers("name", {
helperText:
"The role name cannot be modified after the role is created.",
})}
autoFocus
fullWidth
disabled={role !== undefined}
label="Name"
/>
<TextField
{...getFieldHelpers("display_name", {
helperText: "Optional: keep empty to default to the name.",
})}
fullWidth
label="Display Name"
/>
<ActionCheckboxes
permissions={role?.organization_permissions || []}
form={form}
allResources={allResources}
/>
</FormFields>
</FormSection>
{canAssignOrgRole && (
<FormFooter
onCancel={onCancel}
isLoading={isLoading}
submitLabel={role !== undefined ? "Save" : "Create Role"}
/>
</FormFields>
</FormSection>
<FormFooter
onCancel={onCancel}
isLoading={isLoading}
submitLabel={role !== undefined ? "Save" : "Create Role"}
/>
</HorizontalForm>
)}
</HorizontalForm>
</>
);
};

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

const ResourceActionComparator = (
Expand Down Expand Up @@ -120,9 +180,13 @@ const filteredRBACResourceActions = Object.fromEntries(
),
);

const ActionCheckboxes: FC<ActionCheckboxesProps> = ({ permissions, form }) => {
const ActionCheckboxes: FC<ActionCheckboxesProps> = ({
permissions,
form,
allResources,
}) => {
const [checkedActions, setCheckActions] = useState(permissions);
const [showAllResources, setShowAllResources] = useState(false);
const [showAllResources, setShowAllResources] = useState(allResources);

const handleActionCheckChange = async (
e: ChangeEvent<HTMLInputElement>,
Expand Down
Loading
Loading