Skip to content

Commit e3647cb

Browse files
committed
feat: add page to create and edit custom roles
1 parent 1b450da commit e3647cb

File tree

6 files changed

+321
-160
lines changed

6 files changed

+321
-160
lines changed

site/src/api/api.ts

+21-5
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,7 @@ export const watchBuildLogsByTemplateVersionId = (
187187

188188
const proto = location.protocol === "https:" ? "wss:" : "ws:";
189189
const socket = new WebSocket(
190-
`${proto}//${
191-
location.host
190+
`${proto}//${location.host
192191
}/api/v2/templateversions/${versionId}/logs?${searchParams.toString()}`,
193192
);
194193

@@ -270,8 +269,7 @@ export const watchBuildLogsByBuildId = (
270269
}
271270
const proto = location.protocol === "https:" ? "wss:" : "ws:";
272271
const socket = new WebSocket(
273-
`${proto}//${
274-
location.host
272+
`${proto}//${location.host
275273
}/api/v2/workspacebuilds/${buildId}/logs?${searchParams.toString()}`,
276274
);
277275
socket.binaryType = "blob";
@@ -394,7 +392,7 @@ export class MissingBuildParameters extends Error {
394392
* lexical scope.
395393
*/
396394
class ApiMethods {
397-
constructor(protected readonly axios: AxiosInstance) {}
395+
constructor(protected readonly axios: AxiosInstance) { }
398396

399397
login = async (
400398
email: string,
@@ -600,6 +598,24 @@ class ApiMethods {
600598
return response.data;
601599
};
602600

601+
patchOrganizationRole = async (
602+
organizationId: string,
603+
role: TypesGen.Role,
604+
): Promise<TypesGen.Role> => {
605+
const response = await this.axios.patch<TypesGen.Role>(
606+
`/api/v2/organizations/${organizationId}/members/roles`,
607+
role,
608+
);
609+
610+
return response.data;
611+
};
612+
613+
deleteOrganizationRole = async (organizationId: string, roleName: string) => {
614+
await this.axios.delete(
615+
`/api/v2/organizations/${organizationId}/members/roles/${roleName}`,
616+
);
617+
};
618+
603619
/**
604620
* @param organization Can be the organization's ID or name
605621
*/

site/src/api/queries/roles.ts

+36
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1+
import type { QueryClient } from "react-query";
12
import { API } from "api/api";
3+
import type { Role } from "api/typesGenerated";
4+
5+
const getRoleQueryKey = (organizationId: string, roleName: string) => [
6+
"organization",
7+
organizationId,
8+
"role",
9+
roleName,
10+
];
211

312
export const roles = () => {
413
return {
@@ -13,3 +22,30 @@ export const organizationRoles = (organizationId: string) => {
1322
queryFn: () => API.getOrganizationRoles(organizationId),
1423
};
1524
};
25+
26+
export const patchOrganizationRole = (
27+
queryClient: QueryClient,
28+
organizationId: string,
29+
) => {
30+
return {
31+
mutationFn: (request: Role) =>
32+
API.patchOrganizationRole(organizationId, request),
33+
onSuccess: async (updatedRole: Role) =>
34+
await queryClient.invalidateQueries(
35+
getRoleQueryKey(organizationId, updatedRole.name),
36+
),
37+
};
38+
};
39+
40+
export const deleteRole = (
41+
queryClient: QueryClient,
42+
organizationId: string,
43+
) => {
44+
return {
45+
mutationFn: API.deleteOrganizationRole,
46+
onSuccess: async (_: void, roleName: string) =>
47+
await queryClient.invalidateQueries(
48+
getRoleQueryKey(organizationId, roleName),
49+
),
50+
};
51+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { FC } from "react";
2+
import { Helmet } from "react-helmet-async";
3+
import { useMutation, useQueryClient } from "react-query";
4+
import { useNavigate, useParams } from "react-router-dom";
5+
import { patchOrganizationRole } from "api/queries/roles";
6+
import { pageTitle } from "utils/page";
7+
import CreateEditRolePageView from "./CreateEditRolePageView";
8+
9+
export const CreateGroupPage: FC = () => {
10+
const queryClient = useQueryClient();
11+
const navigate = useNavigate();
12+
const { organization } = useParams() as { organization: string };
13+
const patchOrganizationRoleMutation = useMutation(
14+
patchOrganizationRole(queryClient, organization ?? "default"),
15+
);
16+
17+
return (
18+
<>
19+
<Helmet>
20+
<title>{pageTitle("Create Custom Role")}</title>
21+
</Helmet>
22+
<CreateEditRolePageView
23+
onSubmit={async (data) => {
24+
const newRole = await patchOrganizationRoleMutation.mutateAsync(data);
25+
console.log({ newRole });
26+
navigate(`/organizations/${organization}/roles`);
27+
}}
28+
error={patchOrganizationRoleMutation.error}
29+
isLoading={patchOrganizationRoleMutation.isLoading}
30+
/>
31+
</>
32+
);
33+
};
34+
35+
export default CreateGroupPage;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import Table from "@mui/material/Table";
3+
import TableBody from "@mui/material/TableBody";
4+
import TableCell from "@mui/material/TableCell";
5+
import TableContainer from "@mui/material/TableContainer";
6+
import TableRow from "@mui/material/TableRow";
7+
import TextField from "@mui/material/TextField";
8+
import { useFormik } from "formik";
9+
import type { FC } from "react";
10+
import { useNavigate } from "react-router-dom";
11+
import * as Yup from "yup";
12+
import { isApiValidationError } from "api/errors";
13+
import { RBACResourceActions } from "api/rbacresources_gen";
14+
import type { Role } from "api/typesGenerated";
15+
import { ErrorAlert } from "components/Alert/ErrorAlert";
16+
import {
17+
FormFields,
18+
FormFooter,
19+
FormSection,
20+
HorizontalForm,
21+
} from "components/Form/Form";
22+
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
23+
import { getFormHelpers } from "utils/formUtils";
24+
25+
const validationSchema = Yup.object({
26+
name: Yup.string().required().label("Name"),
27+
});
28+
29+
export type CreateEditRolePageViewProps = {
30+
onSubmit: (data: Role) => void;
31+
error?: unknown;
32+
isLoading: boolean;
33+
};
34+
35+
export const CreateEditRolePageView: FC<CreateEditRolePageViewProps> = ({
36+
onSubmit,
37+
error,
38+
isLoading,
39+
}) => {
40+
const navigate = useNavigate();
41+
const form = useFormik<Role>({
42+
initialValues: {
43+
name: "",
44+
display_name: "",
45+
site_permissions: [],
46+
organization_permissions: [],
47+
user_permissions: [],
48+
},
49+
validationSchema,
50+
onSubmit,
51+
});
52+
const getFieldHelpers = getFormHelpers<Role>(form, error);
53+
const onCancel = () => navigate(-1);
54+
55+
return (
56+
<>
57+
<PageHeader css={{ paddingTop: 8 }}>
58+
<PageHeaderTitle>Create custom role</PageHeaderTitle>
59+
</PageHeader>
60+
<HorizontalForm onSubmit={form.handleSubmit}>
61+
<FormSection
62+
title="Role settings"
63+
description="Set a name for this role."
64+
>
65+
<FormFields>
66+
{Boolean(error) && !isApiValidationError(error) && (
67+
<ErrorAlert error={error} />
68+
)}
69+
70+
<TextField
71+
{...getFieldHelpers("name")}
72+
autoFocus
73+
fullWidth
74+
label="Name"
75+
/>
76+
<TextField
77+
{...getFieldHelpers("display_name", {
78+
helperText: "Optional: keep empty to default to the name.",
79+
})}
80+
fullWidth
81+
label="Display Name"
82+
/>
83+
<ActionCheckboxes permissions={[]}></ActionCheckboxes>
84+
</FormFields>
85+
</FormSection>
86+
<FormFooter onCancel={onCancel} isLoading={isLoading} />
87+
</HorizontalForm>
88+
</>
89+
);
90+
};
91+
92+
interface ActionCheckboxesProps {
93+
permissions: Permissions[];
94+
}
95+
96+
const ActionCheckboxes: FC<ActionCheckboxesProps> = ({ permissions }) => {
97+
return (
98+
<TableContainer>
99+
<Table>
100+
<TableBody>
101+
{Object.entries(RBACResourceActions).map(([key, value]) => {
102+
return (
103+
<TableRow key={key}>
104+
<TableCell>
105+
<li key={key} css={styles.checkBoxes}>
106+
<input type="checkbox" /> {key}
107+
<ul css={styles.checkBoxes}>
108+
{Object.entries(value).map(([key, value]) => {
109+
return (
110+
<li key={key}>
111+
<span css={styles.actionText}>
112+
<input type="checkbox" /> {key}
113+
</span>{" "}
114+
-{" "}
115+
<span css={styles.actionDescription}>{value}</span>
116+
</li>
117+
);
118+
})}
119+
</ul>
120+
</li>
121+
</TableCell>
122+
</TableRow>
123+
);
124+
})}
125+
</TableBody>
126+
</Table>
127+
</TableContainer>
128+
);
129+
};
130+
131+
const styles = {
132+
rolesDropdown: {
133+
marginBottom: 20,
134+
},
135+
checkBoxes: {
136+
margin: 0,
137+
listStyleType: "none",
138+
},
139+
actionText: (theme) => ({
140+
color: theme.palette.text.primary,
141+
}),
142+
actionDescription: (theme) => ({
143+
color: theme.palette.text.secondary,
144+
}),
145+
} satisfies Record<string, Interpolation<Theme>>;
146+
147+
export default CreateEditRolePageView;

0 commit comments

Comments
 (0)