@firecms/user_management 3.0.0-3.0.0-beta.4.pre.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/dist/UserManagementProvider.d.ts +7 -0
  4. package/dist/admin_views.d.ts +2 -0
  5. package/dist/components/index.d.ts +2 -0
  6. package/dist/components/roles/RoleChip.d.ts +5 -0
  7. package/dist/components/roles/RolesDetailsForm.d.ts +20 -0
  8. package/dist/components/roles/RolesTable.d.ts +5 -0
  9. package/dist/components/roles/RolesView.d.ts +4 -0
  10. package/dist/components/roles/default_roles.d.ts +2 -0
  11. package/dist/components/roles/index.d.ts +4 -0
  12. package/dist/components/users/UserDetailsForm.d.ts +20 -0
  13. package/dist/components/users/UsersTable.d.ts +4 -0
  14. package/dist/components/users/UsersView.d.ts +4 -0
  15. package/dist/components/users/index.d.ts +3 -0
  16. package/dist/hooks/index.d.ts +2 -0
  17. package/dist/hooks/useBuildFirestoreUserManagement.d.ts +44 -0
  18. package/dist/hooks/useUserManagement.d.ts +2 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.es.js +1263 -0
  21. package/dist/index.es.js.map +1 -0
  22. package/dist/index.umd.js +2 -0
  23. package/dist/index.umd.js.map +1 -0
  24. package/dist/types/firecms_user.d.ts +7 -0
  25. package/dist/types/index.d.ts +3 -0
  26. package/dist/types/roles.d.ts +31 -0
  27. package/dist/types/user_management.d.ts +39 -0
  28. package/dist/useUserManagementPlugin.d.ts +5 -0
  29. package/dist/utils/colors.d.ts +2 -0
  30. package/dist/utils/index.d.ts +3 -0
  31. package/dist/utils/local_storage.d.ts +3 -0
  32. package/dist/utils/permissions.d.ts +9 -0
  33. package/dist/utils/useTraceUpdate.d.ts +1 -0
  34. package/package.json +76 -0
  35. package/src/UserManagementProvider.tsx +19 -0
  36. package/src/admin_views.tsx +19 -0
  37. package/src/components/index.ts +2 -0
  38. package/src/components/roles/RoleChip.tsx +28 -0
  39. package/src/components/roles/RolesDetailsForm.tsx +402 -0
  40. package/src/components/roles/RolesTable.tsx +139 -0
  41. package/src/components/roles/RolesView.tsx +63 -0
  42. package/src/components/roles/default_roles.tsx +36 -0
  43. package/src/components/roles/index.ts +4 -0
  44. package/src/components/users/UserDetailsForm.tsx +230 -0
  45. package/src/components/users/UsersTable.tsx +178 -0
  46. package/src/components/users/UsersView.tsx +59 -0
  47. package/src/components/users/index.ts +3 -0
  48. package/src/hooks/index.ts +2 -0
  49. package/src/hooks/useBuildFirestoreUserManagement.tsx +241 -0
  50. package/src/hooks/useUserManagement.tsx +5 -0
  51. package/src/index.ts +7 -0
  52. package/src/types/firecms_user.ts +8 -0
  53. package/src/types/index.ts +3 -0
  54. package/src/types/roles.ts +41 -0
  55. package/src/types/user_management.tsx +50 -0
  56. package/src/useUserManagementPlugin.tsx +18 -0
  57. package/src/utils/colors.ts +52 -0
  58. package/src/utils/index.ts +3 -0
  59. package/src/utils/local_storage.ts +53 -0
  60. package/src/utils/permissions.ts +83 -0
  61. package/src/utils/useTraceUpdate.tsx +23 -0
  62. package/tailwind.config.js +68 -0
