@authhero/react-admin 0.14.0 → 0.15.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/CHANGELOG.md +6 -0
- package/package.json +2 -1
- package/src/components/resource-servers/edit.tsx +131 -28
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@authhero/react-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"react-admin": "^5.8.0",
|
|
27
27
|
"react-admin-color-picker": "^1.0.3",
|
|
28
28
|
"react-dom": "^19.1.0",
|
|
29
|
+
"react-hook-form": "^7.69.0",
|
|
29
30
|
"react-router-dom": "^7.6.0",
|
|
30
31
|
"recharts": "^2.15.0"
|
|
31
32
|
},
|
|
@@ -2,8 +2,6 @@ import {
|
|
|
2
2
|
Edit,
|
|
3
3
|
TextInput,
|
|
4
4
|
BooleanInput,
|
|
5
|
-
ArrayInput,
|
|
6
|
-
SimpleFormIterator,
|
|
7
5
|
TextField,
|
|
8
6
|
TabbedForm,
|
|
9
7
|
required,
|
|
@@ -11,7 +9,11 @@ import {
|
|
|
11
9
|
FormDataConsumer,
|
|
12
10
|
useRecordContext,
|
|
13
11
|
} from "react-admin";
|
|
14
|
-
import { Stack, Alert } from "@mui/material";
|
|
12
|
+
import { Stack, Alert, Pagination, Box, Typography, Button, IconButton, TextField as MuiTextField } from "@mui/material";
|
|
13
|
+
import DeleteIcon from "@mui/icons-material/Delete";
|
|
14
|
+
import AddIcon from "@mui/icons-material/Add";
|
|
15
|
+
import { useState, useMemo, useCallback } from "react";
|
|
16
|
+
import { useFormContext, useWatch } from "react-hook-form";
|
|
15
17
|
|
|
16
18
|
function SystemEntityAlert() {
|
|
17
19
|
const record = useRecordContext();
|
|
@@ -26,6 +28,131 @@ function SystemEntityAlert() {
|
|
|
26
28
|
);
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
const SCOPES_PER_PAGE = 10;
|
|
32
|
+
|
|
33
|
+
interface Scope {
|
|
34
|
+
value: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function PaginatedScopesInput({ disabled }: { disabled?: boolean }) {
|
|
39
|
+
const { setValue, getValues } = useFormContext();
|
|
40
|
+
const scopes: Scope[] = useWatch({ name: "scopes" }) || [];
|
|
41
|
+
const [page, setPage] = useState(1);
|
|
42
|
+
|
|
43
|
+
const totalPages = Math.max(1, Math.ceil(scopes.length / SCOPES_PER_PAGE));
|
|
44
|
+
|
|
45
|
+
const paginatedScopes = useMemo(() => {
|
|
46
|
+
const start = (page - 1) * SCOPES_PER_PAGE;
|
|
47
|
+
const end = start + SCOPES_PER_PAGE;
|
|
48
|
+
return scopes.slice(start, end).map((scope, index) => ({
|
|
49
|
+
...scope,
|
|
50
|
+
actualIndex: start + index,
|
|
51
|
+
}));
|
|
52
|
+
}, [scopes, page]);
|
|
53
|
+
|
|
54
|
+
const handlePageChange = (_event: React.ChangeEvent<unknown>, value: number) => {
|
|
55
|
+
setPage(value);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleAdd = useCallback(() => {
|
|
59
|
+
const currentScopes = getValues("scopes") || [];
|
|
60
|
+
setValue("scopes", [...currentScopes, { value: "", description: "" }], { shouldDirty: true });
|
|
61
|
+
// Navigate to the last page where the new item will be
|
|
62
|
+
const newTotalPages = Math.ceil((currentScopes.length + 1) / SCOPES_PER_PAGE);
|
|
63
|
+
setPage(newTotalPages);
|
|
64
|
+
}, [getValues, setValue]);
|
|
65
|
+
|
|
66
|
+
const handleRemove = useCallback((actualIndex: number) => {
|
|
67
|
+
const currentScopes = getValues("scopes") || [];
|
|
68
|
+
const newScopes = currentScopes.filter((_: Scope, i: number) => i !== actualIndex);
|
|
69
|
+
setValue("scopes", newScopes, { shouldDirty: true });
|
|
70
|
+
// Adjust page if we removed the last item on current page
|
|
71
|
+
const newTotalPages = Math.ceil(newScopes.length / SCOPES_PER_PAGE);
|
|
72
|
+
if (page > newTotalPages && newTotalPages > 0) {
|
|
73
|
+
setPage(newTotalPages);
|
|
74
|
+
}
|
|
75
|
+
}, [getValues, setValue, page]);
|
|
76
|
+
|
|
77
|
+
const handleScopeChange = useCallback((actualIndex: number, field: "value" | "description", newValue: string) => {
|
|
78
|
+
const currentScopes = getValues("scopes") || [];
|
|
79
|
+
const newScopes = [...currentScopes];
|
|
80
|
+
newScopes[actualIndex] = { ...newScopes[actualIndex], [field]: newValue };
|
|
81
|
+
setValue("scopes", newScopes, { shouldDirty: true });
|
|
82
|
+
}, [getValues, setValue]);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Box>
|
|
86
|
+
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
|
|
87
|
+
<Typography variant="body2" color="text.secondary">
|
|
88
|
+
{scopes.length} scope{scopes.length !== 1 ? "s" : ""} total
|
|
89
|
+
</Typography>
|
|
90
|
+
{!disabled && (
|
|
91
|
+
<Button
|
|
92
|
+
startIcon={<AddIcon />}
|
|
93
|
+
onClick={handleAdd}
|
|
94
|
+
size="small"
|
|
95
|
+
variant="outlined"
|
|
96
|
+
>
|
|
97
|
+
Add Scope
|
|
98
|
+
</Button>
|
|
99
|
+
)}
|
|
100
|
+
</Box>
|
|
101
|
+
|
|
102
|
+
{paginatedScopes.map((scope) => (
|
|
103
|
+
<Stack
|
|
104
|
+
key={scope.actualIndex}
|
|
105
|
+
spacing={2}
|
|
106
|
+
direction="row"
|
|
107
|
+
sx={{ width: "100%", alignItems: "flex-start", mb: 2 }}
|
|
108
|
+
>
|
|
109
|
+
<MuiTextField
|
|
110
|
+
value={scope.value || ""}
|
|
111
|
+
onChange={(e) => handleScopeChange(scope.actualIndex, "value", e.target.value)}
|
|
112
|
+
label="Scope Name"
|
|
113
|
+
helperText="e.g., read:users, write:posts"
|
|
114
|
+
sx={{ flex: 1 }}
|
|
115
|
+
disabled={disabled}
|
|
116
|
+
size="small"
|
|
117
|
+
required
|
|
118
|
+
/>
|
|
119
|
+
<MuiTextField
|
|
120
|
+
value={scope.description || ""}
|
|
121
|
+
onChange={(e) => handleScopeChange(scope.actualIndex, "description", e.target.value)}
|
|
122
|
+
label="Description"
|
|
123
|
+
helperText="What this scope allows"
|
|
124
|
+
sx={{ flex: 2 }}
|
|
125
|
+
disabled={disabled}
|
|
126
|
+
size="small"
|
|
127
|
+
/>
|
|
128
|
+
{!disabled && (
|
|
129
|
+
<IconButton
|
|
130
|
+
onClick={() => handleRemove(scope.actualIndex)}
|
|
131
|
+
color="error"
|
|
132
|
+
sx={{ mt: 1 }}
|
|
133
|
+
>
|
|
134
|
+
<DeleteIcon />
|
|
135
|
+
</IconButton>
|
|
136
|
+
)}
|
|
137
|
+
</Stack>
|
|
138
|
+
))}
|
|
139
|
+
|
|
140
|
+
{totalPages > 1 && (
|
|
141
|
+
<Box sx={{ display: "flex", justifyContent: "center", mt: 2 }}>
|
|
142
|
+
<Pagination
|
|
143
|
+
count={totalPages}
|
|
144
|
+
page={page}
|
|
145
|
+
onChange={handlePageChange}
|
|
146
|
+
color="primary"
|
|
147
|
+
showFirstButton
|
|
148
|
+
showLastButton
|
|
149
|
+
/>
|
|
150
|
+
</Box>
|
|
151
|
+
)}
|
|
152
|
+
</Box>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
29
156
|
function ResourceServerForm() {
|
|
30
157
|
const record = useRecordContext();
|
|
31
158
|
const isSystem = record?.is_system;
|
|
@@ -115,31 +242,7 @@ function ResourceServerForm() {
|
|
|
115
242
|
</TabbedForm.Tab>
|
|
116
243
|
|
|
117
244
|
<TabbedForm.Tab label="Scopes">
|
|
118
|
-
<
|
|
119
|
-
<SimpleFormIterator disableAdd={isSystem} disableRemove={isSystem} disableReordering={isSystem}>
|
|
120
|
-
<Stack
|
|
121
|
-
spacing={2}
|
|
122
|
-
direction="row"
|
|
123
|
-
sx={{ width: "100%", alignItems: "flex-start" }}
|
|
124
|
-
>
|
|
125
|
-
<TextInput
|
|
126
|
-
source="value"
|
|
127
|
-
validate={[required()]}
|
|
128
|
-
label="Scope Name"
|
|
129
|
-
helperText="e.g., read:users, write:posts"
|
|
130
|
-
sx={{ flex: 1 }}
|
|
131
|
-
disabled={isSystem}
|
|
132
|
-
/>
|
|
133
|
-
<TextInput
|
|
134
|
-
source="description"
|
|
135
|
-
label="Description"
|
|
136
|
-
helperText="What this scope allows"
|
|
137
|
-
sx={{ flex: 2 }}
|
|
138
|
-
disabled={isSystem}
|
|
139
|
-
/>
|
|
140
|
-
</Stack>
|
|
141
|
-
</SimpleFormIterator>
|
|
142
|
-
</ArrayInput>
|
|
245
|
+
<PaginatedScopesInput disabled={isSystem} />
|
|
143
246
|
</TabbedForm.Tab>
|
|
144
247
|
</TabbedForm>
|
|
145
248
|
);
|