@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.
Files changed (110) hide show
  1. package/.eslintrc.js +21 -0
  2. package/.vercelignore +4 -0
  3. package/CHANGELOG.md +56 -0
  4. package/LICENSE +21 -0
  5. package/README.md +50 -0
  6. package/index.html +125 -0
  7. package/package.json +61 -0
  8. package/prettier.config.js +1 -0
  9. package/public/favicon.ico +0 -0
  10. package/public/manifest.json +15 -0
  11. package/src/App.spec.tsx +42 -0
  12. package/src/App.tsx +232 -0
  13. package/src/AuthCallback.tsx +138 -0
  14. package/src/Layout.tsx +12 -0
  15. package/src/TenantsApp.tsx +115 -0
  16. package/src/auth0DataProvider.ts +1242 -0
  17. package/src/authProvider.ts +521 -0
  18. package/src/components/CertificateErrorDialog.tsx +116 -0
  19. package/src/components/DomainSelector.tsx +401 -0
  20. package/src/components/TenantAppBar.tsx +83 -0
  21. package/src/components/TenantLayout.tsx +25 -0
  22. package/src/components/TenantsAppBar.tsx +21 -0
  23. package/src/components/TenantsLayout.tsx +28 -0
  24. package/src/components/activity/ActivityDashboard.tsx +381 -0
  25. package/src/components/activity/index.ts +1 -0
  26. package/src/components/branding/BrandingList.tsx +0 -0
  27. package/src/components/branding/BrandingShow.tsx +0 -0
  28. package/src/components/branding/ThemesTab.tsx +286 -0
  29. package/src/components/branding/edit.tsx +149 -0
  30. package/src/components/branding/hooks/useThemesData.ts +123 -0
  31. package/src/components/branding/index.ts +2 -0
  32. package/src/components/branding/list.tsx +12 -0
  33. package/src/components/clients/create.tsx +12 -0
  34. package/src/components/clients/edit.tsx +1285 -0
  35. package/src/components/clients/index.ts +3 -0
  36. package/src/components/clients/list.tsx +37 -0
  37. package/src/components/common/DateAgo.tsx +6 -0
  38. package/src/components/common/JsonOutput.tsx +26 -0
  39. package/src/components/common/index.ts +1 -0
  40. package/src/components/connections/create.tsx +35 -0
  41. package/src/components/connections/edit.tsx +212 -0
  42. package/src/components/connections/index.ts +3 -0
  43. package/src/components/connections/list.tsx +15 -0
  44. package/src/components/custom-domains/create.tsx +26 -0
  45. package/src/components/custom-domains/edit.tsx +101 -0
  46. package/src/components/custom-domains/index.ts +3 -0
  47. package/src/components/custom-domains/list.tsx +16 -0
  48. package/src/components/flows/create.tsx +30 -0
  49. package/src/components/flows/edit.tsx +238 -0
  50. package/src/components/flows/index.ts +3 -0
  51. package/src/components/flows/list.tsx +15 -0
  52. package/src/components/forms/FlowEditor.tsx +1363 -0
  53. package/src/components/forms/NodeEditor.tsx +1119 -0
  54. package/src/components/forms/RichTextEditor.tsx +145 -0
  55. package/src/components/forms/create.tsx +30 -0
  56. package/src/components/forms/edit.tsx +256 -0
  57. package/src/components/forms/index.ts +3 -0
  58. package/src/components/forms/list.tsx +16 -0
  59. package/src/components/hooks/create.tsx +96 -0
  60. package/src/components/hooks/edit.tsx +114 -0
  61. package/src/components/hooks/index.ts +3 -0
  62. package/src/components/hooks/list.tsx +17 -0
  63. package/src/components/listActions/PostListActions.tsx +10 -0
  64. package/src/components/logs/LogIcon.tsx +32 -0
  65. package/src/components/logs/LogShow.tsx +82 -0
  66. package/src/components/logs/LogType.tsx +38 -0
  67. package/src/components/logs/index.ts +4 -0
  68. package/src/components/logs/list.tsx +41 -0
  69. package/src/components/organizations/create.tsx +13 -0
  70. package/src/components/organizations/edit.tsx +682 -0
  71. package/src/components/organizations/index.ts +3 -0
  72. package/src/components/organizations/list.tsx +21 -0
  73. package/src/components/resource-servers/create.tsx +87 -0
  74. package/src/components/resource-servers/edit.tsx +121 -0
  75. package/src/components/resource-servers/index.ts +3 -0
  76. package/src/components/resource-servers/list.tsx +47 -0
  77. package/src/components/roles/create.tsx +12 -0
  78. package/src/components/roles/edit.tsx +426 -0
  79. package/src/components/roles/index.ts +3 -0
  80. package/src/components/roles/list.tsx +24 -0
  81. package/src/components/sessions/edit.tsx +101 -0
  82. package/src/components/sessions/index.ts +3 -0
  83. package/src/components/sessions/list.tsx +20 -0
  84. package/src/components/sessions/show.tsx +113 -0
  85. package/src/components/settings/edit.tsx +236 -0
  86. package/src/components/settings/index.ts +2 -0
  87. package/src/components/settings/list.tsx +14 -0
  88. package/src/components/tenants/create.tsx +20 -0
  89. package/src/components/tenants/edit.tsx +54 -0
  90. package/src/components/tenants/index.ts +2 -0
  91. package/src/components/tenants/list.tsx +67 -0
  92. package/src/components/themes/edit.tsx +200 -0
  93. package/src/components/themes/index.ts +2 -0
  94. package/src/components/themes/list.tsx +12 -0
  95. package/src/components/users/create.tsx +144 -0
  96. package/src/components/users/edit.tsx +1711 -0
  97. package/src/components/users/index.ts +3 -0
  98. package/src/components/users/list.tsx +35 -0
  99. package/src/data.json +121 -0
  100. package/src/dataProvider.ts +97 -0
  101. package/src/index.tsx +106 -0
  102. package/src/lib/logs.ts +21 -0
  103. package/src/types/reactflow.d.ts +86 -0
  104. package/src/utils/domainUtils.ts +169 -0
  105. package/src/utils/tokenUtils.ts +75 -0
  106. package/src/vite-env.d.ts +1 -0
  107. package/tsconfig.json +37 -0
  108. package/tsconfig.node.json +10 -0
  109. package/vercel.json +17 -0
  110. 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
+ }