@authhero/react-admin 0.10.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/.eslintrc.js +21 -0
- package/.vercelignore +4 -0
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/index.html +125 -0
- package/package.json +61 -0
- package/prettier.config.js +1 -0
- package/public/favicon.ico +0 -0
- package/public/manifest.json +15 -0
- package/src/App.spec.tsx +42 -0
- package/src/App.tsx +232 -0
- package/src/AuthCallback.tsx +138 -0
- package/src/Layout.tsx +12 -0
- package/src/TenantsApp.tsx +115 -0
- package/src/auth0DataProvider.ts +1242 -0
- package/src/authProvider.ts +521 -0
- package/src/components/CertificateErrorDialog.tsx +116 -0
- package/src/components/DomainSelector.tsx +401 -0
- package/src/components/TenantAppBar.tsx +83 -0
- package/src/components/TenantLayout.tsx +25 -0
- package/src/components/TenantsAppBar.tsx +21 -0
- package/src/components/TenantsLayout.tsx +28 -0
- package/src/components/activity/ActivityDashboard.tsx +381 -0
- package/src/components/activity/index.ts +1 -0
- package/src/components/branding/BrandingList.tsx +0 -0
- package/src/components/branding/BrandingShow.tsx +0 -0
- package/src/components/branding/ThemesTab.tsx +286 -0
- package/src/components/branding/edit.tsx +149 -0
- package/src/components/branding/hooks/useThemesData.ts +123 -0
- package/src/components/branding/index.ts +2 -0
- package/src/components/branding/list.tsx +12 -0
- package/src/components/clients/create.tsx +12 -0
- package/src/components/clients/edit.tsx +1285 -0
- package/src/components/clients/index.ts +3 -0
- package/src/components/clients/list.tsx +37 -0
- package/src/components/common/DateAgo.tsx +6 -0
- package/src/components/common/JsonOutput.tsx +26 -0
- package/src/components/common/index.ts +1 -0
- package/src/components/connections/create.tsx +35 -0
- package/src/components/connections/edit.tsx +212 -0
- package/src/components/connections/index.ts +3 -0
- package/src/components/connections/list.tsx +15 -0
- package/src/components/custom-domains/create.tsx +26 -0
- package/src/components/custom-domains/edit.tsx +101 -0
- package/src/components/custom-domains/index.ts +3 -0
- package/src/components/custom-domains/list.tsx +16 -0
- package/src/components/flows/create.tsx +30 -0
- package/src/components/flows/edit.tsx +238 -0
- package/src/components/flows/index.ts +3 -0
- package/src/components/flows/list.tsx +15 -0
- package/src/components/forms/FlowEditor.tsx +1363 -0
- package/src/components/forms/NodeEditor.tsx +1119 -0
- package/src/components/forms/RichTextEditor.tsx +145 -0
- package/src/components/forms/create.tsx +30 -0
- package/src/components/forms/edit.tsx +256 -0
- package/src/components/forms/index.ts +3 -0
- package/src/components/forms/list.tsx +16 -0
- package/src/components/hooks/create.tsx +96 -0
- package/src/components/hooks/edit.tsx +114 -0
- package/src/components/hooks/index.ts +3 -0
- package/src/components/hooks/list.tsx +17 -0
- package/src/components/listActions/PostListActions.tsx +10 -0
- package/src/components/logs/LogIcon.tsx +32 -0
- package/src/components/logs/LogShow.tsx +82 -0
- package/src/components/logs/LogType.tsx +38 -0
- package/src/components/logs/index.ts +4 -0
- package/src/components/logs/list.tsx +41 -0
- package/src/components/organizations/create.tsx +13 -0
- package/src/components/organizations/edit.tsx +682 -0
- package/src/components/organizations/index.ts +3 -0
- package/src/components/organizations/list.tsx +21 -0
- package/src/components/resource-servers/create.tsx +87 -0
- package/src/components/resource-servers/edit.tsx +121 -0
- package/src/components/resource-servers/index.ts +3 -0
- package/src/components/resource-servers/list.tsx +47 -0
- package/src/components/roles/create.tsx +12 -0
- package/src/components/roles/edit.tsx +426 -0
- package/src/components/roles/index.ts +3 -0
- package/src/components/roles/list.tsx +24 -0
- package/src/components/sessions/edit.tsx +101 -0
- package/src/components/sessions/index.ts +3 -0
- package/src/components/sessions/list.tsx +20 -0
- package/src/components/sessions/show.tsx +113 -0
- package/src/components/settings/edit.tsx +236 -0
- package/src/components/settings/index.ts +2 -0
- package/src/components/settings/list.tsx +14 -0
- package/src/components/tenants/create.tsx +20 -0
- package/src/components/tenants/edit.tsx +54 -0
- package/src/components/tenants/index.ts +2 -0
- package/src/components/tenants/list.tsx +67 -0
- package/src/components/themes/edit.tsx +200 -0
- package/src/components/themes/index.ts +2 -0
- package/src/components/themes/list.tsx +12 -0
- package/src/components/users/create.tsx +144 -0
- package/src/components/users/edit.tsx +1711 -0
- package/src/components/users/index.ts +3 -0
- package/src/components/users/list.tsx +35 -0
- package/src/data.json +121 -0
- package/src/dataProvider.ts +97 -0
- package/src/index.tsx +106 -0
- package/src/lib/logs.ts +21 -0
- package/src/types/reactflow.d.ts +86 -0
- package/src/utils/domainUtils.ts +169 -0
- package/src/utils/tokenUtils.ts +75 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +37 -0
- package/tsconfig.node.json +10 -0
- package/vercel.json +17 -0
- package/vite.config.ts +30 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Button,
|
|
5
|
+
TextField,
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogTitle,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogActions,
|
|
10
|
+
List,
|
|
11
|
+
ListItem,
|
|
12
|
+
ListItemText,
|
|
13
|
+
IconButton,
|
|
14
|
+
CircularProgress,
|
|
15
|
+
Typography,
|
|
16
|
+
FormControl,
|
|
17
|
+
InputLabel,
|
|
18
|
+
Select,
|
|
19
|
+
MenuItem,
|
|
20
|
+
FormHelperText,
|
|
21
|
+
} from "@mui/material";
|
|
22
|
+
import DeleteIcon from "@mui/icons-material/Delete";
|
|
23
|
+
import AddIcon from "@mui/icons-material/Add";
|
|
24
|
+
import {
|
|
25
|
+
ConnectionMethod,
|
|
26
|
+
DomainConfig,
|
|
27
|
+
getDomainFromStorage,
|
|
28
|
+
saveDomainToStorage,
|
|
29
|
+
saveSelectedDomainToStorage,
|
|
30
|
+
formatDomain,
|
|
31
|
+
} from "../utils/domainUtils";
|
|
32
|
+
|
|
33
|
+
interface DomainSelectorProps {
|
|
34
|
+
onDomainSelected: (domain: string) => void;
|
|
35
|
+
disableCloseOnRootPath?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function DomainSelector({
|
|
39
|
+
onDomainSelected,
|
|
40
|
+
disableCloseOnRootPath = false,
|
|
41
|
+
}: DomainSelectorProps) {
|
|
42
|
+
const [domains, setDomains] = useState<DomainConfig[]>([]);
|
|
43
|
+
const [selectedDomain, setSelectedDomain] = useState<string>("");
|
|
44
|
+
const [inputDomain, setInputDomain] = useState<string>("");
|
|
45
|
+
const [connectionMethod, setConnectionMethod] =
|
|
46
|
+
useState<ConnectionMethod>("login");
|
|
47
|
+
|
|
48
|
+
// Login method fields
|
|
49
|
+
const [inputClientId, setInputClientId] = useState<string>("");
|
|
50
|
+
const [inputRestApiUrl, setInputRestApiUrl] = useState<string>("");
|
|
51
|
+
|
|
52
|
+
// Token method field
|
|
53
|
+
const [inputToken, setInputToken] = useState<string>("");
|
|
54
|
+
|
|
55
|
+
// Client credentials fields
|
|
56
|
+
const [inputClientSecret, setInputClientSecret] = useState<string>("");
|
|
57
|
+
|
|
58
|
+
const [showDomainDialog, setShowDomainDialog] = useState<boolean>(true);
|
|
59
|
+
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
60
|
+
|
|
61
|
+
// Load domains from cookies on component mount
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const savedDomains = getDomainFromStorage();
|
|
64
|
+
setDomains(savedDomains);
|
|
65
|
+
setIsLoading(false);
|
|
66
|
+
|
|
67
|
+
// If domains exist, show the dialog but don't auto-select
|
|
68
|
+
if (savedDomains.length === 0) {
|
|
69
|
+
setShowDomainDialog(true);
|
|
70
|
+
}
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
// Helper function to navigate after domain selection
|
|
74
|
+
const selectDomainAndNavigate = (domain: string) => {
|
|
75
|
+
// Save the selected domain to cookies and notify parent
|
|
76
|
+
saveSelectedDomainToStorage(domain);
|
|
77
|
+
onDomainSelected(domain);
|
|
78
|
+
|
|
79
|
+
// Close dialog
|
|
80
|
+
setShowDomainDialog(false);
|
|
81
|
+
|
|
82
|
+
// Get the current path to preserve tenant segment if it exists
|
|
83
|
+
const currentPath = window.location.pathname;
|
|
84
|
+
const pathSegments = currentPath.split("/").filter(Boolean);
|
|
85
|
+
|
|
86
|
+
// Check if the first segment is a tenant ID (not "tenants")
|
|
87
|
+
if (pathSegments.length > 0 && pathSegments[0] !== "tenants") {
|
|
88
|
+
const tenantId = pathSegments[0];
|
|
89
|
+
// Preserve the tenant ID in the URL
|
|
90
|
+
window.location.href = `/${tenantId}`;
|
|
91
|
+
} else {
|
|
92
|
+
// Otherwise navigate to the tenants page to trigger auth flow
|
|
93
|
+
window.location.href = "/tenants";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleAddDomain = () => {
|
|
98
|
+
if (inputDomain.trim() === "") return;
|
|
99
|
+
|
|
100
|
+
// Format the domain to ensure consistency (remove http/https)
|
|
101
|
+
const formattedDomain = formatDomain(inputDomain);
|
|
102
|
+
|
|
103
|
+
let newDomainConfig: DomainConfig;
|
|
104
|
+
|
|
105
|
+
switch (connectionMethod) {
|
|
106
|
+
case "login":
|
|
107
|
+
newDomainConfig = {
|
|
108
|
+
url: formattedDomain, // Use formatted domain
|
|
109
|
+
connectionMethod: "login",
|
|
110
|
+
clientId: inputClientId,
|
|
111
|
+
restApiUrl: inputRestApiUrl.trim() || undefined,
|
|
112
|
+
};
|
|
113
|
+
break;
|
|
114
|
+
case "token":
|
|
115
|
+
newDomainConfig = {
|
|
116
|
+
url: formattedDomain, // Use formatted domain
|
|
117
|
+
connectionMethod: "token",
|
|
118
|
+
token: inputToken,
|
|
119
|
+
};
|
|
120
|
+
break;
|
|
121
|
+
case "client_credentials":
|
|
122
|
+
newDomainConfig = {
|
|
123
|
+
url: formattedDomain, // Use formatted domain
|
|
124
|
+
connectionMethod: "client_credentials",
|
|
125
|
+
clientId: inputClientId,
|
|
126
|
+
clientSecret: inputClientSecret,
|
|
127
|
+
};
|
|
128
|
+
break;
|
|
129
|
+
default:
|
|
130
|
+
return; // Invalid connection method
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if domain with the same formatted URL already exists
|
|
134
|
+
const domainExists = domains.some((d) => d.url === formattedDomain);
|
|
135
|
+
let newDomains;
|
|
136
|
+
|
|
137
|
+
if (domainExists) {
|
|
138
|
+
// Update existing domain
|
|
139
|
+
newDomains = domains.map((d) =>
|
|
140
|
+
d.url === formattedDomain ? newDomainConfig : d,
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
// Add new domain
|
|
144
|
+
newDomains = [...domains, newDomainConfig];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Save the domains to storage and update state
|
|
148
|
+
saveDomainToStorage(newDomains);
|
|
149
|
+
setDomains(newDomains);
|
|
150
|
+
|
|
151
|
+
// Don't automatically navigate, just highlight the new domain
|
|
152
|
+
setSelectedDomain(formattedDomain);
|
|
153
|
+
|
|
154
|
+
// Reset all input fields
|
|
155
|
+
setInputDomain("");
|
|
156
|
+
setInputClientId("");
|
|
157
|
+
setInputRestApiUrl("");
|
|
158
|
+
setInputToken("");
|
|
159
|
+
setInputClientSecret("");
|
|
160
|
+
|
|
161
|
+
// Toast or feedback message could be added here
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleRemoveDomain = (domainToRemove: string) => {
|
|
165
|
+
const newDomains = domains.filter(
|
|
166
|
+
(domain) => domain.url !== domainToRemove,
|
|
167
|
+
);
|
|
168
|
+
setDomains(newDomains);
|
|
169
|
+
saveDomainToStorage(newDomains);
|
|
170
|
+
|
|
171
|
+
if (selectedDomain === domainToRemove) {
|
|
172
|
+
setSelectedDomain("");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (newDomains.length === 0) {
|
|
176
|
+
setShowDomainDialog(true);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const handleSelectDomain = (domain: string) => {
|
|
181
|
+
const formattedDomain = formatDomain(domain);
|
|
182
|
+
setSelectedDomain(formattedDomain);
|
|
183
|
+
|
|
184
|
+
// Use the helper function to select domain and navigate
|
|
185
|
+
selectDomainAndNavigate(formattedDomain);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (isLoading) {
|
|
189
|
+
return (
|
|
190
|
+
<Box
|
|
191
|
+
sx={{
|
|
192
|
+
display: "flex",
|
|
193
|
+
justifyContent: "center",
|
|
194
|
+
alignItems: "center",
|
|
195
|
+
height: "100vh",
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
<CircularProgress />
|
|
199
|
+
</Box>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<Dialog
|
|
205
|
+
open={showDomainDialog}
|
|
206
|
+
onClose={() => {
|
|
207
|
+
// If we're on root path and disableCloseOnRootPath is true, don't allow closing
|
|
208
|
+
if (disableCloseOnRootPath) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Otherwise follow the existing logic
|
|
212
|
+
if (domains.length > 0) {
|
|
213
|
+
setShowDomainDialog(false);
|
|
214
|
+
}
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
<DialogTitle>Select Auth Domain</DialogTitle>
|
|
218
|
+
<DialogContent>
|
|
219
|
+
<Box sx={{ minWidth: 400, my: 2 }}>
|
|
220
|
+
<Typography variant="body1" sx={{ mb: 2 }}>
|
|
221
|
+
Please select or add an authentication domain to connect to.
|
|
222
|
+
</Typography>
|
|
223
|
+
|
|
224
|
+
{domains.length > 0 && (
|
|
225
|
+
<List>
|
|
226
|
+
{domains.map((domain) => (
|
|
227
|
+
<ListItem
|
|
228
|
+
key={domain.url}
|
|
229
|
+
disablePadding
|
|
230
|
+
secondaryAction={
|
|
231
|
+
<IconButton
|
|
232
|
+
edge="end"
|
|
233
|
+
onClick={(e) => {
|
|
234
|
+
e.stopPropagation();
|
|
235
|
+
handleRemoveDomain(domain.url);
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
<DeleteIcon />
|
|
239
|
+
</IconButton>
|
|
240
|
+
}
|
|
241
|
+
>
|
|
242
|
+
<Box
|
|
243
|
+
onClick={() => handleSelectDomain(domain.url)}
|
|
244
|
+
sx={{
|
|
245
|
+
textAlign: "left",
|
|
246
|
+
justifyContent: "flex-start",
|
|
247
|
+
width: "100%",
|
|
248
|
+
cursor: "pointer",
|
|
249
|
+
textTransform: "none",
|
|
250
|
+
padding: "8px 16px",
|
|
251
|
+
backgroundColor:
|
|
252
|
+
domain.url === selectedDomain
|
|
253
|
+
? "rgba(0, 0, 0, 0.04)"
|
|
254
|
+
: "transparent",
|
|
255
|
+
"&:hover": {
|
|
256
|
+
backgroundColor: "rgba(0, 0, 0, 0.08)",
|
|
257
|
+
},
|
|
258
|
+
}}
|
|
259
|
+
>
|
|
260
|
+
<ListItemText
|
|
261
|
+
primary={domain.url}
|
|
262
|
+
secondary={
|
|
263
|
+
<>
|
|
264
|
+
<Typography
|
|
265
|
+
component="span"
|
|
266
|
+
variant="body2"
|
|
267
|
+
color="text.primary"
|
|
268
|
+
>
|
|
269
|
+
{domain.connectionMethod === "login"
|
|
270
|
+
? "Login"
|
|
271
|
+
: domain.connectionMethod === "token"
|
|
272
|
+
? "API Token"
|
|
273
|
+
: "Client Credentials"}
|
|
274
|
+
</Typography>
|
|
275
|
+
{domain.connectionMethod === "login" &&
|
|
276
|
+
domain.clientId && (
|
|
277
|
+
<> · Client ID: {domain.clientId}</>
|
|
278
|
+
)}
|
|
279
|
+
{domain.connectionMethod === "client_credentials" &&
|
|
280
|
+
domain.clientId && (
|
|
281
|
+
<> · Client ID: {domain.clientId}</>
|
|
282
|
+
)}
|
|
283
|
+
</>
|
|
284
|
+
}
|
|
285
|
+
/>
|
|
286
|
+
</Box>
|
|
287
|
+
</ListItem>
|
|
288
|
+
))}
|
|
289
|
+
</List>
|
|
290
|
+
)}
|
|
291
|
+
|
|
292
|
+
<Box sx={{ mt: 2, display: "flex", flexDirection: "column", gap: 2 }}>
|
|
293
|
+
<TextField
|
|
294
|
+
fullWidth
|
|
295
|
+
label="Auth Domain"
|
|
296
|
+
variant="outlined"
|
|
297
|
+
value={inputDomain}
|
|
298
|
+
onChange={(e) => setInputDomain(e.target.value)}
|
|
299
|
+
placeholder="e.g., auth2.sesamy.dev"
|
|
300
|
+
/>
|
|
301
|
+
|
|
302
|
+
<FormControl fullWidth>
|
|
303
|
+
<InputLabel id="connection-method-label">
|
|
304
|
+
Connection Method
|
|
305
|
+
</InputLabel>
|
|
306
|
+
<Select
|
|
307
|
+
labelId="connection-method-label"
|
|
308
|
+
id="connection-method"
|
|
309
|
+
value={connectionMethod}
|
|
310
|
+
label="Connection Method"
|
|
311
|
+
onChange={(e) =>
|
|
312
|
+
setConnectionMethod(e.target.value as ConnectionMethod)
|
|
313
|
+
}
|
|
314
|
+
>
|
|
315
|
+
<MenuItem value="login">Login (Authentication Flow)</MenuItem>
|
|
316
|
+
<MenuItem value="token">API Token</MenuItem>
|
|
317
|
+
<MenuItem value="client_credentials">
|
|
318
|
+
Client Credentials
|
|
319
|
+
</MenuItem>
|
|
320
|
+
</Select>
|
|
321
|
+
<FormHelperText>
|
|
322
|
+
Select how you want to connect to the Auth domain
|
|
323
|
+
</FormHelperText>
|
|
324
|
+
</FormControl>
|
|
325
|
+
|
|
326
|
+
{/* Conditional fields based on connection method */}
|
|
327
|
+
{connectionMethod === "login" && (
|
|
328
|
+
<>
|
|
329
|
+
<TextField
|
|
330
|
+
fullWidth
|
|
331
|
+
label="Client ID"
|
|
332
|
+
variant="outlined"
|
|
333
|
+
value={inputClientId}
|
|
334
|
+
onChange={(e) => setInputClientId(e.target.value)}
|
|
335
|
+
placeholder="e.g., your-client-id"
|
|
336
|
+
/>
|
|
337
|
+
<TextField
|
|
338
|
+
fullWidth
|
|
339
|
+
label="REST API URL"
|
|
340
|
+
variant="outlined"
|
|
341
|
+
value={inputRestApiUrl}
|
|
342
|
+
onChange={(e) => setInputRestApiUrl(e.target.value)}
|
|
343
|
+
placeholder="e.g., https://api.example.com"
|
|
344
|
+
/>
|
|
345
|
+
</>
|
|
346
|
+
)}
|
|
347
|
+
|
|
348
|
+
{connectionMethod === "token" && (
|
|
349
|
+
<TextField
|
|
350
|
+
fullWidth
|
|
351
|
+
label="API Token"
|
|
352
|
+
variant="outlined"
|
|
353
|
+
value={inputToken}
|
|
354
|
+
onChange={(e) => setInputToken(e.target.value)}
|
|
355
|
+
placeholder="Bearer eyJhbGciOiJIUzI1..."
|
|
356
|
+
multiline
|
|
357
|
+
rows={3}
|
|
358
|
+
/>
|
|
359
|
+
)}
|
|
360
|
+
|
|
361
|
+
{connectionMethod === "client_credentials" && (
|
|
362
|
+
<>
|
|
363
|
+
<TextField
|
|
364
|
+
fullWidth
|
|
365
|
+
label="Client ID"
|
|
366
|
+
variant="outlined"
|
|
367
|
+
value={inputClientId}
|
|
368
|
+
onChange={(e) => setInputClientId(e.target.value)}
|
|
369
|
+
placeholder="e.g., your-client-id"
|
|
370
|
+
/>
|
|
371
|
+
<TextField
|
|
372
|
+
fullWidth
|
|
373
|
+
label="Client Secret"
|
|
374
|
+
variant="outlined"
|
|
375
|
+
type="password"
|
|
376
|
+
value={inputClientSecret}
|
|
377
|
+
onChange={(e) => setInputClientSecret(e.target.value)}
|
|
378
|
+
placeholder="e.g., your-client-secret"
|
|
379
|
+
/>
|
|
380
|
+
</>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
<Button
|
|
384
|
+
variant="contained"
|
|
385
|
+
color="primary"
|
|
386
|
+
onClick={handleAddDomain}
|
|
387
|
+
startIcon={<AddIcon />}
|
|
388
|
+
>
|
|
389
|
+
Add
|
|
390
|
+
</Button>
|
|
391
|
+
</Box>
|
|
392
|
+
</Box>
|
|
393
|
+
</DialogContent>
|
|
394
|
+
{domains.length > 0 && !disableCloseOnRootPath && (
|
|
395
|
+
<DialogActions>
|
|
396
|
+
<Button onClick={() => setShowDomainDialog(false)}>Cancel</Button>
|
|
397
|
+
</DialogActions>
|
|
398
|
+
)}
|
|
399
|
+
</Dialog>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { AppBar, TitlePortal, useDataProvider } from "react-admin";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Link, Box } from "@mui/material";
|
|
4
|
+
|
|
5
|
+
type TenantResponse = {
|
|
6
|
+
audience: string;
|
|
7
|
+
created_at: string;
|
|
8
|
+
id: string;
|
|
9
|
+
language: string;
|
|
10
|
+
logo: string;
|
|
11
|
+
updated_at: string;
|
|
12
|
+
name: string;
|
|
13
|
+
primary_color: string;
|
|
14
|
+
secondary_color: string;
|
|
15
|
+
sender_email: string;
|
|
16
|
+
sender_name: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface TenantAppBarProps {
|
|
20
|
+
domainSelectorButton?: React.ReactNode;
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TenantAppBar(props: TenantAppBarProps) {
|
|
25
|
+
const { domainSelectorButton, ...rest } = props;
|
|
26
|
+
const pathSegments = location.pathname.split("/").filter(Boolean);
|
|
27
|
+
const tenantId = pathSegments[0];
|
|
28
|
+
const [tenant, setTenant] = useState<TenantResponse>();
|
|
29
|
+
const dataProvider = useDataProvider();
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
// Use the dataProvider to fetch tenants list and find the matching one
|
|
33
|
+
// This ensures we use the correct API URL configured in the app
|
|
34
|
+
dataProvider
|
|
35
|
+
.getList("tenants", {
|
|
36
|
+
pagination: { page: 1, perPage: 100 },
|
|
37
|
+
sort: { field: "id", order: "ASC" },
|
|
38
|
+
filter: {},
|
|
39
|
+
})
|
|
40
|
+
.then((result) => {
|
|
41
|
+
const foundTenant = result.data.find(
|
|
42
|
+
(t: any) => t.id === tenantId || t.tenant_id === tenantId,
|
|
43
|
+
);
|
|
44
|
+
if (foundTenant) {
|
|
45
|
+
setTenant(foundTenant as TenantResponse);
|
|
46
|
+
} else {
|
|
47
|
+
// Set a minimal tenant object if not found
|
|
48
|
+
setTenant({
|
|
49
|
+
id: tenantId,
|
|
50
|
+
name: tenantId,
|
|
51
|
+
} as TenantResponse);
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
.catch((error) => {
|
|
55
|
+
console.error("Failed to fetch tenant:", error);
|
|
56
|
+
// Set a minimal tenant object on error
|
|
57
|
+
setTenant({
|
|
58
|
+
id: tenantId,
|
|
59
|
+
name: tenantId,
|
|
60
|
+
} as TenantResponse);
|
|
61
|
+
});
|
|
62
|
+
}, [tenantId, dataProvider]);
|
|
63
|
+
|
|
64
|
+
const isDefaultSettings = tenantId === "DEFAULT_SETTINGS";
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<AppBar
|
|
68
|
+
{...rest}
|
|
69
|
+
sx={{
|
|
70
|
+
...rest.sx,
|
|
71
|
+
...(isDefaultSettings && { backgroundColor: "red" }),
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<TitlePortal />
|
|
75
|
+
<Box sx={{ display: "flex", alignItems: "center" }}>
|
|
76
|
+
<Link color="inherit" href="/tenants" underline="none" sx={{ mr: 2 }}>
|
|
77
|
+
{tenant?.name || tenantId || "Unknown"} - Tenants
|
|
78
|
+
</Link>
|
|
79
|
+
{domainSelectorButton}
|
|
80
|
+
</Box>
|
|
81
|
+
</AppBar>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Layout, LayoutProps } from "react-admin";
|
|
2
|
+
import { TenantAppBar } from "./TenantAppBar";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
interface TenantLayoutProps extends LayoutProps {
|
|
6
|
+
appBarProps?: {
|
|
7
|
+
domainSelectorButton?: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function tenantLayout(props: TenantLayoutProps) {
|
|
12
|
+
const { appBarProps, children, ...rest } = props;
|
|
13
|
+
const tenantId = location.pathname.split("/").filter(Boolean)[0];
|
|
14
|
+
const isDefaultSettings = tenantId === "DEFAULT_SETTINGS";
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Layout
|
|
18
|
+
{...rest}
|
|
19
|
+
appBar={TenantAppBar}
|
|
20
|
+
sx={{ ...(isDefaultSettings && { backgroundColor: "red" }) }}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</Layout>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Create a custom AppBar specifically for TenantsApp
|
|
2
|
+
import { AppBar as ReactAdminAppBar, TitlePortal } from "react-admin";
|
|
3
|
+
import { Box } from "@mui/material";
|
|
4
|
+
|
|
5
|
+
interface TenantsAppBarProps {
|
|
6
|
+
domainSelectorButton?: React.ReactNode;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function TenantsAppBar(props: TenantsAppBarProps) {
|
|
11
|
+
const { domainSelectorButton, ...rest } = props;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<ReactAdminAppBar {...rest}>
|
|
15
|
+
<TitlePortal />
|
|
16
|
+
<Box sx={{ display: "flex", alignItems: "center", flex: 1, justifyContent: "flex-end" }}>
|
|
17
|
+
{domainSelectorButton}
|
|
18
|
+
</Box>
|
|
19
|
+
</ReactAdminAppBar>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Layout, LayoutProps, AppBar, TitlePortal } from "react-admin";
|
|
2
|
+
import { Box } from "@mui/material";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
// Custom AppBar specifically for the Tenants management interface
|
|
6
|
+
const TenantsListAppBar = (props: any) => {
|
|
7
|
+
return (
|
|
8
|
+
<AppBar {...props}>
|
|
9
|
+
<TitlePortal />
|
|
10
|
+
<Box flex={1} />
|
|
11
|
+
</AppBar>
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
interface TenantsLayoutProps extends LayoutProps {
|
|
16
|
+
domainSelectorButton?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Custom layout for the TenantsApp component
|
|
20
|
+
export function tenantsLayout(props: TenantsLayoutProps) {
|
|
21
|
+
const { domainSelectorButton, children, ...rest } = props;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Layout {...rest} appBar={TenantsListAppBar}>
|
|
25
|
+
{children}
|
|
26
|
+
</Layout>
|
|
27
|
+
);
|
|
28
|
+
}
|