@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.
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/dist/UserManagementProvider.d.ts +7 -0
- package/dist/admin_views.d.ts +2 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/roles/RoleChip.d.ts +5 -0
- package/dist/components/roles/RolesDetailsForm.d.ts +20 -0
- package/dist/components/roles/RolesTable.d.ts +5 -0
- package/dist/components/roles/RolesView.d.ts +4 -0
- package/dist/components/roles/default_roles.d.ts +2 -0
- package/dist/components/roles/index.d.ts +4 -0
- package/dist/components/users/UserDetailsForm.d.ts +20 -0
- package/dist/components/users/UsersTable.d.ts +4 -0
- package/dist/components/users/UsersView.d.ts +4 -0
- package/dist/components/users/index.d.ts +3 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useBuildFirestoreUserManagement.d.ts +44 -0
- package/dist/hooks/useUserManagement.d.ts +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.es.js +1263 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +2 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/types/firecms_user.d.ts +7 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/roles.d.ts +31 -0
- package/dist/types/user_management.d.ts +39 -0
- package/dist/useUserManagementPlugin.d.ts +5 -0
- package/dist/utils/colors.d.ts +2 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/local_storage.d.ts +3 -0
- package/dist/utils/permissions.d.ts +9 -0
- package/dist/utils/useTraceUpdate.d.ts +1 -0
- package/package.json +76 -0
- package/src/UserManagementProvider.tsx +19 -0
- package/src/admin_views.tsx +19 -0
- package/src/components/index.ts +2 -0
- package/src/components/roles/RoleChip.tsx +28 -0
- package/src/components/roles/RolesDetailsForm.tsx +402 -0
- package/src/components/roles/RolesTable.tsx +139 -0
- package/src/components/roles/RolesView.tsx +63 -0
- package/src/components/roles/default_roles.tsx +36 -0
- package/src/components/roles/index.ts +4 -0
- package/src/components/users/UserDetailsForm.tsx +230 -0
- package/src/components/users/UsersTable.tsx +178 -0
- package/src/components/users/UsersView.tsx +59 -0
- package/src/components/users/index.ts +3 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useBuildFirestoreUserManagement.tsx +241 -0
- package/src/hooks/useUserManagement.tsx +5 -0
- package/src/index.ts +7 -0
- package/src/types/firecms_user.ts +8 -0
- package/src/types/index.ts +3 -0
- package/src/types/roles.ts +41 -0
- package/src/types/user_management.tsx +50 -0
- package/src/useUserManagementPlugin.tsx +18 -0
- package/src/utils/colors.ts +52 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/local_storage.ts +53 -0
- package/src/utils/permissions.ts +83 -0
- package/src/utils/useTraceUpdate.tsx +23 -0
- 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'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,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
|
+
}
|