Skip to content

Commit 11db948

Browse files
committed
feat: add stories for CreateEditRolePageView
1 parent 124090b commit 11db948

File tree

5 files changed

+259
-175
lines changed

5 files changed

+259
-175
lines changed

site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePage.tsx

Lines changed: 13 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
1-
import Button from "@mui/material/Button";
2-
import { useFormik } from "formik";
31
import type { FC } from "react";
42
import { Helmet } from "react-helmet-async";
53
import { useMutation, useQuery, useQueryClient } from "react-query";
64
import { useNavigate, useParams } from "react-router-dom";
7-
import * as Yup from "yup";
85
import { getErrorMessage } from "api/errors";
96
import { organizationPermissions } from "api/queries/organizations";
107
import { patchOrganizationRole, organizationRoles } from "api/queries/roles";
118
import type { PatchRoleRequest } from "api/typesGenerated";
129
import { displayError } from "components/GlobalSnackbar/utils";
1310
import { Loader } from "components/Loader/Loader";
14-
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
15-
import { nameValidator } from "utils/formUtils";
1611
import { pageTitle } from "utils/page";
1712
import { useOrganizationSettings } from "../ManagementSettingsLayout";
1813
import CreateEditRolePageView from "./CreateEditRolePageView";
@@ -36,32 +31,7 @@ export const CreateEditRolePage: FC = () => {
3631
const role = roleData?.find((role) => role.name === roleName);
3732
const permissions = permissionsQuery.data;
3833

39-
const validationSchema = Yup.object({
40-
name: nameValidator("Name"),
41-
});
42-
43-
const onSubmit = async (data: PatchRoleRequest) => {
44-
try {
45-
await patchOrganizationRoleMutation.mutateAsync(data);
46-
navigate(`/organizations/${organizationName}/roles`);
47-
} catch (error) {
48-
displayError(getErrorMessage(error, "Failed to update custom role"));
49-
}
50-
};
51-
52-
const form = useFormik<PatchRoleRequest>({
53-
initialValues: {
54-
name: role?.name || "",
55-
display_name: role?.display_name || "",
56-
site_permissions: role?.site_permissions || [],
57-
organization_permissions: role?.organization_permissions || [],
58-
user_permissions: role?.user_permissions || [],
59-
},
60-
validationSchema,
61-
onSubmit,
62-
});
63-
64-
if (isLoading) {
34+
if (isLoading || !permissions) {
6535
return <Loader />;
6636
}
6737

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

78-
<PageHeader
79-
actions={
80-
permissions &&
81-
permissions.assignOrgRole && (
82-
<>
83-
<Button
84-
onClick={() => {
85-
navigate(`/organizations/${organizationName}/roles`);
86-
}}
87-
>
88-
Cancel
89-
</Button>
90-
<Button
91-
variant="contained"
92-
color="primary"
93-
onClick={() => {
94-
form.handleSubmit();
95-
}}
96-
>
97-
{role !== undefined ? "Save" : "Create Role"}
98-
</Button>
99-
</>
100-
)
101-
}
102-
>
103-
<PageHeaderTitle>
104-
{role ? "Edit" : "Create"} custom role
105-
</PageHeaderTitle>
106-
</PageHeader>
107-
10848
<CreateEditRolePageView
10949
role={role}
110-
form={form}
50+
onSubmit={async (data: PatchRoleRequest) => {
51+
try {
52+
await patchOrganizationRoleMutation.mutateAsync(data);
53+
navigate(`/organizations/${organizationName}/roles`);
54+
} catch (error) {
55+
displayError(
56+
getErrorMessage(error, "Failed to update custom role"),
57+
);
58+
}
59+
}}
11160
error={patchOrganizationRoleMutation.error}
11261
isLoading={patchOrganizationRoleMutation.isLoading}
62+
organizationName={organizationName}
63+
canAssignOrgRole={permissions.assignOrgRole}
11364
/>
11465
</>
11566
);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import {
3+
mockApiError,
4+
MockRoleWithOrgPermissions,
5+
assignableRole,
6+
} from "testHelpers/entities";
7+
import { CreateEditRolePageView } from "./CreateEditRolePageView";
8+
9+
const meta: Meta<typeof CreateEditRolePageView> = {
10+
title: "pages/OrganizationCreateEditRolePage",
11+
component: CreateEditRolePageView,
12+
};
13+
14+
export default meta;
15+
type Story = StoryObj<typeof CreateEditRolePageView>;
16+
17+
export const Default: Story = {
18+
args: {
19+
role: assignableRole(MockRoleWithOrgPermissions, true),
20+
onSubmit: () => null,
21+
error: undefined,
22+
isLoading: false,
23+
organizationName: "my-org",
24+
canAssignOrgRole: true,
25+
},
26+
};
27+
28+
export const WithError: Story = {
29+
args: {
30+
role: assignableRole(MockRoleWithOrgPermissions, true),
31+
onSubmit: () => null,
32+
error: mockApiError({
33+
message: "A role named new-role already exists.",
34+
validations: [{ field: "name", detail: "Role names must be unique" }],
35+
}),
36+
isLoading: false,
37+
organizationName: "my-org",
38+
canAssignOrgRole: true,
39+
},
40+
};
41+
42+
export const CannotEdit: Story = {
43+
args: {
44+
role: assignableRole(MockRoleWithOrgPermissions, true),
45+
onSubmit: () => null,
46+
error: undefined,
47+
isLoading: false,
48+
organizationName: "my-org",
49+
canAssignOrgRole: false,
50+
},
51+
};
52+
53+
export const ShowAllResources: Story = {
54+
args: {
55+
role: assignableRole(MockRoleWithOrgPermissions, true),
56+
onSubmit: () => null,
57+
error: undefined,
58+
isLoading: false,
59+
organizationName: "my-org",
60+
canAssignOrgRole: true,
61+
allResources: true,
62+
},
63+
};

site/src/pages/ManagementSettingsPage/CustomRolesPage/CreateEditRolePageView.tsx

Lines changed: 108 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Interpolation, Theme } from "@emotion/react";
2+
import Button from "@mui/material/Button";
23
import Checkbox from "@mui/material/Checkbox";
34
import FormControlLabel from "@mui/material/FormControlLabel";
45
import Table from "@mui/material/Table";
@@ -8,9 +9,10 @@ import TableContainer from "@mui/material/TableContainer";
89
import TableHead from "@mui/material/TableHead";
910
import TableRow from "@mui/material/TableRow";
1011
import TextField from "@mui/material/TextField";
11-
import type { useFormik } from "formik";
12+
import { useFormik } from "formik";
1213
import { type ChangeEvent, useState, type FC } from "react";
1314
import { useNavigate } from "react-router-dom";
15+
import * as Yup from "yup";
1416
import { isApiValidationError } from "api/errors";
1517
import { RBACResourceActions } from "api/rbacresources_gen";
1618
import type {
@@ -28,71 +30,129 @@ import {
2830
FormSection,
2931
HorizontalForm,
3032
} from "components/Form/Form";
31-
import { getFormHelpers } from "utils/formUtils";
33+
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
34+
import { getFormHelpers, nameValidator } from "utils/formUtils";
35+
36+
const validationSchema = Yup.object({
37+
name: nameValidator("Name"),
38+
});
3239

3340
export type CreateEditRolePageViewProps = {
3441
role: AssignableRoles | undefined;
35-
form: ReturnType<typeof useFormik<PatchRoleRequest>>;
42+
onSubmit: (data: PatchRoleRequest) => void;
3643
error?: unknown;
3744
isLoading: boolean;
45+
organizationName: string;
46+
canAssignOrgRole: boolean;
47+
allResources?: boolean;
3848
};
3949

4050
export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
4151
role,
42-
form,
52+
onSubmit,
4353
error,
4454
isLoading,
55+
organizationName,
56+
canAssignOrgRole,
57+
allResources = false,
4558
}) => {
4659
const navigate = useNavigate();
47-
const getFieldHelpers = getFormHelpers<Role>(form, error);
4860
const onCancel = () => navigate(-1);
4961

62+
const form = useFormik<PatchRoleRequest>({
63+
initialValues: {
64+
name: role?.name || "",
65+
display_name: role?.display_name || "",
66+
site_permissions: role?.site_permissions || [],
67+
organization_permissions: role?.organization_permissions || [],
68+
user_permissions: role?.user_permissions || [],
69+
},
70+
validationSchema,
71+
onSubmit,
72+
});
73+
74+
const getFieldHelpers = getFormHelpers<Role>(form, error);
75+
5076
return (
51-
<HorizontalForm onSubmit={form.handleSubmit}>
52-
<FormSection
53-
title="Role settings"
54-
description="Set a name and permissions for this role."
77+
<>
78+
<PageHeader
79+
actions={
80+
canAssignOrgRole && (
81+
<>
82+
<Button
83+
onClick={() => {
84+
navigate(`/organizations/${organizationName}/roles`);
85+
}}
86+
>
87+
Cancel
88+
</Button>
89+
<Button
90+
variant="contained"
91+
color="primary"
92+
onClick={() => {
93+
form.handleSubmit();
94+
}}
95+
>
96+
{role !== undefined ? "Save" : "Create Role"}
97+
</Button>
98+
</>
99+
)
100+
}
55101
>
56-
<FormFields>
57-
{Boolean(error) && !isApiValidationError(error) && (
58-
<ErrorAlert error={error} />
59-
)}
60-
61-
<TextField
62-
{...getFieldHelpers("name", {
63-
helperText:
64-
"The role name cannot be modified after the role is created.",
65-
})}
66-
autoFocus
67-
fullWidth
68-
disabled={role !== undefined}
69-
label="Name"
70-
/>
71-
<TextField
72-
{...getFieldHelpers("display_name", {
73-
helperText: "Optional: keep empty to default to the name.",
74-
})}
75-
fullWidth
76-
label="Display Name"
77-
/>
78-
<ActionCheckboxes
79-
permissions={role?.organization_permissions || []}
80-
form={form}
102+
<PageHeaderTitle>
103+
{role ? "Edit" : "Create"} custom role
104+
</PageHeaderTitle>
105+
</PageHeader>
106+
<HorizontalForm onSubmit={form.handleSubmit}>
107+
<FormSection
108+
title="Role settings"
109+
description="Set a name and permissions for this role."
110+
>
111+
<FormFields>
112+
{Boolean(error) && !isApiValidationError(error) && (
113+
<ErrorAlert error={error} />
114+
)}
115+
116+
<TextField
117+
{...getFieldHelpers("name", {
118+
helperText:
119+
"The role name cannot be modified after the role is created.",
120+
})}
121+
autoFocus
122+
fullWidth
123+
disabled={role !== undefined}
124+
label="Name"
125+
/>
126+
<TextField
127+
{...getFieldHelpers("display_name", {
128+
helperText: "Optional: keep empty to default to the name.",
129+
})}
130+
fullWidth
131+
label="Display Name"
132+
/>
133+
<ActionCheckboxes
134+
permissions={role?.organization_permissions || []}
135+
form={form}
136+
allResources={allResources}
137+
/>
138+
</FormFields>
139+
</FormSection>
140+
{canAssignOrgRole && (
141+
<FormFooter
142+
onCancel={onCancel}
143+
isLoading={isLoading}
144+
submitLabel={role !== undefined ? "Save" : "Create Role"}
81145
/>
82-
</FormFields>
83-
</FormSection>
84-
<FormFooter
85-
onCancel={onCancel}
86-
isLoading={isLoading}
87-
submitLabel={role !== undefined ? "Save" : "Create Role"}
88-
/>
89-
</HorizontalForm>
146+
)}
147+
</HorizontalForm>
148+
</>
90149
);
91150
};
92151

93152
interface ActionCheckboxesProps {
94153
permissions: readonly Permission[] | undefined;
95154
form: ReturnType<typeof useFormik<Role>> & { values: Role };
155+
allResources: boolean;
96156
}
97157

98158
const ResourceActionComparator = (
@@ -120,9 +180,13 @@ const filteredRBACResourceActions = Object.fromEntries(
120180
),
121181
);
122182

123-
const ActionCheckboxes: FC<ActionCheckboxesProps> = ({ permissions, form }) => {
183+
const ActionCheckboxes: FC<ActionCheckboxesProps> = ({
184+
permissions,
185+
form,
186+
allResources,
187+
}) => {
124188
const [checkedActions, setCheckActions] = useState(permissions);
125-
const [showAllResources, setShowAllResources] = useState(false);
189+
const [showAllResources, setShowAllResources] = useState(allResources);
126190

127191
const handleActionCheckChange = async (
128192
e: ChangeEvent<HTMLInputElement>,

0 commit comments

Comments
 (0)