@@ -0,0 +1,139 @@
1
+ import { useState } from "react";
2
+ import {
3
+ Button,
4
+ CenteredView,
5
+ Checkbox,
6
+ DeleteIcon,
7
+ IconButton,
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableHeader,
12
+ TableRow,
13
+ Tooltip,
14
+ Typography
15
+ } from "@firecms/ui";
16
+ import { DeleteConfirmationDialog } from "@firecms/core";
17
+ import { useUserManagement } from "../../hooks";
18
+ import { Role } from "../../types";
19
+ import { RoleChip } from "./RoleChip";
20
+ import { DEFAULT_ROLES } from "./default_roles";
21
+
22
+ export function RolesTable({
23
+ onRoleClicked,
24
+ editable
25
+ }: {
26
+ onRoleClicked: (role: Role) => void;
27
+ editable: boolean;
28
+ }) {
29
+
30
+ const {
31
+ roles,
32
+ saveRole,
33
+ deleteRole,
34
+ allowDefaultRolesCreation
35
+ } = useUserManagement();
36
+
37
+ const [roleToBeDeleted, setRoleToBeDeleted] = useState<Role | undefined>(undefined);
38
+ const [deleteInProgress, setDeleteInProgress] = useState<boolean>(false);
39
+
40
+ return <div
41
+ className="w-full overflow-auto">
42
+ <Table>
43
+ <TableHeader>
44
+ <TableCell header={true} className="w-16"></TableCell>
45
+ <TableCell header={true}>Role</TableCell>
46
+ <TableCell header={true} className={"items-center"}>Is Admin</TableCell>
47
+ <TableCell header={true}>Default permissions</TableCell>
48
+ </TableHeader>
49
+
50
+ <TableBody>
51
+ {roles && roles.map((role) => {
52
+ const canCreateAll = role.isAdmin || role.defaultPermissions?.create;
53
+ const canReadAll = role.isAdmin || role.defaultPermissions?.read;
54
+ const canUpdateAll = role.isAdmin || role.defaultPermissions?.edit;
55
+ const canDeleteAll = role.isAdmin || role.defaultPermissions?.delete;
56
+ return (
57
+ <TableRow
58
+ key={role.name}
59
+ onClick={() => {
60
+ onRoleClicked(role);
61
+ }}
62
+ >
63
+ <TableCell style={{ width: "64px" }}>
64
+ {!role.isAdmin &&
65
+ <Tooltip title={"Delete this role"}>
66
+ <IconButton
67
+ size={"small"}
68
+ disabled={!editable}
69
+ onClick={(event) => {
70
+ event.stopPropagation();
71
+ return setRoleToBeDeleted(role);
72
+ }}>
73
+ <DeleteIcon/>
74
+ </IconButton>
75
+ </Tooltip>}
76
+ </TableCell>
77
+ <TableCell>
78
+ <RoleChip role={role}/>
79
+ </TableCell>
80
+ <TableCell className={"items-center"}>
81
+ <Checkbox checked={role.isAdmin ?? false}/>
82
+ </TableCell>
83
+ <TableCell>
84
+ <ul>
85
+ {canCreateAll && <li>Create</li>}
86
+ {canReadAll && <li>Read</li>}
87
+ {canUpdateAll && <li>Update</li>}
88
+ {canDeleteAll && <li>Delete</li>}
89
+ </ul>
90
+ </TableCell>
91
+ </TableRow>
92
+ );
93
+ })}
94
+
95
+ {(!roles || roles.length === 0) && <TableRow>
96
+ <TableCell colspan={4}>
97
+ <CenteredView className={"flex flex-col gap-4 my-8 items-center"}>
98
+ <Typography variant={"label"}>
99
+ You don&apos;t have any roles yet.
100
+ </Typography>
101
+ {allowDefaultRolesCreation && <Button variant={"outlined"}
102
+ onClick={() => {
103
+ DEFAULT_ROLES.forEach((role) => {
104
+ saveRole(role);
105
+ });
106
+ }}>
107
+ Create default roles
108
+ </Button>}
109
+ </CenteredView>
110
+ </TableCell>
111
+ </TableRow>}
112
+
113
+ </TableBody>
114
+
115
+ </Table>
116
+
117
+ <DeleteConfirmationDialog
118
+ open={Boolean(roleToBeDeleted)}
119
+ loading={deleteInProgress}
120
+ onAccept={() => {
121
+ if (roleToBeDeleted) {
122
+ setDeleteInProgress(true);
123
+ deleteRole(roleToBeDeleted)
124
+ .then(() => {
125
+ setRoleToBeDeleted(undefined);
126
+ })
127
+ .finally(() => {
128
+ setDeleteInProgress(false);
129
+ })
130
+ }
131
+ }}
132
+ onCancel={() => {
133
+ setRoleToBeDeleted(undefined);
134
+ }}
135
+ title={<>Delete?</>}
136
+ body={<>Are you sure you want to delete this role?</>}/>
137
+
138
+ </div>;
139
+ }
@@ -0,0 +1,63 @@
1
+ import React, { useCallback, useState } from "react";
2
+
3
+ import { useNavigationController } from "@firecms/core";
4
+ import { AddIcon, Button, Container, Tooltip, Typography } from "@firecms/ui";
5
+ import { RolesTable } from "./RolesTable";
6
+ import { RolesDetailsForm } from "./RolesDetailsForm";
7
+ import { useUserManagement } from "../../hooks";
8
+ import { Role } from "../../types";
9
+
10
+ export const RolesView = React.memo(
11
+ function RolesView({ children }: { children?: React.ReactNode }) {
12
+
13
+ const { collections } = useNavigationController();
14
+ const [dialogOpen, setDialogOpen] = useState(false);
15
+ const [selectedRole, setSelectedRole] = useState<Role | undefined>();
16
+
17
+ const { canEditRoles } = useUserManagement();
18
+
19
+ const onRoleClicked = useCallback((user: Role) => {
20
+ setDialogOpen(true);
21
+ setSelectedRole(user);
22
+ }, []);
23
+
24
+ const handleClose = () => {
25
+ setSelectedRole(undefined);
26
+ setDialogOpen(false);
27
+ };
28
+
29
+ return (
30
+ <Container className="w-full flex flex-col py-4 gap-4" maxWidth={"6xl"}>
31
+
32
+ {children}
33
+
34
+ <div className="flex items-center mt-12">
35
+ <Typography gutterBottom variant="h4"
36
+ className="flex-grow"
37
+ component="h4">
38
+ Roles
39
+ </Typography>
40
+ <Tooltip title={!canEditRoles ? "Update plans to customise roles" : undefined}>
41
+ <Button
42
+ size={"large"}
43
+ disabled={!canEditRoles}
44
+ startIcon={<AddIcon/>}
45
+ onClick={() => setDialogOpen(true)}>
46
+ Add role
47
+ </Button>
48
+ </Tooltip>
49
+ </div>
50
+
51
+ <RolesTable onRoleClicked={onRoleClicked} editable={Boolean(canEditRoles)}/>
52
+
53
+ <RolesDetailsForm
54
+ key={selectedRole?.id ?? "new"}
55
+ open={dialogOpen}
56
+ role={selectedRole}
57
+ editable={canEditRoles}
58
+ collections={collections}
59
+ handleClose={handleClose}/>
60
+
61
+ </Container>
62
+ )
63
+ });
@@ -0,0 +1,36 @@
1
+ import { Role } from "../../types";
2
+
3
+ export const DEFAULT_ROLES: Role[] = [
4
+ {
5
+ id: "admin",
6
+ name: "Admin",
7
+ isAdmin: true
8
+ },
9
+ {
10
+ id: "editor",
11
+ name: "Editor",
12
+ isAdmin: false,
13
+ defaultPermissions: {
14
+ read: true,
15
+ create: true,
16
+ edit: true,
17
+ delete: true
18
+ },
19
+ config: {
20
+ createCollections: true,
21
+ editCollections: "own",
22
+ deleteCollections: "own"
23
+ }
24
+ },
25
+ {
26
+ id: "viewer",
27
+ name: "Viewer",
28
+ isAdmin: false,
29
+ defaultPermissions: {
30
+ read: true,
31
+ create: false,
32
+ edit: false,
33
+ delete: false
34
+ }
35
+ }
36
+ ];
@@ -0,0 +1,4 @@
1
+ export * from "./RoleChip";
2
+ export * from "./RolesDetailsForm";
3
+ export * from "./RolesTable";
4
+ export * from "./RolesView";
@@ -0,0 +1,230 @@
1
+ import React, { useCallback } from "react";
2
+ import * as Yup from "yup";
3
+ import {
4
+ Button,
5
+ Dialog,
6
+ DialogActions,
7
+ DialogContent,
8
+ DoneIcon,
9
+ LoadingButton,
10
+ MultiSelect,
11
+ MultiSelectItem,
12
+ TextField,
13
+ Typography,
14
+ } from "@firecms/ui";
15
+ import { FieldCaption, useSnackbarController } from "@firecms/core";
16
+ import { Formex, useCreateFormex } from "@firecms/formex";
17
+
18
+ import { Role, UserWithRoles } from "../../types";
19
+ import { areRolesEqual } from "../../utils";
20
+ import { useUserManagement } from "../../hooks";
21
+ import { RoleChip } from "../roles";
22
+
23
+ export const UserYupSchema = Yup.object().shape({
24
+ displayName: Yup.string().required("Required"),
25
+ email: Yup.string().email().required("Required"),
26
+ roles: Yup.array().min(1)
27
+ });
28
+
29
+ function canUserBeEdited(loggedUser: UserWithRoles, user: UserWithRoles, users: UserWithRoles[], roles: Role[], prevUser?: UserWithRoles) {
30
+ const admins = users.filter(u => u.roles.map(r => r.id).includes("admin"));
31
+ const loggedUserIsAdmin = loggedUser.roles.map(r => r.id).includes("admin");
32
+ const didRolesChange = !prevUser || !areRolesEqual(prevUser.roles, user.roles);
33
+
34
+ if (didRolesChange && !loggedUserIsAdmin) {
35
+ throw new Error("Only admins can change roles");
36
+ }
37
+
38
+ // was the admin role removed
39
+ const adminRoleRemoved = prevUser && prevUser.roles.map(r => r.id).includes("admin") && !user.roles.map(r => r.id).includes("admin");
40
+
41
+ // avoid removing the last admin
42
+ if (adminRoleRemoved && admins.length === 1) {
43
+ throw new Error("There must be at least one admin");
44
+ }
45
+ return true;
46
+ }
47
+
48
+ export function UserDetailsForm({
49
+ open,
50
+ user: userProp,
51
+ handleClose
52
+ }: {
53
+ open: boolean,
54
+ user?: UserWithRoles,
55
+ handleClose: () => void
56
+ }) {
57
+
58
+ const snackbarController = useSnackbarController();
59
+ const {
60
+ loggedInUser,
61
+ saveUser,
62
+ users,
63
+ roles,
64
+ } = useUserManagement();
65
+ const isNewUser = !userProp;
66
+
67
+ const onUserUpdated = useCallback((savedUser: UserWithRoles): Promise<UserWithRoles> => {
68
+ if (!loggedInUser) {
69
+ throw new Error("Logged user not found");
70
+ }
71
+ try {
72
+ canUserBeEdited(loggedInUser, savedUser, users, roles, userProp);
73
+ return saveUser(savedUser);
74
+ } catch (e: any) {
75
+ return Promise.reject(e);
76
+ }
77
+ }, [roles, saveUser, userProp, users, loggedInUser]);
78
+
79
+ const formex = useCreateFormex({
80
+ initialValues: userProp ?? {
81
+ displayName: "",
82
+ email: "",
83
+ roles: roles.filter(r => r.id === "editor")
84
+ } as UserWithRoles,
85
+ validation: (values) => {
86
+ return UserYupSchema.validate(values, { abortEarly: false })
87
+ .then(() => {
88
+ return {};
89
+ }).catch((e) => {
90
+ return e.inner.reduce((acc: any, error: any) => {
91
+ acc[error.path] = error.message;
92
+ return acc;
93
+ }, {});
94
+ });
95
+ },
96
+ onSubmit: (user: UserWithRoles, formexController) => {
97
+
98
+ return onUserUpdated(user)
99
+ .then(() => {
100
+ handleClose();
101
+ formexController.resetForm({
102
+ values: user
103
+ });
104
+ }).catch((e) => {
105
+ snackbarController.open({
106
+ type: "error",
107
+ message: e.message
108
+ });
109
+ });
110
+ }
111
+ });
112
+
113
+ const {
114
+ isSubmitting,
115
+ touched,
116
+ handleChange,
117
+ values,
118
+ errors,
119
+ setFieldValue,
120
+ dirty,
121
+ handleSubmit,
122
+ submitCount
123
+ } = formex;
124
+
125
+ return (
126
+ <Dialog
127
+ open={open}
128
+ onOpenChange={(open) => !open ? handleClose() : undefined}
129
+ maxWidth={"4xl"}
130
+ >
131
+ <Formex value={formex}>
132
+ <form
133
+ onSubmit={handleSubmit}
134
+ autoComplete={"off"}
135
+ noValidate
136
+ style={{
137
+ display: "flex",
138
+ flexDirection: "column",
139
+ position: "relative",
140
+ height: "100%"
141
+ }}>
142
+ <DialogContent className="h-full flex-grow">
143
+ <div
144
+ className="flex flex-row pt-4 pb-4">
145
+ <Typography variant={"h4"}
146
+ className="flex-grow">
147
+ User
148
+ </Typography>
149
+ </div>
150
+
151
+ <div className={"grid grid-cols-12 gap-8"}>
152
+
153
+ <div className={"col-span-12"}>
154
+ <TextField
155
+ name="displayName"
156
+ required
157
+ error={submitCount > 0 && Boolean(errors.displayName)}
158
+ value={values.displayName ?? ""}
159
+ onChange={handleChange}
160
+ aria-describedby="name-helper-text"
161
+ label="Name"
162
+ />
163
+ <FieldCaption>
164
+ {submitCount > 0 && Boolean(errors.displayName) ? errors.displayName : "Name of this user"}
165
+ </FieldCaption>
166
+ </div>
167
+ <div className={"col-span-12"}>
168
+ <TextField
169
+ required
170
+ error={submitCount > 0 && Boolean(errors.email)}
171
+ name="email"
172
+ value={values.email ?? ""}
173
+ onChange={handleChange}
174
+ aria-describedby="email-helper-text"
175
+ label="Email"
176
+ />
177
+ <FieldCaption>
178
+ {submitCount > 0 && Boolean(errors.email) ? errors.email : "Email of this user"}
179
+ </FieldCaption>
180
+ </div>
181
+ <div className={"col-span-12"}>
182
+ <MultiSelect
183
+ label="Roles"
184
+ value={values.roles.map(r => r.id) ?? []}
185
+ onMultiValueChange={(value: string[]) => setFieldValue("roles", value.map(id => roles.find(r => r.id === id) as Role))}
186
+ renderValue={(value: string) => {
187
+ const userRole = roles
188
+ .find((role) => role.id === value);
189
+ if (!userRole) return null;
190
+ return <div className="flex flex-wrap space-x-2 space-y-2">
191
+ <RoleChip key={userRole?.id} role={userRole}/>
192
+ </div>;
193
+ }}>
194
+ {roles.map(userRole => <MultiSelectItem key={userRole.id}
195
+ value={userRole.id}>
196
+ <RoleChip key={userRole?.id} role={userRole}/>
197
+ </MultiSelectItem>)}
198
+ </MultiSelect>
199
+ </div>
200
+
201
+ </div>
202
+
203
+ </DialogContent>
204
+
205
+ <DialogActions>
206
+
207
+ <Button variant={"text"}
208
+ onClick={() => {
209
+ handleClose();
210
+ }}>
211
+ Cancel
212
+ </Button>
213
+
214
+ <LoadingButton
215
+ variant="filled"
216
+ color="primary"
217
+ type="submit"
218
+ disabled={!dirty}
219
+ loading={isSubmitting}
220
+ startIcon={<DoneIcon/>}
221
+ >
222
+ {isNewUser ? "Create user" : "Update"}
223
+ </LoadingButton>
224
+ </DialogActions>
225
+ </form>
226
+ </Formex>
227
+
228
+ </Dialog>
229
+ );
230
+ }
@@ -0,0 +1,178 @@
1
+ import { useState } from "react";
2
+ import { User as FirebaseUser } from "firebase/auth";
3
+
4
+ import { format } from "date-fns";
5
+ import * as locales from "date-fns/locale";
6
+
7
+ import {
8
+ defaultDateFormat,
9
+ DeleteConfirmationDialog,
10
+ useAuthController,
11
+ useCustomizationController,
12
+ useSnackbarController
13
+ } from "@firecms/core";
14
+ import {
15
+ Button,
16
+ CenteredView,
17
+ DeleteIcon,
18
+ IconButton,
19
+ Table,
20
+ TableBody,
21
+ TableCell,
22
+ TableHeader,
23
+ TableRow,
24
+ Tooltip,
25
+ Typography,
26
+ } from "@firecms/ui";
27
+ import { Role, UserWithRoles } from "../../types";
28
+ import { useUserManagement } from "../../hooks/useUserManagement";
29
+ import { RoleChip } from "../roles/RoleChip";
30
+
31
+ export function UsersTable({ onUserClicked }: {
32
+ onUserClicked: (user: UserWithRoles) => void;
33
+ }) {
34
+
35
+ const {
36
+ users,
37
+ saveUser,
38
+ deleteUser
39
+ } = useUserManagement();
40
+
41
+ const authController = useAuthController<FirebaseUser>();
42
+ const snackbarController = useSnackbarController();
43
+
44
+ const customizationController = useCustomizationController();
45
+ const dateUtilsLocale = customizationController?.locale ? locales[customizationController?.locale as keyof typeof locales] : undefined;
46
+ const dateFormat: string = customizationController?.dateTimeFormat ?? defaultDateFormat;
47
+
48
+ const [userToBeDeleted, setUserToBeDeleted] = useState<UserWithRoles | undefined>(undefined);
49
+ const [deleteInProgress, setDeleteInProgress] = useState<boolean>(false);
50
+
51
+ return (
52
+ <div className="overflow-auto">
53
+
54
+ <Table>
55
+
56
+ <TableHeader>
57
+ <TableCell className="truncate w-16"></TableCell>
58
+ <TableCell>ID</TableCell>
59
+ <TableCell>Email</TableCell>
60
+ <TableCell>Name</TableCell>
61
+ <TableCell>Roles</TableCell>
62
+ <TableCell>Created on</TableCell>
63
+ </TableHeader>
64
+ <TableBody>
65
+ {users && users.map((user) => {
66
+
67
+ const userRoles: Role[] | undefined = user.roles;
68
+
69
+ const formattedDate = user.created_on ? format(user.created_on, dateFormat, { locale: dateUtilsLocale }) : "";
70
+
71
+ return (
72
+ <TableRow
73
+ key={"row_" + user.uid}
74
+ onClick={() => {
75
+ onUserClicked(user);
76
+ }}
77
+ >
78
+ <TableCell className={"w-10"}>
79
+ <Tooltip title={"Delete this user"}>
80
+ <IconButton
81
+ size={"small"}
82
+ onClick={(event) => {
83
+ event.stopPropagation();
84
+ return setUserToBeDeleted(user);
85
+ }}>
86
+ <DeleteIcon/>
87
+ </IconButton>
88
+ </Tooltip>
89
+ </TableCell>
90
+ <TableCell>{user.uid}</TableCell>
91
+ <TableCell>{user.email}</TableCell>
92
+ <TableCell className={"font-medium align-left"}>{user.displayName}</TableCell>
93
+ <TableCell className="align-left">
94
+ {userRoles
95
+ ? <div className="flex flex-wrap gap-2">
96
+ {userRoles.map(userRole =>
97
+ <RoleChip key={userRole?.id} role={userRole}/>
98
+ )}
99
+ </div>
100
+ : null}
101
+ </TableCell>
102
+ <TableCell>{formattedDate}</TableCell>
103
+ </TableRow>
104
+ );
105
+ })}
106
+
107
+ {(!users || users.length === 0) && <TableRow>
108
+ <TableCell colspan={6}>
109
+ <CenteredView className={"flex flex-col gap-4 my-8 items-center"}>
110
+ <Typography variant={"label"}>
111
+ There are no users yet
112
+ </Typography>
113
+ <Button variant={"outlined"}
114
+ onClick={() => {
115
+ if (!authController.user?.uid) {
116
+ throw Error("UsersTable, authController misconfiguration");
117
+ }
118
+ saveUser({
119
+ uid: authController.user?.uid,
120
+ email: authController.user?.email,
121
+ displayName: authController.user?.displayName,
122
+ photoURL: authController.user?.photoURL,
123
+ providerId: authController.user?.providerId,
124
+ isAnonymous: authController.user?.isAnonymous,
125
+ roles: [{ id: "admin", name: "Admin" }],
126
+ created_on: new Date()
127
+ })
128
+ .then(() => {
129
+ snackbarController.open({
130
+ type: "success",
131
+ message: "User added successfully"
132
+ })
133
+ })
134
+ .catch((error) => {
135
+ snackbarController.open({
136
+ type: "error",
137
+ message: "Error adding user: " + error.message,
138
+ })
139
+ });
140
+ }}>
141
+
142
+ Add the logged user as an admin
143
+ </Button>
144
+ </CenteredView>
145
+ </TableCell>
146
+ </TableRow>}
147
+
148
+ </TableBody>
149
+ </Table>
150
+
151
+ <DeleteConfirmationDialog
152
+ open={Boolean(userToBeDeleted)}
153
+ loading={deleteInProgress}
154
+ onAccept={() => {
155
+ if (userToBeDeleted) {
156
+ setDeleteInProgress(true);
157
+ deleteUser(userToBeDeleted)
158
+ .then(() => {
159
+ setUserToBeDeleted(undefined);
160
+ })
161
+ .catch((error) => {
162
+ snackbarController.open({
163
+ type: "error",
164
+ message: "Error deleting user: " + error.message,
165
+ })
166
+ })
167
+ .finally(() => {
168
+ setDeleteInProgress(false);
169
+ })
170
+ }
171
+ }}
172
+ onCancel={() => {
173
+ setUserToBeDeleted(undefined);
174
+ }}
175
+ title={<>Delete?</>}
176
+ body={<>Are you sure you want to delete this user?</>}/>
177
+ </div>);
178
+ }