@strapi-community/plugin-better-auth-dashboard 1.0.0-alpha.1 → 1.0.0-alpha.10
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/README.md +101 -0
- package/dist/admin/{Root-hwPhIfaT.mjs → Root-6Fpvuogl.mjs} +2435 -1507
- package/dist/admin/{Root-Bl4iPGDu.js → Root-CN4cZKCJ.js} +2427 -1499
- package/dist/admin/index-WTRykUoM.js +116 -0
- package/dist/admin/index-X2-Fh6sz.mjs +117 -0
- package/dist/admin/index.js +2 -2
- package/dist/admin/index.mjs +2 -2
- package/dist/server/index.js +261 -19
- package/dist/server/index.mjs +261 -19
- package/package.json +10 -8
- package/dist/admin/index-A9PUvldu.js +0 -26
- package/dist/admin/index-Cvcysa5M.mjs +0 -27
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState, useMemo } from "react";
|
|
2
|
+
import { useId, useEffect, useState, useRef, useMemo } from "react";
|
|
3
3
|
import { useQuery, useQueryClient, useMutation, QueryClient, QueryClientProvider } from "react-query";
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
4
|
+
import { NavLink, Outlet, Routes, Route, Navigate, useOutletContext } from "react-router-dom";
|
|
5
|
+
import { Typography, ScrollArea, Box, Flex, Badge, SubNav as SubNav$1, Divider, Loader, Alert, Modal, Button, Portal, IconButton, Field, TextInput, Combobox, ComboboxOption, Checkbox, SingleSelect, SingleSelectOption, NumberInput, JSONInput, Textarea, Tabs, Grid, SearchForm, Searchbar } from "@strapi/design-system";
|
|
6
|
+
import { useRBAC, Page, Layouts, BackButton, useNotification, useFetchClient } from "@strapi/strapi/admin";
|
|
6
7
|
import { dashClient } from "@better-auth/infra/client";
|
|
7
8
|
import { createAuthClient, InferPlugin } from "better-auth/client";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { useIntl } from "react-intl";
|
|
10
|
+
import { P as PLUGIN_ID, a as PERMISSIONS, g as getMediaLibraryComponent, b as getEditViewSidePanels } from "./index-X2-Fh6sz.mjs";
|
|
11
|
+
import { Cross, Images, Trash, Plus, Pencil } from "@strapi/icons";
|
|
12
|
+
import styled, { keyframes } from "styled-components";
|
|
11
13
|
const dashPathMethods = () => ({
|
|
12
14
|
id: "dash-path-methods",
|
|
13
15
|
pathMethods: {
|
|
@@ -99,6 +101,315 @@ const client = createAuthClient({
|
|
|
99
101
|
headers: getAuthHeaders()
|
|
100
102
|
}
|
|
101
103
|
});
|
|
104
|
+
const getTranslation = (id) => `${PLUGIN_ID}.${id}`;
|
|
105
|
+
const HEIGHT_TOP_NAVIGATION = "6.4rem";
|
|
106
|
+
const HEIGHT_TOP_NAVIGATION_MEDIUM = "5.6rem";
|
|
107
|
+
const WIDTH_SIDE_NAVIGATION = "23.2rem";
|
|
108
|
+
const MainSubNav = styled(SubNav$1)`
|
|
109
|
+
width: 100%;
|
|
110
|
+
height: 100%;
|
|
111
|
+
overflow: hidden;
|
|
112
|
+
background-color: ${({ theme }) => theme.colors.neutral0};
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
border-right: 0;
|
|
116
|
+
box-shadow: none;
|
|
117
|
+
position: relative;
|
|
118
|
+
|
|
119
|
+
${({ theme }) => theme.breakpoints.medium} {
|
|
120
|
+
width: ${WIDTH_SIDE_NAVIGATION};
|
|
121
|
+
position: sticky;
|
|
122
|
+
top: 0;
|
|
123
|
+
border-right: 1px solid ${({ theme }) => theme.colors.neutral150};
|
|
124
|
+
overscroll-behavior: contain;
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
const Main = ({ children, ...props }) => /* @__PURE__ */ jsx(MainSubNav, { ...props, children });
|
|
128
|
+
const StyledLink = styled(NavLink)`
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
justify-content: space-between;
|
|
132
|
+
text-decoration: none;
|
|
133
|
+
height: 32px;
|
|
134
|
+
color: ${({ theme }) => theme.colors.neutral800};
|
|
135
|
+
|
|
136
|
+
&.active > div {
|
|
137
|
+
background-color: ${({ theme }) => theme.colors.primary100};
|
|
138
|
+
color: ${({ theme }) => theme.colors.primary700};
|
|
139
|
+
font-weight: 500;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
&:hover.active > div {
|
|
143
|
+
background-color: ${({ theme }) => theme.colors.primary100};
|
|
144
|
+
color: ${({ theme }) => theme.colors.primary700};
|
|
145
|
+
font-weight: 500;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
&:hover > div {
|
|
149
|
+
background-color: ${({ theme }) => theme.colors.neutral100};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
&:focus-visible {
|
|
153
|
+
outline-offset: -2px;
|
|
154
|
+
}
|
|
155
|
+
`;
|
|
156
|
+
const Link = (props) => {
|
|
157
|
+
const { to, label, endAction, handleClick, ...rest } = props;
|
|
158
|
+
return /* @__PURE__ */ jsx(StyledLink, { to, ...rest, onClick: handleClick, children: /* @__PURE__ */ jsx(
|
|
159
|
+
Box,
|
|
160
|
+
{
|
|
161
|
+
width: "100%",
|
|
162
|
+
paddingLeft: 3,
|
|
163
|
+
paddingRight: 3,
|
|
164
|
+
paddingTop: { initial: 1, large: 0 },
|
|
165
|
+
paddingBottom: { initial: 1, large: 0 },
|
|
166
|
+
borderRadius: 1,
|
|
167
|
+
children: /* @__PURE__ */ jsxs(
|
|
168
|
+
Flex,
|
|
169
|
+
{
|
|
170
|
+
justifyContent: "space-between",
|
|
171
|
+
width: "100%",
|
|
172
|
+
gap: { initial: 2, large: 1 },
|
|
173
|
+
children: [
|
|
174
|
+
/* @__PURE__ */ jsx(
|
|
175
|
+
Typography,
|
|
176
|
+
{
|
|
177
|
+
tag: "div",
|
|
178
|
+
lineHeight: "32px",
|
|
179
|
+
width: { initial: "80dvw", medium: "100%" },
|
|
180
|
+
overflow: "hidden",
|
|
181
|
+
style: { textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
182
|
+
children: label
|
|
183
|
+
}
|
|
184
|
+
),
|
|
185
|
+
/* @__PURE__ */ jsx(Flex, { gap: 2, children: endAction })
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
) });
|
|
191
|
+
};
|
|
192
|
+
const StyledHeader = styled(Flex)`
|
|
193
|
+
flex: 0 0 ${HEIGHT_TOP_NAVIGATION};
|
|
194
|
+
height: ${HEIGHT_TOP_NAVIGATION};
|
|
195
|
+
|
|
196
|
+
${({ theme }) => theme.breakpoints.medium} {
|
|
197
|
+
flex: 0 0 ${HEIGHT_TOP_NAVIGATION_MEDIUM};
|
|
198
|
+
height: ${HEIGHT_TOP_NAVIGATION_MEDIUM};
|
|
199
|
+
}
|
|
200
|
+
`;
|
|
201
|
+
const Header = ({ label }) => {
|
|
202
|
+
return /* @__PURE__ */ jsx(
|
|
203
|
+
StyledHeader,
|
|
204
|
+
{
|
|
205
|
+
justifyContent: "space-between",
|
|
206
|
+
paddingLeft: 5,
|
|
207
|
+
paddingRight: 5,
|
|
208
|
+
children: /* @__PURE__ */ jsx(Typography, { variant: "beta", tag: "h2", children: label })
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
const Sections = ({
|
|
213
|
+
children,
|
|
214
|
+
...props
|
|
215
|
+
}) => {
|
|
216
|
+
return /* @__PURE__ */ jsx(
|
|
217
|
+
Box,
|
|
218
|
+
{
|
|
219
|
+
paddingTop: { initial: 5, large: 4 },
|
|
220
|
+
paddingBottom: { initial: 5, large: 4 },
|
|
221
|
+
paddingLeft: { initial: 3, large: 0 },
|
|
222
|
+
paddingRight: { initial: 3, large: 0 },
|
|
223
|
+
maxWidth: { initial: "100%", medium: WIDTH_SIDE_NAVIGATION },
|
|
224
|
+
children: /* @__PURE__ */ jsx(Flex, { tag: "ul", gap: 6, direction: "column", alignItems: "stretch", ...props, children: children.map((child, index) => {
|
|
225
|
+
return /* @__PURE__ */ jsx("li", { children: child }, index);
|
|
226
|
+
}) })
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
const Section = ({
|
|
231
|
+
label,
|
|
232
|
+
children,
|
|
233
|
+
badgeLabel
|
|
234
|
+
}) => {
|
|
235
|
+
const listId = useId();
|
|
236
|
+
return /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
|
|
237
|
+
/* @__PURE__ */ jsx(
|
|
238
|
+
Box,
|
|
239
|
+
{
|
|
240
|
+
paddingLeft: {
|
|
241
|
+
initial: 3,
|
|
242
|
+
large: 5
|
|
243
|
+
},
|
|
244
|
+
paddingRight: {
|
|
245
|
+
initial: 3,
|
|
246
|
+
large: 5
|
|
247
|
+
},
|
|
248
|
+
children: /* @__PURE__ */ jsxs(Flex, { position: "relative", justifyContent: "space-between", gap: 2, children: [
|
|
249
|
+
/* @__PURE__ */ jsx(Flex, { children: /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: label }) }) }),
|
|
250
|
+
/* @__PURE__ */ jsx(Flex, { gap: 1, children: badgeLabel && /* @__PURE__ */ jsx(Badge, { backgroundColor: "neutral150", textColor: "neutral600", children: badgeLabel }) })
|
|
251
|
+
] })
|
|
252
|
+
}
|
|
253
|
+
),
|
|
254
|
+
/* @__PURE__ */ jsx(
|
|
255
|
+
Flex,
|
|
256
|
+
{
|
|
257
|
+
tag: "ol",
|
|
258
|
+
id: listId,
|
|
259
|
+
direction: "column",
|
|
260
|
+
gap: { initial: 2, large: "2px" },
|
|
261
|
+
alignItems: "stretch",
|
|
262
|
+
marginLeft: {
|
|
263
|
+
initial: 0,
|
|
264
|
+
large: 2
|
|
265
|
+
},
|
|
266
|
+
marginRight: {
|
|
267
|
+
initial: 0,
|
|
268
|
+
large: 2
|
|
269
|
+
},
|
|
270
|
+
children: children.map((child, index) => {
|
|
271
|
+
return /* @__PURE__ */ jsx("li", { children: child }, index);
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
] });
|
|
276
|
+
};
|
|
277
|
+
styled.button`
|
|
278
|
+
cursor: pointer;
|
|
279
|
+
width: 100%;
|
|
280
|
+
border: none;
|
|
281
|
+
padding: 0;
|
|
282
|
+
background: transparent;
|
|
283
|
+
display: flex;
|
|
284
|
+
align-items: center;
|
|
285
|
+
border-radius: ${({ theme }) => theme.borderRadius};
|
|
286
|
+
padding-left: ${({ theme }) => theme.spaces[3]};
|
|
287
|
+
padding-right: ${({ theme }) => theme.spaces[3]};
|
|
288
|
+
padding-top: ${({ theme }) => theme.spaces[2]};
|
|
289
|
+
padding-bottom: ${({ theme }) => theme.spaces[2]};
|
|
290
|
+
|
|
291
|
+
&:hover {
|
|
292
|
+
background-color: ${({ theme }) => theme.colors.neutral100};
|
|
293
|
+
}
|
|
294
|
+
`;
|
|
295
|
+
styled.li`
|
|
296
|
+
${StyledLink} > div {
|
|
297
|
+
padding-left: 36px;
|
|
298
|
+
}
|
|
299
|
+
`;
|
|
300
|
+
styled(Box)`
|
|
301
|
+
width: 100%;
|
|
302
|
+
|
|
303
|
+
${MainSubNav} {
|
|
304
|
+
background-color: transparent;
|
|
305
|
+
border-right: none;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
${({ theme }) => theme.breakpoints.medium} {
|
|
309
|
+
overflow: hidden;
|
|
310
|
+
|
|
311
|
+
${MainSubNav} {
|
|
312
|
+
top: 0;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
`;
|
|
316
|
+
const Content = ({ children }) => {
|
|
317
|
+
return /* @__PURE__ */ jsx(ScrollArea, { children });
|
|
318
|
+
};
|
|
319
|
+
const SubNav = {
|
|
320
|
+
Main,
|
|
321
|
+
Content,
|
|
322
|
+
Header,
|
|
323
|
+
Link,
|
|
324
|
+
Sections,
|
|
325
|
+
Section
|
|
326
|
+
};
|
|
327
|
+
const sections = [
|
|
328
|
+
{
|
|
329
|
+
id: "api-roles",
|
|
330
|
+
intlLabel: {
|
|
331
|
+
id: getTranslation("settings.general"),
|
|
332
|
+
defaultMessage: "General"
|
|
333
|
+
},
|
|
334
|
+
links: [
|
|
335
|
+
{
|
|
336
|
+
id: "overview",
|
|
337
|
+
intlLabel: {
|
|
338
|
+
id: getTranslation("settings.overview"),
|
|
339
|
+
defaultMessage: "Overview"
|
|
340
|
+
},
|
|
341
|
+
to: "/plugins/better-auth-dashboard/overview",
|
|
342
|
+
testid: "nav-overview"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: "users",
|
|
346
|
+
intlLabel: {
|
|
347
|
+
id: getTranslation("settings.users"),
|
|
348
|
+
defaultMessage: "User Management"
|
|
349
|
+
},
|
|
350
|
+
to: "/plugins/better-auth-dashboard/users",
|
|
351
|
+
testid: "nav-users"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: "organizations",
|
|
355
|
+
intlLabel: {
|
|
356
|
+
id: getTranslation("settings.organizations"),
|
|
357
|
+
defaultMessage: "Organization Management"
|
|
358
|
+
},
|
|
359
|
+
to: "/plugins/better-auth-dashboard/organizations",
|
|
360
|
+
testid: "nav-organizations"
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
}
|
|
364
|
+
];
|
|
365
|
+
const Nav = ({ isFullPage = false, orgEnabled = false }) => {
|
|
366
|
+
const { formatMessage } = useIntl();
|
|
367
|
+
const overviewRBAC = useRBAC(PERMISSIONS.overview);
|
|
368
|
+
const userRBAC = useRBAC(PERMISSIONS.user);
|
|
369
|
+
const organizationRBAC = useRBAC(PERMISSIONS.organization);
|
|
370
|
+
return /* @__PURE__ */ jsxs(SubNav.Main, { children: [
|
|
371
|
+
!isFullPage && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
372
|
+
/* @__PURE__ */ jsx(SubNav.Header, { label: "Authentication" }),
|
|
373
|
+
/* @__PURE__ */ jsx(Divider, {})
|
|
374
|
+
] }),
|
|
375
|
+
/* @__PURE__ */ jsxs(SubNav.Content, { children: [
|
|
376
|
+
isFullPage && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
377
|
+
/* @__PURE__ */ jsx(SubNav.Header, { label: "Authentication" }),
|
|
378
|
+
/* @__PURE__ */ jsx(Divider, {})
|
|
379
|
+
] }),
|
|
380
|
+
/* @__PURE__ */ jsx(SubNav.Sections, { "data-testid": "main-nav", children: sections.map((section) => /* @__PURE__ */ jsx(
|
|
381
|
+
SubNav.Section,
|
|
382
|
+
{
|
|
383
|
+
label: formatMessage(section.intlLabel),
|
|
384
|
+
children: section.links.filter((link) => orgEnabled || link.id !== "organizations").filter((link) => {
|
|
385
|
+
if (link.id === "overview") {
|
|
386
|
+
return overviewRBAC.allowedActions.canRead;
|
|
387
|
+
}
|
|
388
|
+
if (link.id === "users") {
|
|
389
|
+
return userRBAC.allowedActions.canRead;
|
|
390
|
+
}
|
|
391
|
+
if (link.id === "organizations") {
|
|
392
|
+
return organizationRBAC.allowedActions.canRead;
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}).map((link) => {
|
|
396
|
+
return /* @__PURE__ */ jsx(
|
|
397
|
+
SubNav.Link,
|
|
398
|
+
{
|
|
399
|
+
to: link.to,
|
|
400
|
+
label: formatMessage(link.intlLabel),
|
|
401
|
+
"data-testid": link.testid,
|
|
402
|
+
children: formatMessage(link.intlLabel)
|
|
403
|
+
},
|
|
404
|
+
link.id
|
|
405
|
+
);
|
|
406
|
+
})
|
|
407
|
+
},
|
|
408
|
+
section.id
|
|
409
|
+
)) })
|
|
410
|
+
] })
|
|
411
|
+
] });
|
|
412
|
+
};
|
|
102
413
|
function useDashConfig() {
|
|
103
414
|
return useQuery({
|
|
104
415
|
queryKey: ["dash-config"],
|
|
@@ -115,6 +426,74 @@ function useDashConfig() {
|
|
|
115
426
|
function hasPlugin(config, id) {
|
|
116
427
|
return config?.plugins.some((p) => p.id === id) ?? false;
|
|
117
428
|
}
|
|
429
|
+
const RESPONSIVE_DEFAULT_SPACING = {
|
|
430
|
+
initial: 4,
|
|
431
|
+
medium: 6,
|
|
432
|
+
large: 10
|
|
433
|
+
};
|
|
434
|
+
const App = () => {
|
|
435
|
+
const { data: config, isLoading, isError, error } = useDashConfig();
|
|
436
|
+
const orgEnabled = hasPlugin(config, "organization");
|
|
437
|
+
const orgOptionsQuery = useQuery({
|
|
438
|
+
queryKey: ["dash-org-options"],
|
|
439
|
+
queryFn: async () => {
|
|
440
|
+
const result = await client.dash.organization.options();
|
|
441
|
+
if (result.error) return { teamsEnabled: false };
|
|
442
|
+
return result.data ?? { teamsEnabled: false };
|
|
443
|
+
},
|
|
444
|
+
enabled: orgEnabled
|
|
445
|
+
});
|
|
446
|
+
const teamsEnabled = orgEnabled ? orgOptionsQuery.data?.teamsEnabled ?? false : false;
|
|
447
|
+
if (isLoading) {
|
|
448
|
+
return /* @__PURE__ */ jsx(Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsx(Loader, { children: "Loading Better Auth dashboard…" }) });
|
|
449
|
+
}
|
|
450
|
+
if (isError) {
|
|
451
|
+
return /* @__PURE__ */ jsx(Box, { padding: 6, children: /* @__PURE__ */ jsx(
|
|
452
|
+
Alert,
|
|
453
|
+
{
|
|
454
|
+
closeLabel: "Close",
|
|
455
|
+
title: "Error loading configuration",
|
|
456
|
+
variant: "danger",
|
|
457
|
+
children: error?.message
|
|
458
|
+
}
|
|
459
|
+
) });
|
|
460
|
+
}
|
|
461
|
+
if (!config) return null;
|
|
462
|
+
return /* @__PURE__ */ jsx(
|
|
463
|
+
"div",
|
|
464
|
+
{
|
|
465
|
+
"data-testid": "dashboard-root",
|
|
466
|
+
style: { display: "flex", flex: "1 1 auto", minHeight: "100vh" },
|
|
467
|
+
children: /* @__PURE__ */ jsxs(Layouts.Root, { sideNav: /* @__PURE__ */ jsx(Nav, { orgEnabled }), children: [
|
|
468
|
+
/* @__PURE__ */ jsx(Page.Title, { children: "Authentication" }),
|
|
469
|
+
/* @__PURE__ */ jsx(
|
|
470
|
+
Box,
|
|
471
|
+
{
|
|
472
|
+
display: { initial: "block", medium: "none" },
|
|
473
|
+
paddingLeft: RESPONSIVE_DEFAULT_SPACING,
|
|
474
|
+
paddingRight: RESPONSIVE_DEFAULT_SPACING,
|
|
475
|
+
paddingTop: RESPONSIVE_DEFAULT_SPACING,
|
|
476
|
+
children: /* @__PURE__ */ jsx(BackButton, { fallback: "/plugins/better-auth-dashboard" })
|
|
477
|
+
}
|
|
478
|
+
),
|
|
479
|
+
/* @__PURE__ */ jsx(Outlet, { context: { config, teamsEnabled } })
|
|
480
|
+
] })
|
|
481
|
+
}
|
|
482
|
+
);
|
|
483
|
+
};
|
|
484
|
+
const ProtectedApp = () => {
|
|
485
|
+
return /* @__PURE__ */ jsx(
|
|
486
|
+
Page.Protect,
|
|
487
|
+
{
|
|
488
|
+
permissions: [
|
|
489
|
+
...PERMISSIONS.overview,
|
|
490
|
+
...PERMISSIONS.user,
|
|
491
|
+
...PERMISSIONS.organization
|
|
492
|
+
],
|
|
493
|
+
children: /* @__PURE__ */ jsx(App, {})
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
};
|
|
118
497
|
function ConfirmDialog({
|
|
119
498
|
title,
|
|
120
499
|
message,
|
|
@@ -141,8 +520,9 @@ function ConfirmDialog({
|
|
|
141
520
|
);
|
|
142
521
|
}
|
|
143
522
|
const DASH_CONTEXT_HEADER = "X-Dash-Context";
|
|
144
|
-
const withContext = (ctx) => ({
|
|
523
|
+
const withContext = (ctx, headers) => ({
|
|
145
524
|
headers: {
|
|
525
|
+
...headers,
|
|
146
526
|
[DASH_CONTEXT_HEADER]: btoa(JSON.stringify(ctx))
|
|
147
527
|
}
|
|
148
528
|
});
|
|
@@ -450,11 +830,22 @@ function UserCombobox({
|
|
|
450
830
|
queryKey: ["user-combobox-search", search],
|
|
451
831
|
queryFn: async () => {
|
|
452
832
|
const whereClause = search ? JSON.stringify([
|
|
453
|
-
{
|
|
833
|
+
{
|
|
834
|
+
field: "email",
|
|
835
|
+
operator: "contains",
|
|
836
|
+
value: search,
|
|
837
|
+
connector: "OR"
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
field: "name",
|
|
841
|
+
operator: "contains",
|
|
842
|
+
value: search,
|
|
843
|
+
connector: "OR"
|
|
844
|
+
}
|
|
454
845
|
]) : void 0;
|
|
455
846
|
const result = await client.dash.listUsers({
|
|
456
847
|
query: {
|
|
457
|
-
limit: 20,
|
|
848
|
+
limit: search ? 100 : 20,
|
|
458
849
|
offset: 0,
|
|
459
850
|
sortBy: "createdAt",
|
|
460
851
|
sortOrder: "desc",
|
|
@@ -506,7 +897,7 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
|
|
|
506
897
|
...logo ? { logo } : {},
|
|
507
898
|
...teamsEnabled && !skipDefaultTeam && defaultTeamName ? { defaultTeamName } : {}
|
|
508
899
|
},
|
|
509
|
-
withContext({ userId: ownerId, skipDefaultTeam })
|
|
900
|
+
withContext({ userId: ownerId, skipDefaultTeam }, getAuthHeaders())
|
|
510
901
|
);
|
|
511
902
|
if (result.error)
|
|
512
903
|
throw new Error(result.error.message ?? "Create failed");
|
|
@@ -542,6 +933,7 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
|
|
|
542
933
|
loading: createMutation.isLoading,
|
|
543
934
|
disabled: !name || !slug || !ownerId,
|
|
544
935
|
onClick: () => createMutation.mutate(),
|
|
936
|
+
"data-testid": "create-org-submit",
|
|
545
937
|
children: "Create organization"
|
|
546
938
|
}
|
|
547
939
|
)
|
|
@@ -629,6 +1021,159 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
|
|
|
629
1021
|
function slugify(str) {
|
|
630
1022
|
return str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
631
1023
|
}
|
|
1024
|
+
const EntryRow = styled.div`
|
|
1025
|
+
display: flex;
|
|
1026
|
+
align-items: center;
|
|
1027
|
+
justify-content: space-between;
|
|
1028
|
+
gap: 8px;
|
|
1029
|
+
padding: 8px 12px;
|
|
1030
|
+
width: 100%;
|
|
1031
|
+
box-sizing: border-box;
|
|
1032
|
+
background: #ffffff;
|
|
1033
|
+
border: 1px solid ${({ theme }) => theme.colors?.neutral200 ?? "#dcdce4"};
|
|
1034
|
+
border-radius: 4px;
|
|
1035
|
+
min-width: 0;
|
|
1036
|
+
`;
|
|
1037
|
+
function getDisplayLabel(doc) {
|
|
1038
|
+
for (const key of ["name", "title", "email", "username", "label", "slug"]) {
|
|
1039
|
+
const v = doc[key];
|
|
1040
|
+
if (typeof v === "string" && v.trim()) return v;
|
|
1041
|
+
}
|
|
1042
|
+
return String(doc.documentId ?? doc.id ?? "");
|
|
1043
|
+
}
|
|
1044
|
+
function normalizeValue(val, cache) {
|
|
1045
|
+
if (!val) return [];
|
|
1046
|
+
if (typeof val === "object" && !Array.isArray(val) && val !== null && "set" in val) {
|
|
1047
|
+
const items2 = val.set ?? [];
|
|
1048
|
+
return items2.filter((item) => item?.documentId).map((item) => ({
|
|
1049
|
+
documentId: item.documentId,
|
|
1050
|
+
label: cache.get(item.documentId) ?? item.documentId
|
|
1051
|
+
}));
|
|
1052
|
+
}
|
|
1053
|
+
const items = Array.isArray(val) ? val : [val];
|
|
1054
|
+
return items.filter(
|
|
1055
|
+
(item) => typeof item === "object" && item !== null && "documentId" in item
|
|
1056
|
+
).map((item) => ({
|
|
1057
|
+
documentId: String(item.documentId),
|
|
1058
|
+
label: getDisplayLabel(item)
|
|
1059
|
+
}));
|
|
1060
|
+
}
|
|
1061
|
+
function RelationField({
|
|
1062
|
+
name,
|
|
1063
|
+
label,
|
|
1064
|
+
attribute,
|
|
1065
|
+
value,
|
|
1066
|
+
onChange,
|
|
1067
|
+
readOnly = false
|
|
1068
|
+
}) {
|
|
1069
|
+
const { get } = useFetchClient();
|
|
1070
|
+
const cache = useRef(/* @__PURE__ */ new Map());
|
|
1071
|
+
const [search, setSearch] = useState("");
|
|
1072
|
+
const [addKey, setAddKey] = useState(0);
|
|
1073
|
+
const target = attribute.target ?? "";
|
|
1074
|
+
const relationType = attribute.relation ?? "manyToOne";
|
|
1075
|
+
const isMulti = relationType === "oneToMany" || relationType === "manyToMany";
|
|
1076
|
+
const currentDocs = normalizeValue(value, cache.current);
|
|
1077
|
+
const selectedIds = new Set(currentDocs.map((d) => d.documentId));
|
|
1078
|
+
const { data: searchResults = [], isLoading } = useQuery({
|
|
1079
|
+
queryKey: ["relation-search", target, search],
|
|
1080
|
+
queryFn: async () => {
|
|
1081
|
+
const params = new URLSearchParams({
|
|
1082
|
+
uid: target,
|
|
1083
|
+
"pagination[pageSize]": "50"
|
|
1084
|
+
});
|
|
1085
|
+
if (search) params.set("filters[name][$containsi]", search);
|
|
1086
|
+
const { data } = await get(
|
|
1087
|
+
`/better-auth-dashboard/db?${params}`
|
|
1088
|
+
);
|
|
1089
|
+
const docs = data.results ?? [];
|
|
1090
|
+
for (const doc of docs) {
|
|
1091
|
+
if (doc.documentId) {
|
|
1092
|
+
cache.current.set(String(doc.documentId), getDisplayLabel(doc));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return docs.map((doc) => ({
|
|
1096
|
+
documentId: String(doc.documentId ?? ""),
|
|
1097
|
+
label: getDisplayLabel(doc)
|
|
1098
|
+
})).filter((d) => d.documentId);
|
|
1099
|
+
},
|
|
1100
|
+
keepPreviousData: true,
|
|
1101
|
+
enabled: !readOnly
|
|
1102
|
+
});
|
|
1103
|
+
const availableOptions = searchResults.filter(
|
|
1104
|
+
(r) => !selectedIds.has(r.documentId)
|
|
1105
|
+
);
|
|
1106
|
+
const commit = (ids) => {
|
|
1107
|
+
onChange(name, { set: ids.map((id) => ({ documentId: id })) });
|
|
1108
|
+
};
|
|
1109
|
+
const handleSelect = (val) => {
|
|
1110
|
+
if (!val) return;
|
|
1111
|
+
const opt = searchResults.find((r) => r.documentId === val);
|
|
1112
|
+
if (opt) cache.current.set(val, opt.label);
|
|
1113
|
+
if (isMulti) {
|
|
1114
|
+
if (selectedIds.has(val)) return;
|
|
1115
|
+
commit([...currentDocs.map((d) => d.documentId), val]);
|
|
1116
|
+
} else {
|
|
1117
|
+
commit([val]);
|
|
1118
|
+
}
|
|
1119
|
+
setSearch("");
|
|
1120
|
+
setAddKey((k) => k + 1);
|
|
1121
|
+
};
|
|
1122
|
+
const handleRemove = (docId) => {
|
|
1123
|
+
commit(
|
|
1124
|
+
currentDocs.filter((d) => d.documentId !== docId).map((d) => d.documentId)
|
|
1125
|
+
);
|
|
1126
|
+
};
|
|
1127
|
+
return /* @__PURE__ */ jsxs(Field.Root, { style: { width: "100%" }, children: [
|
|
1128
|
+
/* @__PURE__ */ jsx(Field.Label, { children: label }),
|
|
1129
|
+
!readOnly && /* @__PURE__ */ jsx(
|
|
1130
|
+
Combobox,
|
|
1131
|
+
{
|
|
1132
|
+
value: "",
|
|
1133
|
+
onChange: (val) => handleSelect(val),
|
|
1134
|
+
onInputChange: (e) => setSearch(e.target.value),
|
|
1135
|
+
loading: isLoading,
|
|
1136
|
+
placeholder: "Search…",
|
|
1137
|
+
children: availableOptions.map((opt) => /* @__PURE__ */ jsx(ComboboxOption, { value: opt.documentId, children: opt.label }, opt.documentId))
|
|
1138
|
+
},
|
|
1139
|
+
addKey
|
|
1140
|
+
),
|
|
1141
|
+
currentDocs.length > 0 && /* @__PURE__ */ jsx(
|
|
1142
|
+
Flex,
|
|
1143
|
+
{
|
|
1144
|
+
direction: "column",
|
|
1145
|
+
gap: 1,
|
|
1146
|
+
style: { marginTop: 8, width: "100%" },
|
|
1147
|
+
children: currentDocs.map((doc) => /* @__PURE__ */ jsxs(EntryRow, { children: [
|
|
1148
|
+
/* @__PURE__ */ jsx(
|
|
1149
|
+
Typography,
|
|
1150
|
+
{
|
|
1151
|
+
variant: "omega",
|
|
1152
|
+
textColor: "neutral800",
|
|
1153
|
+
style: {
|
|
1154
|
+
overflow: "hidden",
|
|
1155
|
+
textOverflow: "ellipsis",
|
|
1156
|
+
whiteSpace: "nowrap",
|
|
1157
|
+
flex: 1,
|
|
1158
|
+
minWidth: 0
|
|
1159
|
+
},
|
|
1160
|
+
children: doc.label
|
|
1161
|
+
}
|
|
1162
|
+
),
|
|
1163
|
+
!readOnly && /* @__PURE__ */ jsx(
|
|
1164
|
+
IconButton,
|
|
1165
|
+
{
|
|
1166
|
+
label: "Remove",
|
|
1167
|
+
size: "S",
|
|
1168
|
+
onClick: () => handleRemove(doc.documentId),
|
|
1169
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
1170
|
+
}
|
|
1171
|
+
)
|
|
1172
|
+
] }, doc.documentId))
|
|
1173
|
+
}
|
|
1174
|
+
)
|
|
1175
|
+
] });
|
|
1176
|
+
}
|
|
632
1177
|
function makeLabel(name) {
|
|
633
1178
|
return name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
634
1179
|
}
|
|
@@ -752,24 +1297,15 @@ function DynamicField({
|
|
|
752
1297
|
] });
|
|
753
1298
|
}
|
|
754
1299
|
if (type === "relation") {
|
|
755
|
-
return /* @__PURE__ */
|
|
756
|
-
|
|
1300
|
+
return /* @__PURE__ */ jsx(
|
|
1301
|
+
RelationField,
|
|
757
1302
|
{
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
{
|
|
765
|
-
value: value != null ? String(value) : "",
|
|
766
|
-
onChange: (e) => onChange(name, e.target.value),
|
|
767
|
-
disabled: readOnly,
|
|
768
|
-
placeholder: "ID…"
|
|
769
|
-
}
|
|
770
|
-
),
|
|
771
|
-
/* @__PURE__ */ jsx(Field.Hint, {})
|
|
772
|
-
]
|
|
1303
|
+
name,
|
|
1304
|
+
label,
|
|
1305
|
+
attribute,
|
|
1306
|
+
value,
|
|
1307
|
+
onChange,
|
|
1308
|
+
readOnly
|
|
773
1309
|
}
|
|
774
1310
|
);
|
|
775
1311
|
}
|
|
@@ -809,6 +1345,21 @@ function CustomFieldsSection({
|
|
|
809
1345
|
)) })
|
|
810
1346
|
] });
|
|
811
1347
|
}
|
|
1348
|
+
function EditViewSidePanels({ model, documentId, document: document2 }) {
|
|
1349
|
+
const panels = getEditViewSidePanels(model);
|
|
1350
|
+
if (panels.length === 0) return null;
|
|
1351
|
+
return /* @__PURE__ */ jsx(Fragment, { children: panels.map(({ id, title, Component }) => /* @__PURE__ */ jsxs(FormSection, { children: [
|
|
1352
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: title }),
|
|
1353
|
+
/* @__PURE__ */ jsx(
|
|
1354
|
+
Component,
|
|
1355
|
+
{
|
|
1356
|
+
model,
|
|
1357
|
+
documentId,
|
|
1358
|
+
document: document2
|
|
1359
|
+
}
|
|
1360
|
+
)
|
|
1361
|
+
] }, id)) });
|
|
1362
|
+
}
|
|
812
1363
|
const SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
813
1364
|
"id",
|
|
814
1365
|
"documentId",
|
|
@@ -840,6 +1391,8 @@ function useModelSchema(model) {
|
|
|
840
1391
|
if (SYSTEM_FIELDS.has(name)) continue;
|
|
841
1392
|
if (attr.type === "relation" && typeof attr.target === "string" && (attr.target.startsWith("plugin::users-permissions") || attr.target.startsWith("admin::")))
|
|
842
1393
|
continue;
|
|
1394
|
+
const baOptions = attr.pluginOptions?.["better-auth"];
|
|
1395
|
+
if (baOptions?.managed === true) continue;
|
|
843
1396
|
attributes[name] = attr;
|
|
844
1397
|
}
|
|
845
1398
|
return attributes;
|
|
@@ -892,12 +1445,13 @@ function OrganizationDetail({
|
|
|
892
1445
|
const schemaQuery = useModelSchema("organization");
|
|
893
1446
|
const qc = useQueryClient();
|
|
894
1447
|
const { toggleNotification } = useNotification();
|
|
1448
|
+
const { get, put } = useFetchClient();
|
|
895
1449
|
const orgQuery = useQuery({
|
|
896
1450
|
queryKey: ["dash-org", organizationId],
|
|
897
1451
|
queryFn: async () => {
|
|
898
1452
|
const result = await client.dash.organization[organizationId](
|
|
899
1453
|
{},
|
|
900
|
-
withContext({ organizationId })
|
|
1454
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
901
1455
|
);
|
|
902
1456
|
if (result.error)
|
|
903
1457
|
throw new Error(result.error.message ?? "Failed to load org");
|
|
@@ -907,7 +1461,7 @@ function OrganizationDetail({
|
|
|
907
1461
|
const membersQuery = useQuery({
|
|
908
1462
|
queryKey: ["dash-org-members", organizationId],
|
|
909
1463
|
queryFn: async () => {
|
|
910
|
-
const result = await client.dash.organization[organizationId].members({}, withContext({ organizationId }));
|
|
1464
|
+
const result = await client.dash.organization[organizationId].members({}, withContext({ organizationId }, getAuthHeaders()));
|
|
911
1465
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
912
1466
|
return result.data ?? [];
|
|
913
1467
|
}
|
|
@@ -916,7 +1470,7 @@ function OrganizationDetail({
|
|
|
916
1470
|
queryKey: ["dash-org-teams", organizationId],
|
|
917
1471
|
queryFn: async () => {
|
|
918
1472
|
if (!teamsEnabled) return [];
|
|
919
|
-
const result = await client.dash.organization[organizationId].teams({}, withContext({ organizationId }));
|
|
1473
|
+
const result = await client.dash.organization[organizationId].teams({}, withContext({ organizationId }, getAuthHeaders()));
|
|
920
1474
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
921
1475
|
return result.data ?? [];
|
|
922
1476
|
},
|
|
@@ -925,11 +1479,29 @@ function OrganizationDetail({
|
|
|
925
1479
|
const ssoQuery = useQuery({
|
|
926
1480
|
queryKey: ["dash-org-sso", organizationId],
|
|
927
1481
|
queryFn: async () => {
|
|
928
|
-
const result = await client.dash.organization[organizationId].ssoProviders({}, withContext({ organizationId }));
|
|
1482
|
+
const result = await client.dash.organization[organizationId].ssoProviders({}, withContext({ organizationId }, getAuthHeaders()));
|
|
1483
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1484
|
+
return result.data ?? [];
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
const invitationsQuery = useQuery({
|
|
1488
|
+
queryKey: ["dash-org-invitations", organizationId],
|
|
1489
|
+
queryFn: async () => {
|
|
1490
|
+
const result = await client.dash.organization[organizationId].invitations({}, withContext({ organizationId }, getAuthHeaders()));
|
|
929
1491
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
930
1492
|
return result.data ?? [];
|
|
931
1493
|
}
|
|
932
1494
|
});
|
|
1495
|
+
const strapiOrgQuery = useQuery({
|
|
1496
|
+
queryKey: ["dash-strapi-org", organizationId],
|
|
1497
|
+
enabled: !!orgQuery.data,
|
|
1498
|
+
queryFn: async () => {
|
|
1499
|
+
const { data } = await get(
|
|
1500
|
+
`/better-auth-dashboard/db?uid=plugin::better-auth.organization&filters[id][$eq]=${organizationId}&pagination[pageSize]=1`
|
|
1501
|
+
);
|
|
1502
|
+
return data.results?.[0] ?? null;
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
933
1505
|
const [activeTab, setActiveTab] = useState("details");
|
|
934
1506
|
const [editName, setEditName] = useState(void 0);
|
|
935
1507
|
const [editSlug, setEditSlug] = useState(void 0);
|
|
@@ -942,12 +1514,15 @@ function OrganizationDetail({
|
|
|
942
1514
|
const [confirmDeleteSsoId, setConfirmDeleteSsoId] = useState(
|
|
943
1515
|
null
|
|
944
1516
|
);
|
|
1517
|
+
const [inviteEmail, setInviteEmail] = useState("");
|
|
1518
|
+
const [inviteRole, setInviteRole] = useState("member");
|
|
1519
|
+
const [confirmCancelInvitationId, setConfirmCancelInvitationId] = useState(null);
|
|
945
1520
|
const handleExtraChange = (name, value) => {
|
|
946
1521
|
setEditExtra((prev) => ({ ...prev, [name]: value }));
|
|
947
1522
|
};
|
|
948
1523
|
const org = orgQuery.data;
|
|
949
1524
|
const extraData = {
|
|
950
|
-
...
|
|
1525
|
+
...strapiOrgQuery.data ?? {},
|
|
951
1526
|
...editExtra
|
|
952
1527
|
};
|
|
953
1528
|
const updateOrgMutation = useMutation({
|
|
@@ -956,17 +1531,21 @@ function OrganizationDetail({
|
|
|
956
1531
|
if (editName !== void 0) body.name = editName;
|
|
957
1532
|
if (editSlug !== void 0) body.slug = editSlug;
|
|
958
1533
|
if (editLogo !== void 0) body.logo = editLogo;
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
1534
|
+
const documentId = strapiOrgQuery.data?.documentId;
|
|
1535
|
+
if (!documentId)
|
|
1536
|
+
throw new Error("Could not resolve documentId for organization");
|
|
1537
|
+
await put(
|
|
1538
|
+
`/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.organization`,
|
|
1539
|
+
body
|
|
962
1540
|
);
|
|
963
|
-
if (result.error)
|
|
964
|
-
throw new Error(result.error.message ?? "Update failed");
|
|
965
|
-
return result.data;
|
|
966
1541
|
},
|
|
967
|
-
onSuccess: () => {
|
|
1542
|
+
onSuccess: async () => {
|
|
1543
|
+
await qc.invalidateQueries({
|
|
1544
|
+
queryKey: ["dash-strapi-org", organizationId]
|
|
1545
|
+
});
|
|
968
1546
|
qc.invalidateQueries({ queryKey: ["dash-org", organizationId] });
|
|
969
1547
|
qc.invalidateQueries({ queryKey: ["dash-organizations"] });
|
|
1548
|
+
qc.invalidateQueries({ queryKey: ["dash-strapi-org", organizationId] });
|
|
970
1549
|
setEditName(void 0);
|
|
971
1550
|
setEditSlug(void 0);
|
|
972
1551
|
setEditLogo(void 0);
|
|
@@ -990,7 +1569,7 @@ function OrganizationDetail({
|
|
|
990
1569
|
mutationFn: async () => {
|
|
991
1570
|
const result = await client.dash.organization.addMember(
|
|
992
1571
|
{ userId: addUserId, role: addRole },
|
|
993
|
-
withContext({ organizationId })
|
|
1572
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
994
1573
|
);
|
|
995
1574
|
if (result.error)
|
|
996
1575
|
throw new Error(result.error.message ?? "Add member failed");
|
|
@@ -1013,7 +1592,7 @@ function OrganizationDetail({
|
|
|
1013
1592
|
mutationFn: async (memberId) => {
|
|
1014
1593
|
const result = await client.dash.organization.removeMember(
|
|
1015
1594
|
{ memberId },
|
|
1016
|
-
withContext({ organizationId })
|
|
1595
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
1017
1596
|
);
|
|
1018
1597
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1019
1598
|
},
|
|
@@ -1036,7 +1615,7 @@ function OrganizationDetail({
|
|
|
1036
1615
|
}) => {
|
|
1037
1616
|
const result = await client.dash.organization.updateMemberRole(
|
|
1038
1617
|
{ memberId, role },
|
|
1039
|
-
withContext({ organizationId })
|
|
1618
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
1040
1619
|
);
|
|
1041
1620
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1042
1621
|
return result.data;
|
|
@@ -1057,7 +1636,7 @@ function OrganizationDetail({
|
|
|
1057
1636
|
mutationFn: async () => {
|
|
1058
1637
|
const result = await client.dash.organization.createTeam(
|
|
1059
1638
|
{ name: newTeamName },
|
|
1060
|
-
withContext({ organizationId })
|
|
1639
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
1061
1640
|
);
|
|
1062
1641
|
if (result.error)
|
|
1063
1642
|
throw new Error(result.error.message ?? "Create team failed");
|
|
@@ -1079,7 +1658,7 @@ function OrganizationDetail({
|
|
|
1079
1658
|
mutationFn: async (teamId) => {
|
|
1080
1659
|
const result = await client.dash.organization.deleteTeam(
|
|
1081
1660
|
{ teamId },
|
|
1082
|
-
withContext({ organizationId })
|
|
1661
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
1083
1662
|
);
|
|
1084
1663
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1085
1664
|
},
|
|
@@ -1097,7 +1676,10 @@ function OrganizationDetail({
|
|
|
1097
1676
|
});
|
|
1098
1677
|
const deleteSsoMutation = useMutation({
|
|
1099
1678
|
mutationFn: async (providerId) => {
|
|
1100
|
-
const result = await client.dash.organization[organizationId].ssoProvider.delete(
|
|
1679
|
+
const result = await client.dash.organization[organizationId].ssoProvider.delete(
|
|
1680
|
+
{ providerId },
|
|
1681
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
1682
|
+
);
|
|
1101
1683
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1102
1684
|
},
|
|
1103
1685
|
onSuccess: () => {
|
|
@@ -1112,9 +1694,74 @@ function OrganizationDetail({
|
|
|
1112
1694
|
});
|
|
1113
1695
|
}
|
|
1114
1696
|
});
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1697
|
+
const inviteMemberMutation = useMutation({
|
|
1698
|
+
mutationFn: async () => {
|
|
1699
|
+
const result = await client.dash.organization.inviteMember(
|
|
1700
|
+
{ email: inviteEmail, role: inviteRole, invitedBy: "" },
|
|
1701
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
1702
|
+
);
|
|
1703
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1704
|
+
return result.data;
|
|
1705
|
+
},
|
|
1706
|
+
onSuccess: () => {
|
|
1707
|
+
qc.invalidateQueries({
|
|
1708
|
+
queryKey: ["dash-org-invitations", organizationId]
|
|
1709
|
+
});
|
|
1710
|
+
setInviteEmail("");
|
|
1711
|
+
setInviteRole("member");
|
|
1712
|
+
toggleNotification({ type: "success", message: "Invitation sent" });
|
|
1713
|
+
},
|
|
1714
|
+
onError: (err) => {
|
|
1715
|
+
toggleNotification({
|
|
1716
|
+
type: "danger",
|
|
1717
|
+
message: err.message ?? "Failed to invite"
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
const cancelInvitationMutation = useMutation({
|
|
1722
|
+
mutationFn: async (invitationId) => {
|
|
1723
|
+
const result = await client.dash.organization.cancelInvitation(
|
|
1724
|
+
{ invitationId },
|
|
1725
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
1726
|
+
);
|
|
1727
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1728
|
+
},
|
|
1729
|
+
onSuccess: () => {
|
|
1730
|
+
setConfirmCancelInvitationId(null);
|
|
1731
|
+
qc.invalidateQueries({
|
|
1732
|
+
queryKey: ["dash-org-invitations", organizationId]
|
|
1733
|
+
});
|
|
1734
|
+
toggleNotification({ type: "success", message: "Invitation cancelled" });
|
|
1735
|
+
},
|
|
1736
|
+
onError: (err) => {
|
|
1737
|
+
toggleNotification({
|
|
1738
|
+
type: "danger",
|
|
1739
|
+
message: err.message ?? "Failed to cancel"
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
const resendInvitationMutation = useMutation({
|
|
1744
|
+
mutationFn: async (invitationId) => {
|
|
1745
|
+
const result = await client.dash.organization.resendInvitation(
|
|
1746
|
+
{ invitationId },
|
|
1747
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
1748
|
+
);
|
|
1749
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1750
|
+
},
|
|
1751
|
+
onSuccess: () => {
|
|
1752
|
+
toggleNotification({ type: "success", message: "Invitation resent" });
|
|
1753
|
+
},
|
|
1754
|
+
onError: (err) => {
|
|
1755
|
+
toggleNotification({
|
|
1756
|
+
type: "danger",
|
|
1757
|
+
message: err.message ?? "Failed to resend"
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
const members = membersQuery.data ?? [];
|
|
1762
|
+
const teams = teamsQuery.data ?? [];
|
|
1117
1763
|
const ssoProviders = ssoQuery.data ?? [];
|
|
1764
|
+
const invitations = invitationsQuery.data ?? [];
|
|
1118
1765
|
const hasOrgEdits = editName !== void 0 || editSlug !== void 0 || editLogo !== void 0 || Object.keys(editExtra).length > 0;
|
|
1119
1766
|
const customFields = Object.entries(schemaQuery.data ?? {}).filter(([name]) => !STANDARD_ORG_FIELDS.has(name)).map(([name, attribute]) => ({ name, attribute }));
|
|
1120
1767
|
const detailsFooter = activeTab === "details" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -1171,6 +1818,11 @@ function OrganizationDetail({
|
|
|
1171
1818
|
teams.length,
|
|
1172
1819
|
")"
|
|
1173
1820
|
] }),
|
|
1821
|
+
/* @__PURE__ */ jsxs(Tabs.Trigger, { value: "invitations", children: [
|
|
1822
|
+
"Invitations (",
|
|
1823
|
+
invitations.length,
|
|
1824
|
+
")"
|
|
1825
|
+
] }),
|
|
1174
1826
|
/* @__PURE__ */ jsxs(Tabs.Trigger, { value: "sso", children: [
|
|
1175
1827
|
"SSO (",
|
|
1176
1828
|
ssoProviders.length,
|
|
@@ -1225,33 +1877,43 @@ function OrganizationDetail({
|
|
|
1225
1877
|
}
|
|
1226
1878
|
)
|
|
1227
1879
|
] }),
|
|
1228
|
-
/* @__PURE__ */
|
|
1229
|
-
/* @__PURE__ */
|
|
1230
|
-
|
|
1231
|
-
|
|
1880
|
+
/* @__PURE__ */ jsxs(EditSidebar, { children: [
|
|
1881
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
1882
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Details" }),
|
|
1883
|
+
/* @__PURE__ */ jsxs(
|
|
1884
|
+
"div",
|
|
1885
|
+
{
|
|
1886
|
+
style: {
|
|
1887
|
+
display: "grid",
|
|
1888
|
+
gridTemplateColumns: "1fr 1fr",
|
|
1889
|
+
gap: 12
|
|
1890
|
+
},
|
|
1891
|
+
children: [
|
|
1892
|
+
/* @__PURE__ */ jsxs(MetaItem, { style: { gridColumn: "1 / -1" }, children: [
|
|
1893
|
+
/* @__PURE__ */ jsx(MetaKey, { children: "Organization ID" }),
|
|
1894
|
+
/* @__PURE__ */ jsx(MonoChip, { children: org?.id })
|
|
1895
|
+
] }),
|
|
1896
|
+
/* @__PURE__ */ jsxs(MetaItem, { children: [
|
|
1897
|
+
/* @__PURE__ */ jsx(MetaKey, { children: "Members" }),
|
|
1898
|
+
/* @__PURE__ */ jsx(MetaVal, { children: org?.memberCount ?? 0 })
|
|
1899
|
+
] }),
|
|
1900
|
+
/* @__PURE__ */ jsxs(MetaItem, { children: [
|
|
1901
|
+
/* @__PURE__ */ jsx(MetaKey, { children: "Created" }),
|
|
1902
|
+
/* @__PURE__ */ jsx(MetaVal, { children: org?.createdAt ? new Date(org.createdAt).toLocaleDateString() : "—" })
|
|
1903
|
+
] })
|
|
1904
|
+
]
|
|
1905
|
+
}
|
|
1906
|
+
)
|
|
1907
|
+
] }),
|
|
1908
|
+
/* @__PURE__ */ jsx(
|
|
1909
|
+
EditViewSidePanels,
|
|
1232
1910
|
{
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
gap: 12
|
|
1237
|
-
},
|
|
1238
|
-
children: [
|
|
1239
|
-
/* @__PURE__ */ jsxs(MetaItem, { style: { gridColumn: "1 / -1" }, children: [
|
|
1240
|
-
/* @__PURE__ */ jsx(MetaKey, { children: "Organization ID" }),
|
|
1241
|
-
/* @__PURE__ */ jsx(MonoChip, { children: org?.id })
|
|
1242
|
-
] }),
|
|
1243
|
-
/* @__PURE__ */ jsxs(MetaItem, { children: [
|
|
1244
|
-
/* @__PURE__ */ jsx(MetaKey, { children: "Members" }),
|
|
1245
|
-
/* @__PURE__ */ jsx(MetaVal, { children: org?.memberCount ?? 0 })
|
|
1246
|
-
] }),
|
|
1247
|
-
/* @__PURE__ */ jsxs(MetaItem, { children: [
|
|
1248
|
-
/* @__PURE__ */ jsx(MetaKey, { children: "Created" }),
|
|
1249
|
-
/* @__PURE__ */ jsx(MetaVal, { children: org?.createdAt ? new Date(org.createdAt).toLocaleDateString() : "—" })
|
|
1250
|
-
] })
|
|
1251
|
-
]
|
|
1911
|
+
model: "plugin::better-auth.organization",
|
|
1912
|
+
documentId: strapiOrgQuery.data?.documentId,
|
|
1913
|
+
document: strapiOrgQuery.data ?? void 0
|
|
1252
1914
|
}
|
|
1253
1915
|
)
|
|
1254
|
-
] })
|
|
1916
|
+
] })
|
|
1255
1917
|
] }) }),
|
|
1256
1918
|
/* @__PURE__ */ jsx(Tabs.Content, { value: "members", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
|
|
1257
1919
|
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
@@ -1426,7 +2088,127 @@ function OrganizationDetail({
|
|
|
1426
2088
|
},
|
|
1427
2089
|
provider.id
|
|
1428
2090
|
)) })
|
|
1429
|
-
] }) }) })
|
|
2091
|
+
] }) }) }),
|
|
2092
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "invitations", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
|
|
2093
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
2094
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Invite member by email" }),
|
|
2095
|
+
/* @__PURE__ */ jsxs(Grid.Root, { gap: 3, children: [
|
|
2096
|
+
/* @__PURE__ */ jsx(Grid.Item, { col: 8, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
|
|
2097
|
+
/* @__PURE__ */ jsx(Field.Label, { children: "Email address" }),
|
|
2098
|
+
/* @__PURE__ */ jsx(
|
|
2099
|
+
TextInput,
|
|
2100
|
+
{
|
|
2101
|
+
type: "email",
|
|
2102
|
+
value: inviteEmail,
|
|
2103
|
+
onChange: (e) => setInviteEmail(e.target.value),
|
|
2104
|
+
placeholder: "user@example.com"
|
|
2105
|
+
}
|
|
2106
|
+
)
|
|
2107
|
+
] }) }),
|
|
2108
|
+
/* @__PURE__ */ jsx(Grid.Item, { col: 4, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
|
|
2109
|
+
/* @__PURE__ */ jsx(Field.Label, { children: "Role" }),
|
|
2110
|
+
/* @__PURE__ */ jsxs(
|
|
2111
|
+
SingleSelect,
|
|
2112
|
+
{
|
|
2113
|
+
value: inviteRole,
|
|
2114
|
+
onChange: (v) => setInviteRole(String(v)),
|
|
2115
|
+
"aria-label": "Invite role",
|
|
2116
|
+
children: [
|
|
2117
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "member", children: "Member" }),
|
|
2118
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "admin", children: "Admin" }),
|
|
2119
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "owner", children: "Owner" })
|
|
2120
|
+
]
|
|
2121
|
+
}
|
|
2122
|
+
)
|
|
2123
|
+
] }) })
|
|
2124
|
+
] }),
|
|
2125
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
|
|
2126
|
+
Button,
|
|
2127
|
+
{
|
|
2128
|
+
size: "S",
|
|
2129
|
+
startIcon: /* @__PURE__ */ jsx(Plus, {}),
|
|
2130
|
+
disabled: !inviteEmail,
|
|
2131
|
+
loading: inviteMemberMutation.isLoading,
|
|
2132
|
+
onClick: () => inviteMemberMutation.mutate(),
|
|
2133
|
+
children: "Send invitation"
|
|
2134
|
+
}
|
|
2135
|
+
) })
|
|
2136
|
+
] }),
|
|
2137
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
2138
|
+
/* @__PURE__ */ jsxs(SectionLabel, { children: [
|
|
2139
|
+
"Invitations (",
|
|
2140
|
+
invitations.length,
|
|
2141
|
+
")"
|
|
2142
|
+
] }),
|
|
2143
|
+
invitationsQuery.isLoading ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral500", children: "Loading…" }) : invitations.length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "No invitations yet." }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: invitations.map((inv) => {
|
|
2144
|
+
const statusColor = {
|
|
2145
|
+
pending: "#f59e0b",
|
|
2146
|
+
accepted: "#5cb176",
|
|
2147
|
+
rejected: "#d02b20",
|
|
2148
|
+
canceled: "#8e8ea9"
|
|
2149
|
+
};
|
|
2150
|
+
const color = statusColor[inv.status] ?? "#8e8ea9";
|
|
2151
|
+
const isPending = inv.status === "pending";
|
|
2152
|
+
return /* @__PURE__ */ jsxs(AccountRow, { children: [
|
|
2153
|
+
/* @__PURE__ */ jsxs(
|
|
2154
|
+
Flex,
|
|
2155
|
+
{
|
|
2156
|
+
direction: "column",
|
|
2157
|
+
gap: 1,
|
|
2158
|
+
alignItems: "flex-start",
|
|
2159
|
+
style: { flex: 1 },
|
|
2160
|
+
children: [
|
|
2161
|
+
/* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: inv.user?.name ?? inv.email }),
|
|
2162
|
+
/* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
|
|
2163
|
+
inv.email,
|
|
2164
|
+
" · role: ",
|
|
2165
|
+
inv.role
|
|
2166
|
+
] }),
|
|
2167
|
+
/* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
|
|
2168
|
+
"Expires",
|
|
2169
|
+
" ",
|
|
2170
|
+
new Date(inv.expiresAt).toLocaleDateString()
|
|
2171
|
+
] })
|
|
2172
|
+
]
|
|
2173
|
+
}
|
|
2174
|
+
),
|
|
2175
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
|
|
2176
|
+
/* @__PURE__ */ jsx(
|
|
2177
|
+
MonoChip,
|
|
2178
|
+
{
|
|
2179
|
+
style: {
|
|
2180
|
+
color,
|
|
2181
|
+
borderColor: color,
|
|
2182
|
+
background: `${color}18`
|
|
2183
|
+
},
|
|
2184
|
+
children: inv.status
|
|
2185
|
+
}
|
|
2186
|
+
),
|
|
2187
|
+
isPending && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2188
|
+
/* @__PURE__ */ jsx(
|
|
2189
|
+
Button,
|
|
2190
|
+
{
|
|
2191
|
+
size: "S",
|
|
2192
|
+
variant: "secondary",
|
|
2193
|
+
loading: resendInvitationMutation.isLoading,
|
|
2194
|
+
onClick: () => resendInvitationMutation.mutate(inv.id),
|
|
2195
|
+
children: "Resend"
|
|
2196
|
+
}
|
|
2197
|
+
),
|
|
2198
|
+
/* @__PURE__ */ jsx(
|
|
2199
|
+
IconButton,
|
|
2200
|
+
{
|
|
2201
|
+
label: "Cancel invitation",
|
|
2202
|
+
onClick: () => setConfirmCancelInvitationId(inv.id),
|
|
2203
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
2204
|
+
}
|
|
2205
|
+
)
|
|
2206
|
+
] })
|
|
2207
|
+
] })
|
|
2208
|
+
] }, inv.id);
|
|
2209
|
+
}) })
|
|
2210
|
+
] })
|
|
2211
|
+
] }) })
|
|
1430
2212
|
] }),
|
|
1431
2213
|
confirmRemoveMemberId && /* @__PURE__ */ jsx(
|
|
1432
2214
|
ConfirmDialog,
|
|
@@ -1460,6 +2242,17 @@ function OrganizationDetail({
|
|
|
1460
2242
|
onConfirm: () => deleteSsoMutation.mutate(confirmDeleteSsoId),
|
|
1461
2243
|
onCancel: () => setConfirmDeleteSsoId(null)
|
|
1462
2244
|
}
|
|
2245
|
+
),
|
|
2246
|
+
confirmCancelInvitationId && /* @__PURE__ */ jsx(
|
|
2247
|
+
ConfirmDialog,
|
|
2248
|
+
{
|
|
2249
|
+
title: "Cancel invitation",
|
|
2250
|
+
message: "Are you sure you want to cancel this invitation? The invite link will no longer work.",
|
|
2251
|
+
confirmLabel: "Cancel invitation",
|
|
2252
|
+
loading: cancelInvitationMutation.isLoading,
|
|
2253
|
+
onConfirm: () => cancelInvitationMutation.mutate(confirmCancelInvitationId),
|
|
2254
|
+
onCancel: () => setConfirmCancelInvitationId(null)
|
|
2255
|
+
}
|
|
1463
2256
|
)
|
|
1464
2257
|
]
|
|
1465
2258
|
}
|
|
@@ -1481,7 +2274,7 @@ function TeamRow({
|
|
|
1481
2274
|
organizationId
|
|
1482
2275
|
].teams[team.id].members(
|
|
1483
2276
|
{ params: { orgId: organizationId, teamId: team.id } },
|
|
1484
|
-
withContext({ organizationId })
|
|
2277
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
1485
2278
|
);
|
|
1486
2279
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1487
2280
|
return result.data ?? [];
|
|
@@ -1493,7 +2286,7 @@ function TeamRow({
|
|
|
1493
2286
|
mutationFn: async () => {
|
|
1494
2287
|
const result = await client.dash.organization.addTeamMember(
|
|
1495
2288
|
{ teamId: team.id, userId: addUserId },
|
|
1496
|
-
withContext({ organizationId })
|
|
2289
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
1497
2290
|
);
|
|
1498
2291
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1499
2292
|
return result.data;
|
|
@@ -1509,7 +2302,7 @@ function TeamRow({
|
|
|
1509
2302
|
mutationFn: async (userId) => {
|
|
1510
2303
|
const result = await client.dash.organization.removeTeamMember(
|
|
1511
2304
|
{ teamId: team.id, userId },
|
|
1512
|
-
withContext({ organizationId })
|
|
2305
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
1513
2306
|
);
|
|
1514
2307
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1515
2308
|
},
|
|
@@ -1605,12 +2398,12 @@ function TeamRow({
|
|
|
1605
2398
|
)
|
|
1606
2399
|
] });
|
|
1607
2400
|
}
|
|
1608
|
-
const PAGE_SIZE$
|
|
1609
|
-
const fadeUp$
|
|
2401
|
+
const PAGE_SIZE$1 = 25;
|
|
2402
|
+
const fadeUp$2 = keyframes`
|
|
1610
2403
|
from { opacity: 0; transform: translateY(6px); }
|
|
1611
2404
|
to { opacity: 1; transform: translateY(0); }
|
|
1612
2405
|
`;
|
|
1613
|
-
const Wrap$
|
|
2406
|
+
const Wrap$2 = styled.div`
|
|
1614
2407
|
padding: 28px 32px;
|
|
1615
2408
|
background: #f6f6f9;
|
|
1616
2409
|
min-height: 100%;
|
|
@@ -1618,39 +2411,39 @@ const Wrap$3 = styled.div`
|
|
|
1618
2411
|
flex-direction: column;
|
|
1619
2412
|
gap: 20px;
|
|
1620
2413
|
`;
|
|
1621
|
-
const PageHeader$
|
|
2414
|
+
const PageHeader$1 = styled.div`
|
|
1622
2415
|
display: flex;
|
|
1623
2416
|
justify-content: space-between;
|
|
1624
2417
|
align-items: flex-start;
|
|
1625
2418
|
`;
|
|
1626
|
-
const TitleBlock$
|
|
2419
|
+
const TitleBlock$1 = styled.div`
|
|
1627
2420
|
display: flex;
|
|
1628
2421
|
flex-direction: column;
|
|
1629
2422
|
gap: 4px;
|
|
1630
2423
|
`;
|
|
1631
|
-
const PageTitle$
|
|
2424
|
+
const PageTitle$1 = styled.h1`
|
|
1632
2425
|
margin: 0;
|
|
1633
2426
|
font-size: 22px;
|
|
1634
2427
|
font-weight: 800;
|
|
1635
2428
|
color: #32324d;
|
|
1636
2429
|
letter-spacing: -0.03em;
|
|
1637
2430
|
`;
|
|
1638
|
-
const PageSubtitle$
|
|
2431
|
+
const PageSubtitle$1 = styled.p`
|
|
1639
2432
|
margin: 0;
|
|
1640
2433
|
font-size: 12px;
|
|
1641
2434
|
color: #8e8ea9;
|
|
1642
2435
|
`;
|
|
1643
|
-
const TableCard$
|
|
2436
|
+
const TableCard$1 = styled.div`
|
|
1644
2437
|
background: #ffffff;
|
|
1645
2438
|
border: 1px solid #eaeaef;
|
|
1646
2439
|
border-radius: 10px;
|
|
1647
2440
|
overflow: hidden;
|
|
1648
2441
|
`;
|
|
1649
|
-
const Table$
|
|
2442
|
+
const Table$1 = styled.table`
|
|
1650
2443
|
width: 100%;
|
|
1651
2444
|
border-collapse: collapse;
|
|
1652
2445
|
`;
|
|
1653
|
-
const TH$
|
|
2446
|
+
const TH$1 = styled.th`
|
|
1654
2447
|
text-align: left;
|
|
1655
2448
|
padding: 10px 14px;
|
|
1656
2449
|
font-size: 10px;
|
|
@@ -1664,13 +2457,13 @@ const TH$2 = styled.th`
|
|
|
1664
2457
|
&:first-child { padding-left: 20px; }
|
|
1665
2458
|
&:last-child { padding-right: 20px; }
|
|
1666
2459
|
`;
|
|
1667
|
-
const THCheck$1 = styled(TH$
|
|
2460
|
+
const THCheck$1 = styled(TH$1)`
|
|
1668
2461
|
width: 44px;
|
|
1669
2462
|
`;
|
|
1670
|
-
const THActions$1 = styled(TH$
|
|
2463
|
+
const THActions$1 = styled(TH$1)`
|
|
1671
2464
|
width: 80px;
|
|
1672
2465
|
`;
|
|
1673
|
-
const TD$
|
|
2466
|
+
const TD$1 = styled.td`
|
|
1674
2467
|
padding: 11px 14px;
|
|
1675
2468
|
font-size: 12px;
|
|
1676
2469
|
color: #32324d;
|
|
@@ -1679,14 +2472,14 @@ const TD$2 = styled.td`
|
|
|
1679
2472
|
&:first-child { padding-left: 20px; }
|
|
1680
2473
|
&:last-child { padding-right: 20px; }
|
|
1681
2474
|
`;
|
|
1682
|
-
const TDCheck$1 = styled(TD$
|
|
2475
|
+
const TDCheck$1 = styled(TD$1)`
|
|
1683
2476
|
width: 44px;
|
|
1684
2477
|
`;
|
|
1685
|
-
const TDActions$1 = styled(TD$
|
|
2478
|
+
const TDActions$1 = styled(TD$1)`
|
|
1686
2479
|
width: 80px;
|
|
1687
2480
|
`;
|
|
1688
|
-
const TR$
|
|
1689
|
-
animation: ${fadeUp$
|
|
2481
|
+
const TR$1 = styled.tr`
|
|
2482
|
+
animation: ${fadeUp$2} 280ms ease both;
|
|
1690
2483
|
animation-delay: ${(p) => (p.$i ?? 0) * 25}ms;
|
|
1691
2484
|
background: ${(p) => p.$selected ? "#f0f0ff" : "transparent"};
|
|
1692
2485
|
transition: background 120ms ease;
|
|
@@ -1773,7 +2566,9 @@ function OrgAvatar({ name, logo }) {
|
|
|
1773
2566
|
return /* @__PURE__ */ jsx(OrgLogoFallback, { $bg: color.bg, $fg: color.fg, children: initials });
|
|
1774
2567
|
}
|
|
1775
2568
|
function OrganizationsPage({ teamsEnabled }) {
|
|
2569
|
+
const rbac = useRBAC(PERMISSIONS.organization);
|
|
1776
2570
|
const qc = useQueryClient();
|
|
2571
|
+
const { toggleNotification } = useNotification();
|
|
1777
2572
|
const [page, setPage] = useState(1);
|
|
1778
2573
|
const [search, setSearch] = useState("");
|
|
1779
2574
|
const [searchInput, setSearchInput] = useState("");
|
|
@@ -1782,13 +2577,13 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1782
2577
|
const [detailOrgId, setDetailOrgId] = useState(null);
|
|
1783
2578
|
const [confirmDelete, setConfirmDelete] = useState(null);
|
|
1784
2579
|
const [confirmDeleteMany, setConfirmDeleteMany] = useState(false);
|
|
1785
|
-
const offset = (page - 1) * PAGE_SIZE$
|
|
2580
|
+
const offset = (page - 1) * PAGE_SIZE$1;
|
|
1786
2581
|
const orgsQuery = useQuery({
|
|
1787
2582
|
queryKey: ["dash-organizations", page, search],
|
|
1788
2583
|
queryFn: async () => {
|
|
1789
2584
|
const result = await client.dash.listOrganizations({
|
|
1790
2585
|
query: {
|
|
1791
|
-
limit: PAGE_SIZE$
|
|
2586
|
+
limit: PAGE_SIZE$1,
|
|
1792
2587
|
offset,
|
|
1793
2588
|
sortBy: "createdAt",
|
|
1794
2589
|
sortOrder: "desc",
|
|
@@ -1805,7 +2600,7 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1805
2600
|
mutationFn: async (organizationId) => {
|
|
1806
2601
|
const result = await client.dash.organization.delete(
|
|
1807
2602
|
{ organizationId },
|
|
1808
|
-
withContext({ organizationId })
|
|
2603
|
+
withContext({ organizationId }, getAuthHeaders())
|
|
1809
2604
|
);
|
|
1810
2605
|
if (result.error)
|
|
1811
2606
|
throw new Error(result.error.message ?? "Delete failed");
|
|
@@ -1813,27 +2608,44 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1813
2608
|
onSuccess: () => {
|
|
1814
2609
|
setConfirmDelete(null);
|
|
1815
2610
|
qc.invalidateQueries({ queryKey: ["dash-organizations"] });
|
|
2611
|
+
toggleNotification({ type: "success", message: "Organization deleted" });
|
|
2612
|
+
},
|
|
2613
|
+
onError: (err) => {
|
|
2614
|
+
toggleNotification({
|
|
2615
|
+
type: "danger",
|
|
2616
|
+
message: err.message ?? "Failed to delete organization"
|
|
2617
|
+
});
|
|
1816
2618
|
}
|
|
1817
2619
|
});
|
|
1818
2620
|
const deleteManyMutation = useMutation({
|
|
1819
2621
|
mutationFn: async (organizationIds) => {
|
|
1820
2622
|
const result = await client.dash.organization.deleteMany(
|
|
1821
2623
|
{},
|
|
1822
|
-
withContext({ organizationIds })
|
|
2624
|
+
withContext({ organizationIds }, getAuthHeaders())
|
|
1823
2625
|
);
|
|
1824
2626
|
if (result.error)
|
|
1825
2627
|
throw new Error(result.error.message ?? "Delete failed");
|
|
1826
2628
|
return result.data;
|
|
1827
2629
|
},
|
|
1828
|
-
onSuccess: () => {
|
|
2630
|
+
onSuccess: (_data, organizationIds) => {
|
|
1829
2631
|
setConfirmDeleteMany(false);
|
|
1830
2632
|
setSelected(/* @__PURE__ */ new Set());
|
|
1831
2633
|
qc.invalidateQueries({ queryKey: ["dash-organizations"] });
|
|
2634
|
+
toggleNotification({
|
|
2635
|
+
type: "success",
|
|
2636
|
+
message: `${organizationIds.length} organization${organizationIds.length !== 1 ? "s" : ""} deleted`
|
|
2637
|
+
});
|
|
2638
|
+
},
|
|
2639
|
+
onError: (err) => {
|
|
2640
|
+
toggleNotification({
|
|
2641
|
+
type: "danger",
|
|
2642
|
+
message: err.message ?? "Failed to delete organizations"
|
|
2643
|
+
});
|
|
1832
2644
|
}
|
|
1833
2645
|
});
|
|
1834
2646
|
const orgs = orgsQuery.data && "organizations" in orgsQuery.data ? orgsQuery.data.organizations : [];
|
|
1835
2647
|
const total = orgsQuery.data && "total" in orgsQuery.data ? orgsQuery.data.total : 0;
|
|
1836
|
-
const pageCount = Math.ceil(total / PAGE_SIZE$
|
|
2648
|
+
const pageCount = Math.ceil(total / PAGE_SIZE$1);
|
|
1837
2649
|
const allSelected = orgs.length > 0 && orgs.every((o) => selected.has(o.id));
|
|
1838
2650
|
const someSelected = selected.size > 0;
|
|
1839
2651
|
const toggleSelect = (id) => {
|
|
@@ -1853,210 +2665,218 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1853
2665
|
setSearch(searchInput);
|
|
1854
2666
|
setPage(1);
|
|
1855
2667
|
};
|
|
1856
|
-
return /* @__PURE__ */
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
) }),
|
|
1918
|
-
/* @__PURE__ */ jsx(TH$2, { children: "Name" }),
|
|
1919
|
-
/* @__PURE__ */ jsx(TH$2, { children: "Slug" }),
|
|
1920
|
-
/* @__PURE__ */ jsx(TH$2, { children: "Members" }),
|
|
1921
|
-
/* @__PURE__ */ jsx(TH$2, { children: "Created" }),
|
|
1922
|
-
/* @__PURE__ */ jsx(THActions$1, {})
|
|
1923
|
-
] }) }),
|
|
1924
|
-
/* @__PURE__ */ jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
|
|
1925
|
-
TD$2,
|
|
1926
|
-
{
|
|
1927
|
-
colSpan: 6,
|
|
1928
|
-
style: {
|
|
1929
|
-
textAlign: "center",
|
|
1930
|
-
padding: "40px",
|
|
1931
|
-
color: "#8e8ea9"
|
|
1932
|
-
},
|
|
1933
|
-
"data-testid": "orgs-empty",
|
|
1934
|
-
children: search ? `No organizations matching "${search}"` : "No organizations found"
|
|
1935
|
-
}
|
|
1936
|
-
) }) : orgs.map((org, i) => /* @__PURE__ */ jsxs(
|
|
1937
|
-
TR$2,
|
|
1938
|
-
{
|
|
1939
|
-
$selected: selected.has(org.id),
|
|
1940
|
-
$i: i,
|
|
1941
|
-
"data-testid": "org-row",
|
|
1942
|
-
children: [
|
|
1943
|
-
/* @__PURE__ */ jsx(TDCheck$1, { children: /* @__PURE__ */ jsx(
|
|
2668
|
+
return /* @__PURE__ */ jsx(
|
|
2669
|
+
Page.Protect,
|
|
2670
|
+
{
|
|
2671
|
+
permissions: PERMISSIONS.organization.filter(
|
|
2672
|
+
(p) => p.action === `plugin::${PLUGIN_ID}.organization.read`
|
|
2673
|
+
),
|
|
2674
|
+
children: /* @__PURE__ */ jsxs(Wrap$2, { "data-testid": "organizations-page", children: [
|
|
2675
|
+
/* @__PURE__ */ jsxs(PageHeader$1, { children: [
|
|
2676
|
+
/* @__PURE__ */ jsxs(TitleBlock$1, { children: [
|
|
2677
|
+
/* @__PURE__ */ jsx(PageTitle$1, { children: "Organizations" }),
|
|
2678
|
+
/* @__PURE__ */ jsxs(PageSubtitle$1, { children: [
|
|
2679
|
+
total.toLocaleString(),
|
|
2680
|
+
" total"
|
|
2681
|
+
] })
|
|
2682
|
+
] }),
|
|
2683
|
+
rbac.allowedActions.canCreate && /* @__PURE__ */ jsx(
|
|
2684
|
+
Button,
|
|
2685
|
+
{
|
|
2686
|
+
startIcon: /* @__PURE__ */ jsx(Plus, {}),
|
|
2687
|
+
onClick: () => setShowCreate(true),
|
|
2688
|
+
"data-testid": "create-org-btn",
|
|
2689
|
+
children: "Create organization"
|
|
2690
|
+
}
|
|
2691
|
+
)
|
|
2692
|
+
] }),
|
|
2693
|
+
/* @__PURE__ */ jsxs(Toolbar$1, { children: [
|
|
2694
|
+
/* @__PURE__ */ jsx(SearchForm, { onSubmit: handleSearch, children: /* @__PURE__ */ jsx(
|
|
2695
|
+
Searchbar,
|
|
2696
|
+
{
|
|
2697
|
+
clearLabel: "Clear",
|
|
2698
|
+
name: "search",
|
|
2699
|
+
placeholder: "Search organizations…",
|
|
2700
|
+
value: searchInput,
|
|
2701
|
+
onChange: (e) => setSearchInput(e.target.value),
|
|
2702
|
+
onClear: () => {
|
|
2703
|
+
setSearchInput("");
|
|
2704
|
+
setSearch("");
|
|
2705
|
+
setPage(1);
|
|
2706
|
+
},
|
|
2707
|
+
children: "Search organizations"
|
|
2708
|
+
}
|
|
2709
|
+
) }),
|
|
2710
|
+
someSelected && rbac.allowedActions.canDelete && /* @__PURE__ */ jsxs(
|
|
2711
|
+
Button,
|
|
2712
|
+
{
|
|
2713
|
+
variant: "danger-light",
|
|
2714
|
+
size: "S",
|
|
2715
|
+
onClick: () => setConfirmDeleteMany(true),
|
|
2716
|
+
"data-testid": "delete-selected-orgs-btn",
|
|
2717
|
+
children: [
|
|
2718
|
+
"Delete ",
|
|
2719
|
+
selected.size,
|
|
2720
|
+
" selected"
|
|
2721
|
+
]
|
|
2722
|
+
}
|
|
2723
|
+
)
|
|
2724
|
+
] }),
|
|
2725
|
+
orgsQuery.isError && /* @__PURE__ */ jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: orgsQuery.error instanceof Error ? orgsQuery.error.message : "An error occurred" }),
|
|
2726
|
+
/* @__PURE__ */ jsx(TableCard$1, { children: orgsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading organizations…" }) }) : /* @__PURE__ */ jsxs(Table$1, { children: [
|
|
2727
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
2728
|
+
rbac.allowedActions.canDelete && /* @__PURE__ */ jsx(THCheck$1, { children: /* @__PURE__ */ jsx(
|
|
1944
2729
|
Checkbox,
|
|
1945
2730
|
{
|
|
1946
|
-
checked:
|
|
1947
|
-
onCheckedChange:
|
|
1948
|
-
"aria-label":
|
|
2731
|
+
checked: allSelected,
|
|
2732
|
+
onCheckedChange: handleSelectAll,
|
|
2733
|
+
"aria-label": "Select all"
|
|
1949
2734
|
}
|
|
1950
2735
|
) }),
|
|
1951
|
-
/* @__PURE__ */ jsx(
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
/* @__PURE__ */ jsx(
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2736
|
+
/* @__PURE__ */ jsx(TH$1, { children: "Name" }),
|
|
2737
|
+
/* @__PURE__ */ jsx(TH$1, { children: "Slug" }),
|
|
2738
|
+
/* @__PURE__ */ jsx(TH$1, { children: "Members" }),
|
|
2739
|
+
/* @__PURE__ */ jsx(TH$1, { children: "Created" }),
|
|
2740
|
+
/* @__PURE__ */ jsx(THActions$1, {})
|
|
2741
|
+
] }) }),
|
|
2742
|
+
/* @__PURE__ */ jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
|
|
2743
|
+
TD$1,
|
|
2744
|
+
{
|
|
2745
|
+
colSpan: 6,
|
|
2746
|
+
style: {
|
|
2747
|
+
textAlign: "center",
|
|
2748
|
+
padding: "40px",
|
|
2749
|
+
color: "#8e8ea9"
|
|
2750
|
+
},
|
|
2751
|
+
"data-testid": "orgs-empty",
|
|
2752
|
+
children: search ? `No organizations matching "${search}"` : "No organizations found"
|
|
2753
|
+
}
|
|
2754
|
+
) }) : orgs.map((org, i) => /* @__PURE__ */ jsxs(
|
|
2755
|
+
TR$1,
|
|
2756
|
+
{
|
|
2757
|
+
$selected: selected.has(org.id),
|
|
2758
|
+
$i: i,
|
|
2759
|
+
"data-testid": "org-row",
|
|
2760
|
+
children: [
|
|
2761
|
+
rbac.allowedActions.canDelete && /* @__PURE__ */ jsx(TDCheck$1, { children: /* @__PURE__ */ jsx(
|
|
2762
|
+
Checkbox,
|
|
2763
|
+
{
|
|
2764
|
+
checked: selected.has(org.id),
|
|
2765
|
+
onCheckedChange: () => toggleSelect(org.id),
|
|
2766
|
+
"aria-label": `Select ${org.name}`
|
|
2767
|
+
}
|
|
2768
|
+
) }),
|
|
2769
|
+
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
|
|
2770
|
+
/* @__PURE__ */ jsx(OrgAvatar, { name: org.name, logo: org.logo }),
|
|
2771
|
+
/* @__PURE__ */ jsx(OrgName, { children: org.name })
|
|
2772
|
+
] }) }),
|
|
2773
|
+
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(SlugChip, { children: org.slug }) }),
|
|
2774
|
+
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(CountChip, { children: org.memberCount }) }),
|
|
2775
|
+
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
|
|
2776
|
+
/* @__PURE__ */ jsx(TDActions$1, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", children: [
|
|
2777
|
+
rbac.allowedActions.canUpdate && /* @__PURE__ */ jsx(
|
|
2778
|
+
IconButton,
|
|
2779
|
+
{
|
|
2780
|
+
label: "Edit organization",
|
|
2781
|
+
onClick: () => setDetailOrgId(org.id),
|
|
2782
|
+
"data-testid": "edit-org-btn",
|
|
2783
|
+
children: /* @__PURE__ */ jsx(Pencil, {})
|
|
2784
|
+
}
|
|
2785
|
+
),
|
|
2786
|
+
rbac.allowedActions.canDelete && /* @__PURE__ */ jsx(
|
|
2787
|
+
IconButton,
|
|
2788
|
+
{
|
|
2789
|
+
label: "Delete organization",
|
|
2790
|
+
onClick: () => setConfirmDelete(org.id),
|
|
2791
|
+
"data-testid": "delete-org-btn",
|
|
2792
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
2793
|
+
}
|
|
2794
|
+
)
|
|
2795
|
+
] }) })
|
|
2796
|
+
]
|
|
2797
|
+
},
|
|
2798
|
+
org.id
|
|
2799
|
+
)) })
|
|
2800
|
+
] }) }),
|
|
2801
|
+
pageCount > 1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
2802
|
+
/* @__PURE__ */ jsx(
|
|
2803
|
+
Button,
|
|
2804
|
+
{
|
|
2805
|
+
variant: "tertiary",
|
|
2806
|
+
size: "S",
|
|
2807
|
+
disabled: page === 1,
|
|
2808
|
+
onClick: () => setPage((p) => p - 1),
|
|
2809
|
+
children: "Previous"
|
|
2810
|
+
}
|
|
2811
|
+
),
|
|
2812
|
+
/* @__PURE__ */ jsx(
|
|
2813
|
+
Button,
|
|
2814
|
+
{
|
|
2815
|
+
variant: "tertiary",
|
|
2816
|
+
size: "S",
|
|
2817
|
+
disabled: page >= pageCount,
|
|
2818
|
+
onClick: () => setPage((p) => p + 1),
|
|
2819
|
+
children: "Next"
|
|
2820
|
+
}
|
|
2821
|
+
)
|
|
2822
|
+
] }) }),
|
|
2823
|
+
rbac.allowedActions.canCreate && showCreate && /* @__PURE__ */ jsx(
|
|
2824
|
+
CreateOrganizationDialog,
|
|
2825
|
+
{
|
|
2826
|
+
teamsEnabled,
|
|
2827
|
+
onClose: () => setShowCreate(false)
|
|
2828
|
+
}
|
|
2829
|
+
),
|
|
2830
|
+
rbac.allowedActions.canUpdate && detailOrgId && /* @__PURE__ */ jsx(
|
|
2831
|
+
OrganizationDetail,
|
|
2832
|
+
{
|
|
2833
|
+
organizationId: detailOrgId,
|
|
2834
|
+
teamsEnabled,
|
|
2835
|
+
onClose: () => setDetailOrgId(null)
|
|
2836
|
+
}
|
|
2837
|
+
),
|
|
2838
|
+
rbac.allowedActions.canDelete && confirmDelete && /* @__PURE__ */ jsx(
|
|
2839
|
+
ConfirmDialog,
|
|
2840
|
+
{
|
|
2841
|
+
title: "Delete organization",
|
|
2842
|
+
message: "Are you sure you want to delete this organization? All members and teams will be removed. This action cannot be undone.",
|
|
2843
|
+
confirmLabel: "Delete",
|
|
2844
|
+
loading: deleteMutation.isLoading,
|
|
2845
|
+
onConfirm: () => deleteMutation.mutate(confirmDelete),
|
|
2846
|
+
onCancel: () => setConfirmDelete(null)
|
|
2847
|
+
}
|
|
2848
|
+
),
|
|
2849
|
+
rbac.allowedActions.canDelete && confirmDeleteMany && /* @__PURE__ */ jsx(
|
|
2850
|
+
ConfirmDialog,
|
|
2851
|
+
{
|
|
2852
|
+
title: `Delete ${selected.size} organization${selected.size !== 1 ? "s" : ""}`,
|
|
2853
|
+
message: `Are you sure you want to delete ${selected.size} organization${selected.size !== 1 ? "s" : ""}? This action cannot be undone.`,
|
|
2854
|
+
confirmLabel: "Delete all",
|
|
2855
|
+
loading: deleteManyMutation.isLoading,
|
|
2856
|
+
onConfirm: () => deleteManyMutation.mutate([...selected]),
|
|
2857
|
+
onCancel: () => setConfirmDeleteMany(false)
|
|
2858
|
+
}
|
|
2859
|
+
)
|
|
2860
|
+
] })
|
|
2861
|
+
}
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2864
|
+
const PALETTE = [
|
|
2865
|
+
{ bg: "#EAF5FF", fg: "#0C75AF" },
|
|
2866
|
+
{ bg: "#F0F0FF", fg: "#4945FF" },
|
|
2867
|
+
{ bg: "#EAFBE7", fg: "#328048" },
|
|
2868
|
+
{ bg: "#FFF3D3", fg: "#8E6A00" },
|
|
2869
|
+
{ bg: "#FCECEA", fg: "#D02B20" },
|
|
2870
|
+
{ bg: "#F6ECFC", fg: "#8312D1" },
|
|
2871
|
+
{ bg: "#E8F8F5", fg: "#1A7F64" }
|
|
2872
|
+
];
|
|
2873
|
+
const Circle = styled.div`
|
|
2874
|
+
width: ${(p) => p.$size}px;
|
|
2875
|
+
height: ${(p) => p.$size}px;
|
|
2876
|
+
border-radius: 50%;
|
|
2877
|
+
background: ${(p) => p.$bg};
|
|
2878
|
+
color: ${(p) => p.$fg};
|
|
2879
|
+
display: inline-flex;
|
|
2060
2880
|
align-items: center;
|
|
2061
2881
|
justify-content: center;
|
|
2062
2882
|
font-size: ${(p) => Math.round(p.$size * 0.38)}px;
|
|
@@ -2101,95 +2921,143 @@ function Avatar({
|
|
|
2101
2921
|
const color = getColor(name || "?");
|
|
2102
2922
|
return /* @__PURE__ */ jsx(Circle, { $bg: color.bg, $fg: color.fg, $size: size, children: getInitials(name || "?") });
|
|
2103
2923
|
}
|
|
2104
|
-
const
|
|
2105
|
-
|
|
2924
|
+
const T = {
|
|
2925
|
+
bg: "#f6f6f9",
|
|
2926
|
+
bgCard: "#ffffff",
|
|
2927
|
+
border: "#eaeaef",
|
|
2928
|
+
borderHover: "rgba(73,69,255,0.35)",
|
|
2929
|
+
accent: "#4945ff",
|
|
2930
|
+
green: "#5cb176",
|
|
2931
|
+
greenDim: "rgba(92,177,118,0.12)",
|
|
2932
|
+
amber: "#d9822f",
|
|
2933
|
+
red: "#d02b20",
|
|
2934
|
+
redDim: "rgba(208,43,32,0.1)",
|
|
2935
|
+
purple: "#8460b8",
|
|
2936
|
+
textPrimary: "#32324d",
|
|
2937
|
+
textSecondary: "#666687",
|
|
2938
|
+
textMuted: "#b8b8c7",
|
|
2939
|
+
mono: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`
|
|
2940
|
+
};
|
|
2941
|
+
const fadeUp$1 = keyframes`
|
|
2942
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
2106
2943
|
to { opacity: 1; transform: translateY(0); }
|
|
2107
2944
|
`;
|
|
2108
|
-
const Wrap$
|
|
2109
|
-
padding: 28px 32px;
|
|
2110
|
-
background:
|
|
2945
|
+
const Wrap$1 = styled.div`
|
|
2946
|
+
padding: 28px 32px 48px;
|
|
2947
|
+
background: ${T.bg};
|
|
2111
2948
|
min-height: 100%;
|
|
2112
2949
|
display: flex;
|
|
2113
2950
|
flex-direction: column;
|
|
2114
|
-
gap:
|
|
2951
|
+
gap: 22px;
|
|
2115
2952
|
`;
|
|
2116
|
-
const
|
|
2117
|
-
display:
|
|
2118
|
-
|
|
2119
|
-
gap:
|
|
2120
|
-
|
|
2121
|
-
@media (max-width: 768px) { grid-template-columns: repeat(2, 1fr); }
|
|
2953
|
+
const SectionDivider = styled.div`
|
|
2954
|
+
display: flex;
|
|
2955
|
+
align-items: center;
|
|
2956
|
+
gap: 10px;
|
|
2957
|
+
margin-bottom: -6px;
|
|
2122
2958
|
`;
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2959
|
+
const DivLabel = styled.span`
|
|
2960
|
+
font-size: 9px;
|
|
2961
|
+
font-weight: 700;
|
|
2962
|
+
letter-spacing: 0.14em;
|
|
2963
|
+
text-transform: uppercase;
|
|
2964
|
+
color: ${T.textMuted};
|
|
2965
|
+
white-space: nowrap;
|
|
2128
2966
|
`;
|
|
2129
|
-
const
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2967
|
+
const DivLine = styled.div`
|
|
2968
|
+
flex: 1;
|
|
2969
|
+
height: 1px;
|
|
2970
|
+
background: ${T.border};
|
|
2133
2971
|
`;
|
|
2134
2972
|
const Card = styled.div`
|
|
2135
|
-
background:
|
|
2136
|
-
border: 1px solid
|
|
2137
|
-
border-radius:
|
|
2973
|
+
background: ${T.bgCard};
|
|
2974
|
+
border: 1px solid ${T.border};
|
|
2975
|
+
border-radius: 12px;
|
|
2138
2976
|
overflow: hidden;
|
|
2139
2977
|
position: relative;
|
|
2140
|
-
animation: ${fadeUp$
|
|
2978
|
+
animation: ${fadeUp$1} 380ms ease both;
|
|
2141
2979
|
animation-delay: ${(p) => (p.$delay ?? 0) * 55}ms;
|
|
2142
|
-
transition: border-color
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2980
|
+
transition: border-color 200ms, box-shadow 200ms;
|
|
2981
|
+
&:hover {
|
|
2982
|
+
border-color: ${T.borderHover};
|
|
2983
|
+
box-shadow: 0 0 0 1px ${T.borderHover}, 0 8px 32px rgba(124,109,250,0.07);
|
|
2984
|
+
}
|
|
2985
|
+
`;
|
|
2986
|
+
const PillGroup = styled.div`
|
|
2987
|
+
display: flex;
|
|
2988
|
+
background: rgba(0,0,0,0.03);
|
|
2989
|
+
border: 1px solid ${T.border};
|
|
2990
|
+
border-radius: 9px;
|
|
2991
|
+
padding: 3px;
|
|
2992
|
+
gap: 2px;
|
|
2993
|
+
`;
|
|
2994
|
+
const Pill = styled.button`
|
|
2995
|
+
appearance: none;
|
|
2996
|
+
border: none;
|
|
2997
|
+
cursor: pointer;
|
|
2998
|
+
padding: 5px 16px;
|
|
2999
|
+
border-radius: 6px;
|
|
3000
|
+
font-size: 11px;
|
|
3001
|
+
font-weight: 600;
|
|
3002
|
+
letter-spacing: 0.03em;
|
|
3003
|
+
transition: background 180ms, color 180ms;
|
|
3004
|
+
background: ${(p) => p.$active ? T.accent : "transparent"};
|
|
3005
|
+
color: ${(p) => p.$active ? "#fff" : T.textSecondary};
|
|
2155
3006
|
&:hover {
|
|
2156
|
-
|
|
2157
|
-
|
|
3007
|
+
background: ${(p) => p.$active ? T.accent : "rgba(0,0,0,0.05)"};
|
|
3008
|
+
color: ${(p) => p.$active ? "#fff" : T.textPrimary};
|
|
2158
3009
|
}
|
|
2159
3010
|
`;
|
|
2160
|
-
const
|
|
2161
|
-
|
|
3011
|
+
const StatGrid = styled.div`
|
|
3012
|
+
display: grid;
|
|
3013
|
+
grid-template-columns: repeat(5, 1fr);
|
|
3014
|
+
gap: 10px;
|
|
3015
|
+
@media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
|
|
3016
|
+
`;
|
|
3017
|
+
const StatCard = styled(Card)`
|
|
3018
|
+
padding: 18px 20px 0;
|
|
3019
|
+
&::after {
|
|
3020
|
+
content: '';
|
|
3021
|
+
position: absolute;
|
|
3022
|
+
top: 0; left: 0; right: 0;
|
|
3023
|
+
height: 2px;
|
|
3024
|
+
background: ${(p) => p.$accent};
|
|
3025
|
+
opacity: 0.75;
|
|
3026
|
+
}
|
|
2162
3027
|
`;
|
|
2163
3028
|
const StatLabel = styled.div`
|
|
2164
|
-
font-size:
|
|
3029
|
+
font-size: 9px;
|
|
2165
3030
|
font-weight: 700;
|
|
2166
|
-
letter-spacing: 0.
|
|
3031
|
+
letter-spacing: 0.1em;
|
|
2167
3032
|
text-transform: uppercase;
|
|
2168
|
-
color:
|
|
2169
|
-
margin-bottom:
|
|
3033
|
+
color: ${T.textMuted};
|
|
3034
|
+
margin-bottom: 10px;
|
|
2170
3035
|
`;
|
|
2171
3036
|
const StatValue = styled.div`
|
|
2172
3037
|
font-size: 30px;
|
|
2173
3038
|
font-weight: 800;
|
|
2174
|
-
color:
|
|
3039
|
+
color: ${T.textPrimary};
|
|
2175
3040
|
line-height: 1;
|
|
2176
|
-
letter-spacing: -0.
|
|
3041
|
+
letter-spacing: -0.045em;
|
|
2177
3042
|
font-variant-numeric: tabular-nums;
|
|
2178
3043
|
margin-bottom: 10px;
|
|
2179
3044
|
`;
|
|
2180
3045
|
const TrendBadge = styled.span`
|
|
2181
3046
|
display: inline-flex;
|
|
2182
3047
|
align-items: center;
|
|
2183
|
-
gap:
|
|
2184
|
-
padding: 2px
|
|
3048
|
+
gap: 3px;
|
|
3049
|
+
padding: 2px 8px;
|
|
2185
3050
|
border-radius: 20px;
|
|
2186
|
-
font-size:
|
|
3051
|
+
font-size: 9px;
|
|
2187
3052
|
font-weight: 700;
|
|
2188
|
-
|
|
2189
|
-
|
|
3053
|
+
letter-spacing: 0.03em;
|
|
3054
|
+
background: ${(p) => p.$pos ? T.greenDim : T.redDim};
|
|
3055
|
+
color: ${(p) => p.$pos ? T.green : T.red};
|
|
3056
|
+
margin-bottom: 12px;
|
|
2190
3057
|
`;
|
|
2191
3058
|
const SparkWrap = styled.div`
|
|
2192
|
-
height:
|
|
3059
|
+
height: 44px;
|
|
3060
|
+
margin: 0 -20px;
|
|
2193
3061
|
`;
|
|
2194
3062
|
const ChartCard = styled(Card)`
|
|
2195
3063
|
padding: 20px 20px 12px;
|
|
@@ -2201,31 +3069,42 @@ const ChartHeader = styled.div`
|
|
|
2201
3069
|
display: flex;
|
|
2202
3070
|
align-items: center;
|
|
2203
3071
|
justify-content: space-between;
|
|
3072
|
+
gap: 12px;
|
|
2204
3073
|
flex-shrink: 0;
|
|
2205
3074
|
`;
|
|
2206
3075
|
const ChartTitle = styled.div`
|
|
2207
|
-
font-size:
|
|
3076
|
+
font-size: 12px;
|
|
2208
3077
|
font-weight: 700;
|
|
2209
|
-
color:
|
|
3078
|
+
color: ${T.textPrimary};
|
|
3079
|
+
letter-spacing: 0.01em;
|
|
2210
3080
|
`;
|
|
2211
|
-
const
|
|
3081
|
+
const SeriesRow = styled.div`
|
|
2212
3082
|
display: flex;
|
|
2213
3083
|
align-items: center;
|
|
2214
|
-
gap:
|
|
3084
|
+
gap: 6px;
|
|
2215
3085
|
`;
|
|
2216
|
-
const
|
|
3086
|
+
const SeriesBtn = styled.button`
|
|
3087
|
+
appearance: none;
|
|
3088
|
+
border: 1px solid ${(p) => p.$on ? `${p.$c}55` : T.border};
|
|
3089
|
+
background: ${(p) => p.$on ? `${p.$c}18` : "transparent"};
|
|
3090
|
+
color: ${(p) => p.$on ? p.$c : T.textMuted};
|
|
3091
|
+
font-size: 10px;
|
|
3092
|
+
font-weight: 600;
|
|
3093
|
+
padding: 3px 10px 3px 8px;
|
|
3094
|
+
border-radius: 6px;
|
|
3095
|
+
cursor: pointer;
|
|
2217
3096
|
display: flex;
|
|
2218
3097
|
align-items: center;
|
|
2219
3098
|
gap: 5px;
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
3099
|
+
transition: all 160ms;
|
|
3100
|
+
&:hover {
|
|
3101
|
+
border-color: ${(p) => `${p.$c}88`};
|
|
3102
|
+
color: ${(p) => p.$c};
|
|
3103
|
+
background: ${(p) => `${p.$c}22`};
|
|
3104
|
+
}
|
|
2225
3105
|
`;
|
|
2226
|
-
const
|
|
2227
|
-
width:
|
|
2228
|
-
height: 8px;
|
|
3106
|
+
const SDot = styled.span`
|
|
3107
|
+
width: 6px; height: 6px;
|
|
2229
3108
|
border-radius: 50%;
|
|
2230
3109
|
background: ${(p) => p.$c};
|
|
2231
3110
|
display: inline-block;
|
|
@@ -2233,173 +3112,241 @@ const LegendDot = styled.span`
|
|
|
2233
3112
|
`;
|
|
2234
3113
|
const HoverInfo = styled.div`
|
|
2235
3114
|
font-size: 11px;
|
|
2236
|
-
color:
|
|
3115
|
+
color: ${T.textSecondary};
|
|
2237
3116
|
font-variant-numeric: tabular-nums;
|
|
2238
|
-
|
|
3117
|
+
font-family: ${T.mono};
|
|
3118
|
+
min-height: 15px;
|
|
3119
|
+
`;
|
|
3120
|
+
const TwoPanel = styled.div`
|
|
3121
|
+
display: grid;
|
|
3122
|
+
grid-template-columns: ${(p) => p.$ratio ?? "1fr 288px"};
|
|
3123
|
+
gap: 10px;
|
|
3124
|
+
align-items: stretch;
|
|
3125
|
+
@media (max-width: 900px) { grid-template-columns: 1fr; }
|
|
2239
3126
|
`;
|
|
2240
3127
|
const FeedCard = styled(Card)`
|
|
2241
3128
|
display: flex;
|
|
2242
3129
|
flex-direction: column;
|
|
2243
|
-
|
|
3130
|
+
overflow: hidden;
|
|
2244
3131
|
`;
|
|
2245
|
-
const
|
|
2246
|
-
padding: 16px
|
|
2247
|
-
border-bottom: 1px solid
|
|
2248
|
-
|
|
2249
|
-
font-size: 10px;
|
|
3132
|
+
const FeedHead = styled.div`
|
|
3133
|
+
padding: 13px 16px 9px;
|
|
3134
|
+
border-bottom: 1px solid ${T.border};
|
|
3135
|
+
font-size: 11px;
|
|
2250
3136
|
font-weight: 700;
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
3137
|
+
color: ${T.textPrimary};
|
|
3138
|
+
flex-shrink: 0;
|
|
3139
|
+
display: flex;
|
|
3140
|
+
justify-content: space-between;
|
|
3141
|
+
align-items: center;
|
|
2254
3142
|
`;
|
|
2255
|
-
const
|
|
3143
|
+
const FeedSelect = styled.select`
|
|
3144
|
+
appearance: none;
|
|
3145
|
+
border: 1px solid ${T.border};
|
|
3146
|
+
border-radius: 6px;
|
|
3147
|
+
background: ${T.bgCard};
|
|
3148
|
+
color: ${T.textSecondary};
|
|
3149
|
+
font-size: 10px;
|
|
3150
|
+
font-weight: 600;
|
|
3151
|
+
padding: 3px 22px 3px 8px;
|
|
3152
|
+
cursor: pointer;
|
|
3153
|
+
outline: none;
|
|
3154
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23666687'/%3E%3C/svg%3E");
|
|
3155
|
+
background-repeat: no-repeat;
|
|
3156
|
+
background-position: right 7px center;
|
|
3157
|
+
transition: border-color 160ms;
|
|
3158
|
+
&:hover, &:focus { border-color: ${T.accent}; color: ${T.textPrimary}; }
|
|
3159
|
+
`;
|
|
3160
|
+
const FeedScroll = styled.div`
|
|
2256
3161
|
flex: 1;
|
|
2257
3162
|
overflow-y: auto;
|
|
2258
|
-
|
|
2259
|
-
&::-webkit-scrollbar { width:
|
|
2260
|
-
&::-webkit-scrollbar-
|
|
2261
|
-
&::-webkit-scrollbar-thumb { background: #d9d8ff; border-radius: 2px; }
|
|
3163
|
+
min-height: 0;
|
|
3164
|
+
&::-webkit-scrollbar { width: 4px; }
|
|
3165
|
+
&::-webkit-scrollbar-thumb { background: ${T.textMuted}; border-radius: 2px; }
|
|
2262
3166
|
`;
|
|
2263
3167
|
const FeedItem = styled.div`
|
|
3168
|
+
padding: 9px 14px;
|
|
3169
|
+
border-bottom: 1px solid #f0f0f5;
|
|
2264
3170
|
display: flex;
|
|
2265
|
-
|
|
2266
|
-
gap:
|
|
2267
|
-
|
|
2268
|
-
&:hover { background: #
|
|
3171
|
+
flex-direction: column;
|
|
3172
|
+
gap: 3px;
|
|
3173
|
+
transition: background 130ms;
|
|
3174
|
+
&:hover { background: #fafaff; }
|
|
3175
|
+
&:last-child { border-bottom: none; }
|
|
2269
3176
|
`;
|
|
2270
|
-
const
|
|
2271
|
-
|
|
2272
|
-
|
|
3177
|
+
const FeedTop = styled.div`
|
|
3178
|
+
display: flex;
|
|
3179
|
+
align-items: center;
|
|
3180
|
+
justify-content: space-between;
|
|
3181
|
+
gap: 6px;
|
|
2273
3182
|
`;
|
|
2274
|
-
const FeedName = styled.
|
|
2275
|
-
font-size:
|
|
3183
|
+
const FeedName = styled.span`
|
|
3184
|
+
font-size: 11px;
|
|
2276
3185
|
font-weight: 600;
|
|
2277
|
-
color:
|
|
2278
|
-
white-space: nowrap;
|
|
3186
|
+
color: ${T.textPrimary};
|
|
2279
3187
|
overflow: hidden;
|
|
2280
3188
|
text-overflow: ellipsis;
|
|
3189
|
+
white-space: nowrap;
|
|
2281
3190
|
`;
|
|
2282
|
-
const
|
|
3191
|
+
const FeedEmail = styled.span`
|
|
2283
3192
|
font-size: 10px;
|
|
2284
|
-
color:
|
|
2285
|
-
white-space: nowrap;
|
|
3193
|
+
color: ${T.textSecondary};
|
|
2286
3194
|
overflow: hidden;
|
|
2287
3195
|
text-overflow: ellipsis;
|
|
2288
|
-
|
|
3196
|
+
white-space: nowrap;
|
|
3197
|
+
display: block;
|
|
2289
3198
|
`;
|
|
2290
|
-
const
|
|
2291
|
-
font-size:
|
|
2292
|
-
color:
|
|
3199
|
+
const FeedMeta = styled.span`
|
|
3200
|
+
font-size: 9px;
|
|
3201
|
+
color: ${T.textMuted};
|
|
2293
3202
|
white-space: nowrap;
|
|
2294
3203
|
font-variant-numeric: tabular-nums;
|
|
2295
|
-
margin-top: 1px;
|
|
2296
3204
|
`;
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
3205
|
+
const RtnCard = styled(Card)`
|
|
3206
|
+
padding: 20px;
|
|
3207
|
+
display: flex;
|
|
3208
|
+
flex-direction: column;
|
|
3209
|
+
gap: 12px;
|
|
2300
3210
|
`;
|
|
2301
|
-
const
|
|
2302
|
-
|
|
2303
|
-
|
|
3211
|
+
const RtnRow = styled.div`
|
|
3212
|
+
display: flex;
|
|
3213
|
+
align-items: center;
|
|
3214
|
+
gap: 10px;
|
|
3215
|
+
padding: 4px 6px;
|
|
3216
|
+
border-radius: 7px;
|
|
3217
|
+
transition: background 130ms;
|
|
3218
|
+
background: ${(p) => p.$hov ? "#f0f0f6" : "transparent"};
|
|
3219
|
+
cursor: default;
|
|
2304
3220
|
`;
|
|
2305
|
-
const
|
|
2306
|
-
text-align: left;
|
|
2307
|
-
padding: 10px 14px;
|
|
3221
|
+
const RtnLabel = styled.span`
|
|
2308
3222
|
font-size: 10px;
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
text-
|
|
2312
|
-
letter-spacing: 0.06em;
|
|
2313
|
-
border-bottom: 1px solid #eaeaef;
|
|
2314
|
-
background: #fafafa;
|
|
3223
|
+
color: ${T.textSecondary};
|
|
3224
|
+
min-width: 80px;
|
|
3225
|
+
text-align: right;
|
|
2315
3226
|
white-space: nowrap;
|
|
2316
|
-
&:first-child { padding-left: 20px; }
|
|
2317
|
-
&:last-child { padding-right: 20px; }
|
|
2318
|
-
`;
|
|
2319
|
-
const TR$1 = styled.tr`
|
|
2320
|
-
&:hover td { background: #fafafe; }
|
|
2321
|
-
&:last-child td { border-bottom: none; }
|
|
2322
3227
|
`;
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
&:last-child { padding-right: 20px; }
|
|
3228
|
+
const RtnSize = styled.span`
|
|
3229
|
+
font-size: 9px;
|
|
3230
|
+
color: ${T.textMuted};
|
|
3231
|
+
min-width: 52px;
|
|
3232
|
+
text-align: right;
|
|
3233
|
+
white-space: nowrap;
|
|
3234
|
+
font-variant-numeric: tabular-nums;
|
|
2331
3235
|
`;
|
|
2332
|
-
const
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
font-size: 10px;
|
|
2339
|
-
font-weight: 700;
|
|
2340
|
-
background: ${(p) => p.$banned ? "#fcecea" : p.$verified ? "#eafbe7" : "#f0f0ff"};
|
|
2341
|
-
color: ${(p) => p.$banned ? "#d02b20" : p.$verified ? "#5cb176" : "#8e8ea9"};
|
|
2342
|
-
|
|
2343
|
-
&::before {
|
|
2344
|
-
content: '';
|
|
2345
|
-
display: inline-block;
|
|
2346
|
-
width: 5px;
|
|
2347
|
-
height: 5px;
|
|
2348
|
-
border-radius: 50%;
|
|
2349
|
-
background: ${(p) => p.$banned ? "#d02b20" : p.$verified ? "#5cb176" : "#b8b8c7"};
|
|
2350
|
-
}
|
|
3236
|
+
const RtnTrack = styled.div`
|
|
3237
|
+
flex: 1;
|
|
3238
|
+
height: 10px;
|
|
3239
|
+
background: #eaeaef;
|
|
3240
|
+
border-radius: 4px;
|
|
3241
|
+
overflow: hidden;
|
|
2351
3242
|
`;
|
|
2352
|
-
const
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
3243
|
+
const RtnBar = styled.div`
|
|
3244
|
+
width: ${(p) => Math.max(p.$w, 0.5)}%;
|
|
3245
|
+
height: 100%;
|
|
3246
|
+
background: hsl(${(p) => p.$hue}, 60%, 50%);
|
|
3247
|
+
border-radius: 4px;
|
|
3248
|
+
transition: width 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
2357
3249
|
`;
|
|
2358
|
-
const
|
|
3250
|
+
const RtnPct = styled.span`
|
|
2359
3251
|
font-size: 10px;
|
|
2360
3252
|
font-weight: 700;
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
3253
|
+
color: hsl(${(p) => p.$hue}, 60%, 60%);
|
|
3254
|
+
min-width: 38px;
|
|
3255
|
+
text-align: right;
|
|
3256
|
+
font-variant-numeric: tabular-nums;
|
|
3257
|
+
font-family: ${T.mono};
|
|
2365
3258
|
`;
|
|
2366
|
-
const
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
3259
|
+
const RtnTip = styled.div`
|
|
3260
|
+
font-size: 10px;
|
|
3261
|
+
color: ${T.textSecondary};
|
|
3262
|
+
min-height: 14px;
|
|
3263
|
+
font-variant-numeric: tabular-nums;
|
|
3264
|
+
padding: 0 6px;
|
|
2370
3265
|
`;
|
|
2371
|
-
const
|
|
3266
|
+
const RingGrid = styled.div`
|
|
2372
3267
|
display: flex;
|
|
2373
3268
|
flex-direction: column;
|
|
2374
|
-
|
|
3269
|
+
gap: 10px;
|
|
3270
|
+
`;
|
|
3271
|
+
const RingCard = styled(Card)`
|
|
3272
|
+
padding: 18px 20px;
|
|
3273
|
+
display: flex;
|
|
3274
|
+
align-items: center;
|
|
3275
|
+
gap: 16px;
|
|
3276
|
+
flex: 1;
|
|
3277
|
+
`;
|
|
3278
|
+
const RingInfo = styled.div`
|
|
3279
|
+
display: flex;
|
|
3280
|
+
flex-direction: column;
|
|
3281
|
+
gap: 4px;
|
|
3282
|
+
`;
|
|
3283
|
+
const RingVal = styled.div`
|
|
3284
|
+
font-size: 24px;
|
|
3285
|
+
font-weight: 800;
|
|
3286
|
+
color: ${T.textPrimary};
|
|
3287
|
+
letter-spacing: -0.04em;
|
|
3288
|
+
font-variant-numeric: tabular-nums;
|
|
3289
|
+
line-height: 1;
|
|
3290
|
+
`;
|
|
3291
|
+
const RingLabel = styled.div`
|
|
3292
|
+
font-size: 9px;
|
|
3293
|
+
font-weight: 700;
|
|
3294
|
+
text-transform: uppercase;
|
|
3295
|
+
letter-spacing: 0.1em;
|
|
3296
|
+
color: ${T.textMuted};
|
|
3297
|
+
`;
|
|
3298
|
+
const Empty = styled.div`
|
|
3299
|
+
display: flex;
|
|
3300
|
+
flex-direction: column;
|
|
3301
|
+
align-items: center;
|
|
2375
3302
|
justify-content: center;
|
|
2376
3303
|
padding: 32px;
|
|
2377
|
-
color:
|
|
3304
|
+
color: ${T.textMuted};
|
|
2378
3305
|
font-size: 12px;
|
|
2379
|
-
gap:
|
|
3306
|
+
gap: 8px;
|
|
2380
3307
|
`;
|
|
3308
|
+
function useCountUp(target, duration = 850) {
|
|
3309
|
+
const [count, setCount] = useState(0);
|
|
3310
|
+
const prevRef = useRef(0);
|
|
3311
|
+
useEffect(() => {
|
|
3312
|
+
let raf;
|
|
3313
|
+
let start = 0;
|
|
3314
|
+
const from = prevRef.current;
|
|
3315
|
+
const diff = target - from;
|
|
3316
|
+
const step = (ts) => {
|
|
3317
|
+
if (!start) start = ts;
|
|
3318
|
+
const progress = Math.min((ts - start) / duration, 1);
|
|
3319
|
+
const eased = 1 - (1 - progress) ** 3;
|
|
3320
|
+
setCount(Math.round(from + eased * diff));
|
|
3321
|
+
if (progress < 1) raf = requestAnimationFrame(step);
|
|
3322
|
+
else prevRef.current = target;
|
|
3323
|
+
};
|
|
3324
|
+
raf = requestAnimationFrame(step);
|
|
3325
|
+
return () => cancelAnimationFrame(raf);
|
|
3326
|
+
}, [target, duration]);
|
|
3327
|
+
return count;
|
|
3328
|
+
}
|
|
2381
3329
|
function Sparkline({
|
|
2382
3330
|
data,
|
|
2383
3331
|
color,
|
|
2384
3332
|
id
|
|
2385
3333
|
}) {
|
|
2386
3334
|
const W = 300;
|
|
2387
|
-
const H =
|
|
3335
|
+
const H = 44;
|
|
2388
3336
|
if (data.length < 2) return /* @__PURE__ */ jsx("svg", { width: "100%", height: H });
|
|
2389
3337
|
const min = Math.min(...data);
|
|
2390
3338
|
const max = Math.max(...data, min + 1);
|
|
2391
|
-
const range = max - min;
|
|
2392
3339
|
const pts = data.map((v, i) => ({
|
|
2393
3340
|
x: i / (data.length - 1) * W,
|
|
2394
|
-
y: H -
|
|
3341
|
+
y: H - 3 - (v - min) / (max - min) * (H - 8)
|
|
2395
3342
|
}));
|
|
2396
|
-
const line = pts.reduce((
|
|
3343
|
+
const line = pts.reduce((a, p, i) => {
|
|
2397
3344
|
if (i === 0) return `M ${p.x} ${p.y}`;
|
|
2398
3345
|
const pr = pts[i - 1];
|
|
2399
3346
|
const cx = (pr.x + p.x) / 2;
|
|
2400
|
-
return `${
|
|
3347
|
+
return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
|
|
2401
3348
|
}, "");
|
|
2402
|
-
const area = `${line} L ${pts
|
|
3349
|
+
const area = `${line} L ${pts.at(-1).x} ${H} L 0 ${H} Z`;
|
|
2403
3350
|
const gid = `spk-${id}`;
|
|
2404
3351
|
return /* @__PURE__ */ jsxs(
|
|
2405
3352
|
"svg",
|
|
@@ -2408,10 +3355,10 @@ function Sparkline({
|
|
|
2408
3355
|
height: H,
|
|
2409
3356
|
viewBox: `0 0 ${W} ${H}`,
|
|
2410
3357
|
preserveAspectRatio: "none",
|
|
2411
|
-
"aria-hidden":
|
|
3358
|
+
"aria-hidden": true,
|
|
2412
3359
|
children: [
|
|
2413
3360
|
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: gid, x1: "0", y1: "0", x2: "0", y2: "1", children: [
|
|
2414
|
-
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.
|
|
3361
|
+
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.32" }),
|
|
2415
3362
|
/* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
|
|
2416
3363
|
] }) }),
|
|
2417
3364
|
/* @__PURE__ */ jsx("path", { d: area, fill: `url(#${gid})` }),
|
|
@@ -2420,57 +3367,58 @@ function Sparkline({
|
|
|
2420
3367
|
{
|
|
2421
3368
|
d: line,
|
|
2422
3369
|
stroke: color,
|
|
2423
|
-
strokeWidth: "
|
|
3370
|
+
strokeWidth: "1.6",
|
|
2424
3371
|
fill: "none",
|
|
2425
|
-
strokeLinecap: "round"
|
|
2426
|
-
strokeLinejoin: "round"
|
|
3372
|
+
strokeLinecap: "round"
|
|
2427
3373
|
}
|
|
2428
3374
|
)
|
|
2429
3375
|
]
|
|
2430
3376
|
}
|
|
2431
3377
|
);
|
|
2432
3378
|
}
|
|
2433
|
-
const
|
|
2434
|
-
{ key: "totalUsers", color:
|
|
2435
|
-
{ key: "newUsers", color:
|
|
2436
|
-
{ key: "activeUsers", color:
|
|
3379
|
+
const ALL_SERIES = [
|
|
3380
|
+
{ key: "totalUsers", color: T.accent, label: "Total" },
|
|
3381
|
+
{ key: "newUsers", color: T.green, label: "New" },
|
|
3382
|
+
{ key: "activeUsers", color: T.amber, label: "Active" }
|
|
2437
3383
|
];
|
|
2438
|
-
function
|
|
3384
|
+
function GrowthChart({
|
|
2439
3385
|
data,
|
|
2440
3386
|
hovered,
|
|
2441
|
-
onHover
|
|
3387
|
+
onHover,
|
|
3388
|
+
activeSeries
|
|
2442
3389
|
}) {
|
|
2443
3390
|
const W = 600;
|
|
2444
|
-
const H =
|
|
2445
|
-
const PL =
|
|
3391
|
+
const H = 200;
|
|
3392
|
+
const PL = 40;
|
|
2446
3393
|
const PR = 12;
|
|
2447
3394
|
const PT = 12;
|
|
2448
3395
|
const PB = 28;
|
|
2449
3396
|
const CW = W - PL - PR;
|
|
2450
3397
|
const CH = H - PT - PB;
|
|
2451
|
-
if (data.length
|
|
3398
|
+
if (!data.length) {
|
|
2452
3399
|
return /* @__PURE__ */ jsxs(Empty, { children: [
|
|
2453
3400
|
/* @__PURE__ */ jsx("div", { style: { fontSize: 24 }, children: "📊" }),
|
|
2454
3401
|
/* @__PURE__ */ jsx("div", { children: "No growth data for this period" })
|
|
2455
3402
|
] });
|
|
2456
3403
|
}
|
|
2457
|
-
const
|
|
3404
|
+
const active = ALL_SERIES.filter((s) => activeSeries.has(s.key));
|
|
3405
|
+
const allVals = data.flatMap((d) => active.map((s) => d[s.key]));
|
|
2458
3406
|
const maxV = Math.max(...allVals, 10);
|
|
2459
3407
|
const yMax = Math.ceil(maxV * 1.15);
|
|
2460
3408
|
const xp = (i) => PL + i / Math.max(data.length - 1, 1) * CW;
|
|
2461
3409
|
const yp = (v) => PT + (1 - v / yMax) * CH;
|
|
2462
3410
|
const smooth = (vals) => {
|
|
2463
3411
|
const pts = vals.map((v, i) => ({ x: xp(i), y: yp(v) }));
|
|
2464
|
-
const path = pts.reduce((
|
|
3412
|
+
const path = pts.reduce((a, p, i) => {
|
|
2465
3413
|
if (i === 0) return `M ${p.x} ${p.y}`;
|
|
2466
3414
|
const pr = pts[i - 1];
|
|
2467
3415
|
const cx = (pr.x + p.x) / 2;
|
|
2468
|
-
return `${
|
|
3416
|
+
return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
|
|
2469
3417
|
}, "");
|
|
2470
|
-
const area = `${path} L ${pts
|
|
3418
|
+
const area = `${path} L ${pts.at(-1).x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
|
|
2471
3419
|
return { pts, path, area };
|
|
2472
3420
|
};
|
|
2473
|
-
const lines =
|
|
3421
|
+
const lines = active.map((s) => ({
|
|
2474
3422
|
...s,
|
|
2475
3423
|
...smooth(data.map((d) => d[s.key]))
|
|
2476
3424
|
}));
|
|
@@ -2493,17 +3441,17 @@ function AreaChart({
|
|
|
2493
3441
|
/* @__PURE__ */ jsx("defs", { children: lines.map((s) => /* @__PURE__ */ jsxs(
|
|
2494
3442
|
"linearGradient",
|
|
2495
3443
|
{
|
|
2496
|
-
id: `ag-${s.
|
|
3444
|
+
id: `ag-${s.key}`,
|
|
2497
3445
|
x1: "0",
|
|
2498
3446
|
y1: "0",
|
|
2499
3447
|
x2: "0",
|
|
2500
3448
|
y2: "1",
|
|
2501
3449
|
children: [
|
|
2502
|
-
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.
|
|
3450
|
+
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.18" }),
|
|
2503
3451
|
/* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: s.color, stopOpacity: "0" })
|
|
2504
3452
|
]
|
|
2505
3453
|
},
|
|
2506
|
-
s.
|
|
3454
|
+
s.key
|
|
2507
3455
|
)) }),
|
|
2508
3456
|
yTicks.map((t) => /* @__PURE__ */ jsxs("g", { children: [
|
|
2509
3457
|
/* @__PURE__ */ jsx(
|
|
@@ -2523,7 +3471,7 @@ function AreaChart({
|
|
|
2523
3471
|
x: PL - 6,
|
|
2524
3472
|
y: t.y + 4,
|
|
2525
3473
|
textAnchor: "end",
|
|
2526
|
-
fill:
|
|
3474
|
+
fill: T.textMuted,
|
|
2527
3475
|
fontSize: "9",
|
|
2528
3476
|
children: t.v >= 1e3 ? `${(t.v / 1e3).toFixed(1)}k` : t.v
|
|
2529
3477
|
}
|
|
@@ -2540,25 +3488,18 @@ function AreaChart({
|
|
|
2540
3488
|
strokeWidth: "1"
|
|
2541
3489
|
}
|
|
2542
3490
|
),
|
|
2543
|
-
lines.map((s) => /* @__PURE__ */ jsx(
|
|
2544
|
-
"path",
|
|
2545
|
-
{
|
|
2546
|
-
d: s.area,
|
|
2547
|
-
fill: `url(#ag-${s.color.replace("#", "")})`
|
|
2548
|
-
},
|
|
2549
|
-
`a-${s.color}`
|
|
2550
|
-
)),
|
|
3491
|
+
lines.map((s) => /* @__PURE__ */ jsx("path", { d: s.area, fill: `url(#ag-${s.key})` }, `area-${s.key}`)),
|
|
2551
3492
|
lines.map((s) => /* @__PURE__ */ jsx(
|
|
2552
3493
|
"path",
|
|
2553
3494
|
{
|
|
2554
3495
|
d: s.path,
|
|
2555
3496
|
stroke: s.color,
|
|
2556
|
-
strokeWidth: "
|
|
3497
|
+
strokeWidth: "2",
|
|
2557
3498
|
fill: "none",
|
|
2558
3499
|
strokeLinecap: "round",
|
|
2559
3500
|
strokeLinejoin: "round"
|
|
2560
3501
|
},
|
|
2561
|
-
`
|
|
3502
|
+
`line-${s.key}`
|
|
2562
3503
|
)),
|
|
2563
3504
|
data.map((d, i) => {
|
|
2564
3505
|
if (i % step !== 0 && i !== data.length - 1) return null;
|
|
@@ -2568,7 +3509,7 @@ function AreaChart({
|
|
|
2568
3509
|
x: xp(i),
|
|
2569
3510
|
y: H - 6,
|
|
2570
3511
|
textAnchor: "middle",
|
|
2571
|
-
fill:
|
|
3512
|
+
fill: T.textMuted,
|
|
2572
3513
|
fontSize: "9",
|
|
2573
3514
|
children: d.label
|
|
2574
3515
|
},
|
|
@@ -2578,7 +3519,7 @@ function AreaChart({
|
|
|
2578
3519
|
data.map((d, i) => {
|
|
2579
3520
|
const isHov = hovered === i;
|
|
2580
3521
|
return (
|
|
2581
|
-
// biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
|
|
3522
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
|
|
2582
3523
|
/* @__PURE__ */ jsxs(
|
|
2583
3524
|
"g",
|
|
2584
3525
|
{
|
|
@@ -2603,10 +3544,9 @@ function AreaChart({
|
|
|
2603
3544
|
y1: PT,
|
|
2604
3545
|
x2: xp(i),
|
|
2605
3546
|
y2: PT + CH,
|
|
2606
|
-
stroke: "
|
|
3547
|
+
stroke: "rgba(50,50,77,0.2)",
|
|
2607
3548
|
strokeWidth: "1",
|
|
2608
|
-
strokeDasharray: "3 3"
|
|
2609
|
-
opacity: "0.35"
|
|
3549
|
+
strokeDasharray: "3 3"
|
|
2610
3550
|
}
|
|
2611
3551
|
),
|
|
2612
3552
|
lines.map((s) => /* @__PURE__ */ jsx(
|
|
@@ -2616,11 +3556,11 @@ function AreaChart({
|
|
|
2616
3556
|
cy: yp(d[s.key]),
|
|
2617
3557
|
r: isHov ? 4 : 2.5,
|
|
2618
3558
|
fill: s.color,
|
|
2619
|
-
stroke:
|
|
3559
|
+
stroke: T.bg,
|
|
2620
3560
|
strokeWidth: isHov ? 2 : 1,
|
|
2621
|
-
opacity: isHov ? 1 : 0.
|
|
3561
|
+
opacity: isHov ? 1 : 0.5
|
|
2622
3562
|
},
|
|
2623
|
-
s.
|
|
3563
|
+
s.key
|
|
2624
3564
|
))
|
|
2625
3565
|
]
|
|
2626
3566
|
},
|
|
@@ -2632,23 +3572,70 @@ function AreaChart({
|
|
|
2632
3572
|
}
|
|
2633
3573
|
);
|
|
2634
3574
|
}
|
|
3575
|
+
function ProgressRing({
|
|
3576
|
+
value,
|
|
3577
|
+
max,
|
|
3578
|
+
color,
|
|
3579
|
+
size = 56
|
|
3580
|
+
}) {
|
|
3581
|
+
const r = (size - 9) / 2;
|
|
3582
|
+
const circ = 2 * Math.PI * r;
|
|
3583
|
+
const pct = Math.min(value / Math.max(max, 1), 1);
|
|
3584
|
+
const offset = circ * (1 - pct);
|
|
3585
|
+
return /* @__PURE__ */ jsxs(
|
|
3586
|
+
"svg",
|
|
3587
|
+
{
|
|
3588
|
+
width: size,
|
|
3589
|
+
height: size,
|
|
3590
|
+
style: { flexShrink: 0 },
|
|
3591
|
+
"aria-hidden": true,
|
|
3592
|
+
children: [
|
|
3593
|
+
/* @__PURE__ */ jsx(
|
|
3594
|
+
"circle",
|
|
3595
|
+
{
|
|
3596
|
+
cx: size / 2,
|
|
3597
|
+
cy: size / 2,
|
|
3598
|
+
r,
|
|
3599
|
+
fill: "none",
|
|
3600
|
+
stroke: "#e5e7eb",
|
|
3601
|
+
strokeWidth: "7"
|
|
3602
|
+
}
|
|
3603
|
+
),
|
|
3604
|
+
/* @__PURE__ */ jsx(
|
|
3605
|
+
"circle",
|
|
3606
|
+
{
|
|
3607
|
+
cx: size / 2,
|
|
3608
|
+
cy: size / 2,
|
|
3609
|
+
r,
|
|
3610
|
+
fill: "none",
|
|
3611
|
+
stroke: color,
|
|
3612
|
+
strokeWidth: "7",
|
|
3613
|
+
strokeDasharray: `${circ} ${circ}`,
|
|
3614
|
+
strokeDashoffset: offset,
|
|
3615
|
+
strokeLinecap: "round",
|
|
3616
|
+
transform: `rotate(-90 ${size / 2} ${size / 2})`,
|
|
3617
|
+
style: {
|
|
3618
|
+
transition: "stroke-dashoffset 0.9s cubic-bezier(0.4,0,0.2,1)"
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
)
|
|
3622
|
+
]
|
|
3623
|
+
}
|
|
3624
|
+
);
|
|
3625
|
+
}
|
|
2635
3626
|
function relTime(date) {
|
|
2636
|
-
const
|
|
2637
|
-
const
|
|
2638
|
-
if (
|
|
2639
|
-
if (
|
|
2640
|
-
const
|
|
2641
|
-
if (
|
|
2642
|
-
return `${Math.floor(
|
|
3627
|
+
const diff = Date.now() - new Date(date).getTime();
|
|
3628
|
+
const mins = Math.floor(diff / 6e4);
|
|
3629
|
+
if (mins < 1) return "just now";
|
|
3630
|
+
if (mins < 60) return `${mins}m`;
|
|
3631
|
+
const hrs = Math.floor(mins / 60);
|
|
3632
|
+
if (hrs < 24) return `${hrs}h`;
|
|
3633
|
+
return `${Math.floor(hrs / 24)}d`;
|
|
2643
3634
|
}
|
|
2644
|
-
function
|
|
2645
|
-
return
|
|
2646
|
-
month: "short",
|
|
2647
|
-
day: "numeric",
|
|
2648
|
-
year: "numeric"
|
|
2649
|
-
});
|
|
3635
|
+
function rateHue(r) {
|
|
3636
|
+
return r >= 70 ? 142 : r >= 40 ? 38 : 4;
|
|
2650
3637
|
}
|
|
2651
|
-
function
|
|
3638
|
+
function StatItem({
|
|
2652
3639
|
id,
|
|
2653
3640
|
label,
|
|
2654
3641
|
value,
|
|
@@ -2657,26 +3644,39 @@ function StatCardItem({
|
|
|
2657
3644
|
color,
|
|
2658
3645
|
delay = 0
|
|
2659
3646
|
}) {
|
|
3647
|
+
const animated = useCountUp(value);
|
|
2660
3648
|
const isPos = pct === void 0 || pct >= 0;
|
|
2661
|
-
return /* @__PURE__ */ jsxs(
|
|
2662
|
-
/* @__PURE__ */
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
"%"
|
|
2670
|
-
] })
|
|
3649
|
+
return /* @__PURE__ */ jsxs(StatCard, { $delay: delay, $accent: color, children: [
|
|
3650
|
+
/* @__PURE__ */ jsx(StatLabel, { children: label }),
|
|
3651
|
+
/* @__PURE__ */ jsx(StatValue, { children: animated.toLocaleString() }),
|
|
3652
|
+
pct !== void 0 && /* @__PURE__ */ jsxs(TrendBadge, { $pos: isPos, children: [
|
|
3653
|
+
isPos ? "↑" : "↓",
|
|
3654
|
+
" ",
|
|
3655
|
+
Math.abs(pct).toFixed(1),
|
|
3656
|
+
"%"
|
|
2671
3657
|
] }),
|
|
2672
3658
|
sparkline && sparkline.length > 1 && /* @__PURE__ */ jsx(SparkWrap, { children: /* @__PURE__ */ jsx(Sparkline, { data: sparkline, color, id }) })
|
|
2673
3659
|
] });
|
|
2674
3660
|
}
|
|
2675
3661
|
function OverviewPage() {
|
|
3662
|
+
const { get } = useFetchClient();
|
|
2676
3663
|
const [period, setPeriod] = useState(
|
|
2677
3664
|
"weekly"
|
|
2678
3665
|
);
|
|
3666
|
+
const [feedMode, setFeedMode] = useState("signups");
|
|
2679
3667
|
const [hovIdx, setHovIdx] = useState(null);
|
|
3668
|
+
const [rtnHov, setRtnHov] = useState(null);
|
|
3669
|
+
const [activeSeries, setActiveSeries] = useState(
|
|
3670
|
+
() => /* @__PURE__ */ new Set(["totalUsers", "newUsers", "activeUsers"])
|
|
3671
|
+
);
|
|
3672
|
+
const toggleSeries = (key) => {
|
|
3673
|
+
setActiveSeries((prev) => {
|
|
3674
|
+
const next = new Set(prev);
|
|
3675
|
+
if (next.has(key) && next.size > 1) next.delete(key);
|
|
3676
|
+
else next.add(key);
|
|
3677
|
+
return next;
|
|
3678
|
+
});
|
|
3679
|
+
};
|
|
2680
3680
|
const statsQuery = useQuery({
|
|
2681
3681
|
queryKey: ["dash-user-stats"],
|
|
2682
3682
|
queryFn: async () => {
|
|
@@ -2693,12 +3693,10 @@ function OverviewPage() {
|
|
|
2693
3693
|
return r.data;
|
|
2694
3694
|
}
|
|
2695
3695
|
});
|
|
2696
|
-
const
|
|
2697
|
-
queryKey: ["dash-
|
|
3696
|
+
const retentionQuery = useQuery({
|
|
3697
|
+
queryKey: ["dash-user-retention", period],
|
|
2698
3698
|
queryFn: async () => {
|
|
2699
|
-
const r = await client.dash.
|
|
2700
|
-
query: { page: 1, limit: 10 }
|
|
2701
|
-
});
|
|
3699
|
+
const r = await client.dash.userRetentionData({ query: { period } });
|
|
2702
3700
|
if (r.error) throw new Error(r.error.message ?? "Failed");
|
|
2703
3701
|
return r.data;
|
|
2704
3702
|
}
|
|
@@ -2707,12 +3705,22 @@ function OverviewPage() {
|
|
|
2707
3705
|
queryKey: ["dash-recent-users"],
|
|
2708
3706
|
queryFn: async () => {
|
|
2709
3707
|
const r = await client.dash.listUsers({
|
|
2710
|
-
query: {
|
|
3708
|
+
query: { limit: 12, offset: 0, sortBy: "createdAt", sortOrder: "desc" }
|
|
2711
3709
|
});
|
|
2712
3710
|
if (r.error) throw new Error(r.error.message ?? "Failed");
|
|
2713
3711
|
return r.data;
|
|
2714
3712
|
}
|
|
2715
3713
|
});
|
|
3714
|
+
const sessionsQuery = useQuery({
|
|
3715
|
+
queryKey: ["dash-recent-sessions"],
|
|
3716
|
+
queryFn: async () => {
|
|
3717
|
+
const { data } = await get(
|
|
3718
|
+
"/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=updatedAt:desc"
|
|
3719
|
+
);
|
|
3720
|
+
return data.results ?? [];
|
|
3721
|
+
},
|
|
3722
|
+
refetchInterval: 3e4
|
|
3723
|
+
});
|
|
2716
3724
|
const orgsQuery = useQuery({
|
|
2717
3725
|
queryKey: ["dash-orgs-count"],
|
|
2718
3726
|
queryFn: async () => {
|
|
@@ -2723,596 +3731,403 @@ function OverviewPage() {
|
|
|
2723
3731
|
return r.data;
|
|
2724
3732
|
}
|
|
2725
3733
|
});
|
|
3734
|
+
const sessionsRaw = sessionsQuery.data ?? [];
|
|
3735
|
+
const activeUserIds = [];
|
|
3736
|
+
const lastActiveByUserId = /* @__PURE__ */ new Map();
|
|
3737
|
+
for (const s of sessionsRaw) {
|
|
3738
|
+
const uid = String(s.userId);
|
|
3739
|
+
if (!lastActiveByUserId.has(uid)) {
|
|
3740
|
+
lastActiveByUserId.set(uid, s.updatedAt ?? s.createdAt);
|
|
3741
|
+
activeUserIds.push(uid);
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
const activeUsersQuery = useQuery({
|
|
3745
|
+
queryKey: ["dash-active-users", activeUserIds],
|
|
3746
|
+
queryFn: async () => {
|
|
3747
|
+
if (activeUserIds.length === 0) return [];
|
|
3748
|
+
const params = new URLSearchParams({ uid: "plugin::better-auth.user" });
|
|
3749
|
+
for (let i = 0; i < activeUserIds.length; i++) {
|
|
3750
|
+
params.set(`filters[id][$in][${i}]`, activeUserIds[i]);
|
|
3751
|
+
}
|
|
3752
|
+
params.set("pagination[pageSize]", "12");
|
|
3753
|
+
const { data } = await get(
|
|
3754
|
+
`/better-auth-dashboard/db?${params}`
|
|
3755
|
+
);
|
|
3756
|
+
return data.results ?? [];
|
|
3757
|
+
},
|
|
3758
|
+
enabled: feedMode === "active" && activeUserIds.length > 0
|
|
3759
|
+
});
|
|
3760
|
+
const chartRef = useRef(null);
|
|
3761
|
+
const [chartHeight, setChartHeight] = useState(0);
|
|
3762
|
+
useEffect(() => {
|
|
3763
|
+
const el = chartRef.current;
|
|
3764
|
+
if (!el) return;
|
|
3765
|
+
const ro = new ResizeObserver((entries) => {
|
|
3766
|
+
for (const entry of entries) setChartHeight(entry.contentRect.height);
|
|
3767
|
+
});
|
|
3768
|
+
ro.observe(el);
|
|
3769
|
+
return () => ro.disconnect();
|
|
3770
|
+
}, []);
|
|
2726
3771
|
if (statsQuery.isLoading) {
|
|
2727
|
-
return /* @__PURE__ */ jsx(
|
|
3772
|
+
return /* @__PURE__ */ jsx(
|
|
3773
|
+
Flex,
|
|
3774
|
+
{
|
|
3775
|
+
justifyContent: "center",
|
|
3776
|
+
alignItems: "center",
|
|
3777
|
+
padding: 12,
|
|
3778
|
+
style: { background: T.bg, minHeight: "100%" },
|
|
3779
|
+
children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
|
|
3780
|
+
}
|
|
3781
|
+
);
|
|
2728
3782
|
}
|
|
2729
3783
|
const stats = statsQuery.data;
|
|
2730
3784
|
if (!stats) return null;
|
|
2731
3785
|
const graphData = graphQuery.data?.data ?? [];
|
|
2732
|
-
const
|
|
2733
|
-
const
|
|
3786
|
+
const rtnData = retentionQuery.data?.data ?? [];
|
|
3787
|
+
const users = usersQuery.data?.users ?? [];
|
|
2734
3788
|
const orgCount = orgsQuery.data?.total ?? 0;
|
|
3789
|
+
const activeUserMap = /* @__PURE__ */ new Map();
|
|
3790
|
+
for (const u of activeUsersQuery.data ?? []) {
|
|
3791
|
+
activeUserMap.set(String(u.id), u);
|
|
3792
|
+
}
|
|
3793
|
+
const sortedActiveUsers = activeUserIds.map((uid) => activeUserMap.get(uid)).filter((u) => u !== void 0);
|
|
2735
3794
|
const totalSpark = graphData.map((d) => d.totalUsers);
|
|
2736
3795
|
const newSpark = graphData.map((d) => d.newUsers);
|
|
2737
3796
|
const activeSpark = graphData.map((d) => d.activeUsers);
|
|
2738
3797
|
const hovRow = hovIdx !== null ? graphData[hovIdx] : null;
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
SingleSelect,
|
|
2752
|
-
{
|
|
2753
|
-
value: period,
|
|
2754
|
-
onChange: (v) => setPeriod(v),
|
|
2755
|
-
size: "S",
|
|
2756
|
-
"aria-label": "Select period",
|
|
2757
|
-
children: [
|
|
2758
|
-
/* @__PURE__ */ jsx(SingleSelectOption, { value: "daily", children: "Daily" }),
|
|
2759
|
-
/* @__PURE__ */ jsx(SingleSelectOption, { value: "weekly", children: "Weekly" }),
|
|
2760
|
-
/* @__PURE__ */ jsx(SingleSelectOption, { value: "monthly", children: "Monthly" })
|
|
2761
|
-
]
|
|
2762
|
-
}
|
|
2763
|
-
) })
|
|
2764
|
-
] }),
|
|
2765
|
-
/* @__PURE__ */ jsxs(Divider, { children: [
|
|
2766
|
-
/* @__PURE__ */ jsx(DivLabel, { children: "Sign-ups" }),
|
|
2767
|
-
/* @__PURE__ */ jsx(DivLine, {})
|
|
2768
|
-
] }),
|
|
2769
|
-
/* @__PURE__ */ jsxs(StatRow, { children: [
|
|
2770
|
-
/* @__PURE__ */ jsx(
|
|
2771
|
-
StatCardItem,
|
|
2772
|
-
{
|
|
2773
|
-
id: "total",
|
|
2774
|
-
label: "Total Users",
|
|
2775
|
-
value: stats.total ?? 0,
|
|
2776
|
-
sparkline: totalSpark,
|
|
2777
|
-
color: "#4945FF",
|
|
2778
|
-
delay: 0
|
|
2779
|
-
}
|
|
2780
|
-
),
|
|
2781
|
-
/* @__PURE__ */ jsx(
|
|
2782
|
-
StatCardItem,
|
|
2783
|
-
{
|
|
2784
|
-
id: "d-sig",
|
|
2785
|
-
label: "Daily Sign-ups",
|
|
2786
|
-
value: stats.daily.signUps ?? 0,
|
|
2787
|
-
pct: stats.daily.percentage ?? void 0,
|
|
2788
|
-
sparkline: newSpark,
|
|
2789
|
-
color: "#5CB176",
|
|
2790
|
-
delay: 1
|
|
2791
|
-
}
|
|
2792
|
-
),
|
|
2793
|
-
/* @__PURE__ */ jsx(
|
|
2794
|
-
StatCardItem,
|
|
2795
|
-
{
|
|
2796
|
-
id: "w-sig",
|
|
2797
|
-
label: "Weekly Sign-ups",
|
|
2798
|
-
value: stats.weekly.signUps ?? 0,
|
|
2799
|
-
pct: stats.weekly.percentage ?? void 0,
|
|
2800
|
-
sparkline: newSpark,
|
|
2801
|
-
color: "#5CB176",
|
|
2802
|
-
delay: 2
|
|
2803
|
-
}
|
|
2804
|
-
),
|
|
2805
|
-
/* @__PURE__ */ jsx(
|
|
2806
|
-
StatCardItem,
|
|
2807
|
-
{
|
|
2808
|
-
id: "m-sig",
|
|
2809
|
-
label: "Monthly Sign-ups",
|
|
2810
|
-
value: stats.monthly.signUps ?? 0,
|
|
2811
|
-
pct: stats.monthly.percentage ?? void 0,
|
|
2812
|
-
sparkline: newSpark,
|
|
2813
|
-
color: "#5CB176",
|
|
2814
|
-
delay: 3
|
|
2815
|
-
}
|
|
3798
|
+
const rtnHovRow = rtnHov !== null ? rtnData[rtnHov] : null;
|
|
3799
|
+
const activeMax = Math.max(
|
|
3800
|
+
stats.activeUsers.daily.active ?? 0,
|
|
3801
|
+
stats.activeUsers.weekly.active ?? 0,
|
|
3802
|
+
stats.activeUsers.monthly.active ?? 0,
|
|
3803
|
+
1
|
|
3804
|
+
);
|
|
3805
|
+
return /* @__PURE__ */ jsx(
|
|
3806
|
+
Page.Protect,
|
|
3807
|
+
{
|
|
3808
|
+
permissions: PERMISSIONS.overview.filter(
|
|
3809
|
+
(p) => p.action === `plugin::${PLUGIN_ID}.overview.read`
|
|
2816
3810
|
),
|
|
2817
|
-
/* @__PURE__ */
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
3811
|
+
children: /* @__PURE__ */ jsxs(Wrap$1, { "data-testid": "overview-page", children: [
|
|
3812
|
+
/* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-end", children: [
|
|
3813
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
3814
|
+
/* @__PURE__ */ jsx(
|
|
3815
|
+
"div",
|
|
3816
|
+
{
|
|
3817
|
+
style: {
|
|
3818
|
+
fontSize: 22,
|
|
3819
|
+
fontWeight: 800,
|
|
3820
|
+
color: T.textPrimary,
|
|
3821
|
+
letterSpacing: "-0.03em"
|
|
3822
|
+
},
|
|
3823
|
+
children: "Overview"
|
|
3824
|
+
}
|
|
3825
|
+
),
|
|
3826
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: T.textSecondary, marginTop: 4 }, children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
|
|
3827
|
+
weekday: "long",
|
|
3828
|
+
year: "numeric",
|
|
3829
|
+
month: "long",
|
|
3830
|
+
day: "numeric"
|
|
3831
|
+
}) })
|
|
3832
|
+
] }),
|
|
3833
|
+
/* @__PURE__ */ jsx(PillGroup, { children: ["daily", "weekly", "monthly"].map((p) => /* @__PURE__ */ jsx(Pill, { $active: period === p, onClick: () => setPeriod(p), children: p[0].toUpperCase() + p.slice(1) }, p)) })
|
|
2836
3834
|
] }),
|
|
2837
|
-
/* @__PURE__ */
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
alignItems: "center",
|
|
2843
|
-
style: { flex: 1, minHeight: 180 },
|
|
2844
|
-
children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
|
|
2845
|
-
}
|
|
2846
|
-
) : /* @__PURE__ */ jsx(AreaChart, { data: graphData, hovered: hovIdx, onHover: setHovIdx })
|
|
2847
|
-
] }),
|
|
2848
|
-
/* @__PURE__ */ jsxs(FeedCard, { $delay: 6, children: [
|
|
2849
|
-
/* @__PURE__ */ jsx(FeedHeader, { children: "Recent Sessions" }),
|
|
2850
|
-
/* @__PURE__ */ jsx(FeedList, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Empty, { children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : sessions.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No recent sessions" }) : sessions.map((s) => /* @__PURE__ */ jsxs(FeedItem, { children: [
|
|
3835
|
+
/* @__PURE__ */ jsxs(SectionDivider, { children: [
|
|
3836
|
+
/* @__PURE__ */ jsx(DivLabel, { children: "Metrics" }),
|
|
3837
|
+
/* @__PURE__ */ jsx(DivLine, {})
|
|
3838
|
+
] }),
|
|
3839
|
+
/* @__PURE__ */ jsxs(StatGrid, { children: [
|
|
2851
3840
|
/* @__PURE__ */ jsx(
|
|
2852
|
-
|
|
3841
|
+
StatItem,
|
|
2853
3842
|
{
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
3843
|
+
id: "total",
|
|
3844
|
+
label: "Total Users",
|
|
3845
|
+
value: stats.total ?? 0,
|
|
3846
|
+
sparkline: totalSpark,
|
|
3847
|
+
color: T.accent,
|
|
3848
|
+
delay: 0
|
|
2857
3849
|
}
|
|
2858
|
-
),
|
|
2859
|
-
/* @__PURE__ */ jsxs(FeedContent, { children: [
|
|
2860
|
-
/* @__PURE__ */ jsx(FeedName, { children: s.user?.name ?? "Unknown" }),
|
|
2861
|
-
/* @__PURE__ */ jsx(FeedSub, { children: s.user?.email ?? "" }),
|
|
2862
|
-
/* @__PURE__ */ jsx(FeedTime, { children: relTime(s.createdAt) })
|
|
2863
|
-
] })
|
|
2864
|
-
] }, s.id)) })
|
|
2865
|
-
] })
|
|
2866
|
-
] }),
|
|
2867
|
-
/* @__PURE__ */ jsxs(Divider, { children: [
|
|
2868
|
-
/* @__PURE__ */ jsx(DivLabel, { children: "Active Users" }),
|
|
2869
|
-
/* @__PURE__ */ jsx(DivLine, {})
|
|
2870
|
-
] }),
|
|
2871
|
-
/* @__PURE__ */ jsxs(ActiveRow, { children: [
|
|
2872
|
-
/* @__PURE__ */ jsx(
|
|
2873
|
-
StatCardItem,
|
|
2874
|
-
{
|
|
2875
|
-
id: "d-act",
|
|
2876
|
-
label: "Daily Active",
|
|
2877
|
-
value: stats.activeUsers.daily.active ?? 0,
|
|
2878
|
-
pct: stats.activeUsers.daily.percentage ?? void 0,
|
|
2879
|
-
sparkline: activeSpark,
|
|
2880
|
-
color: "#E57553",
|
|
2881
|
-
delay: 7
|
|
2882
|
-
}
|
|
2883
|
-
),
|
|
2884
|
-
/* @__PURE__ */ jsx(
|
|
2885
|
-
StatCardItem,
|
|
2886
|
-
{
|
|
2887
|
-
id: "w-act",
|
|
2888
|
-
label: "Weekly Active",
|
|
2889
|
-
value: stats.activeUsers.weekly.active ?? 0,
|
|
2890
|
-
pct: stats.activeUsers.weekly.percentage ?? void 0,
|
|
2891
|
-
sparkline: activeSpark,
|
|
2892
|
-
color: "#E57553",
|
|
2893
|
-
delay: 8
|
|
2894
|
-
}
|
|
2895
|
-
),
|
|
2896
|
-
/* @__PURE__ */ jsx(
|
|
2897
|
-
StatCardItem,
|
|
2898
|
-
{
|
|
2899
|
-
id: "m-act",
|
|
2900
|
-
label: "Monthly Active",
|
|
2901
|
-
value: stats.activeUsers.monthly.active ?? 0,
|
|
2902
|
-
pct: stats.activeUsers.monthly.percentage ?? void 0,
|
|
2903
|
-
sparkline: activeSpark,
|
|
2904
|
-
color: "#E57553",
|
|
2905
|
-
delay: 9
|
|
2906
|
-
}
|
|
2907
|
-
)
|
|
2908
|
-
] }),
|
|
2909
|
-
/* @__PURE__ */ jsxs(Divider, { children: [
|
|
2910
|
-
/* @__PURE__ */ jsx(DivLabel, { children: "Recent Users" }),
|
|
2911
|
-
/* @__PURE__ */ jsx(DivLine, {})
|
|
2912
|
-
] }),
|
|
2913
|
-
/* @__PURE__ */ jsx(UsersCard, { $delay: 10, children: /* @__PURE__ */ jsx(TableWrap, { children: /* @__PURE__ */ jsxs(Table$1, { children: [
|
|
2914
|
-
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: ["User", "Email", "Joined", "Status"].map((h) => /* @__PURE__ */ jsx(TH$1, { children: h }, h)) }) }),
|
|
2915
|
-
/* @__PURE__ */ jsx("tbody", { children: usersQuery.isLoading ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(TD$1, { colSpan: 4, style: { textAlign: "center", padding: 28 }, children: /* @__PURE__ */ jsx(Loader, { children: "Loading users…" }) }) }) : recentUsers.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
|
|
2916
|
-
TD$1,
|
|
2917
|
-
{
|
|
2918
|
-
colSpan: 4,
|
|
2919
|
-
style: {
|
|
2920
|
-
textAlign: "center",
|
|
2921
|
-
padding: 28,
|
|
2922
|
-
color: "#8e8ea9"
|
|
2923
|
-
},
|
|
2924
|
-
children: "No users yet"
|
|
2925
|
-
}
|
|
2926
|
-
) }) : recentUsers.map((u) => /* @__PURE__ */ jsxs(TR$1, { children: [
|
|
2927
|
-
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
|
|
2928
|
-
/* @__PURE__ */ jsx(Avatar, { name: u.name, src: u.image, size: 26 }),
|
|
2929
|
-
/* @__PURE__ */ jsx("span", { style: { fontWeight: 600, fontSize: 12 }, children: u.name })
|
|
2930
|
-
] }) }),
|
|
2931
|
-
/* @__PURE__ */ jsx(
|
|
2932
|
-
TD$1,
|
|
2933
|
-
{
|
|
2934
|
-
style: {
|
|
2935
|
-
color: "#8e8ea9",
|
|
2936
|
-
fontFamily: "monospace",
|
|
2937
|
-
fontSize: 11
|
|
2938
|
-
},
|
|
2939
|
-
children: u.email
|
|
2940
|
-
}
|
|
2941
|
-
),
|
|
2942
|
-
/* @__PURE__ */ jsx(
|
|
2943
|
-
TD$1,
|
|
2944
|
-
{
|
|
2945
|
-
style: {
|
|
2946
|
-
color: "#8e8ea9",
|
|
2947
|
-
fontSize: 11,
|
|
2948
|
-
whiteSpace: "nowrap"
|
|
2949
|
-
},
|
|
2950
|
-
children: fmtDate(u.createdAt)
|
|
2951
|
-
}
|
|
2952
|
-
),
|
|
2953
|
-
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(
|
|
2954
|
-
StatusChip$1,
|
|
2955
|
-
{
|
|
2956
|
-
$verified: u.emailVerified,
|
|
2957
|
-
$banned: u.banned,
|
|
2958
|
-
children: u.banned ? "Banned" : u.emailVerified ? "Verified" : "Unverified"
|
|
2959
|
-
}
|
|
2960
|
-
) })
|
|
2961
|
-
] }, u.id)) })
|
|
2962
|
-
] }) }) })
|
|
2963
|
-
] });
|
|
2964
|
-
}
|
|
2965
|
-
const PAGE_SIZE$1 = 25;
|
|
2966
|
-
const fadeUp$1 = keyframes`
|
|
2967
|
-
from { opacity: 0; transform: translateY(6px); }
|
|
2968
|
-
to { opacity: 1; transform: translateY(0); }
|
|
2969
|
-
`;
|
|
2970
|
-
const Wrap$1 = styled.div`
|
|
2971
|
-
padding: 28px 32px;
|
|
2972
|
-
background: #f6f6f9;
|
|
2973
|
-
min-height: 100%;
|
|
2974
|
-
display: flex;
|
|
2975
|
-
flex-direction: column;
|
|
2976
|
-
gap: 20px;
|
|
2977
|
-
`;
|
|
2978
|
-
const PageHeader$1 = styled.div`
|
|
2979
|
-
display: flex;
|
|
2980
|
-
justify-content: space-between;
|
|
2981
|
-
align-items: flex-start;
|
|
2982
|
-
`;
|
|
2983
|
-
const TitleBlock$1 = styled.div`
|
|
2984
|
-
display: flex;
|
|
2985
|
-
flex-direction: column;
|
|
2986
|
-
gap: 4px;
|
|
2987
|
-
`;
|
|
2988
|
-
const PageTitle$1 = styled.h1`
|
|
2989
|
-
margin: 0;
|
|
2990
|
-
font-size: 22px;
|
|
2991
|
-
font-weight: 800;
|
|
2992
|
-
color: #32324d;
|
|
2993
|
-
letter-spacing: -0.03em;
|
|
2994
|
-
`;
|
|
2995
|
-
const PageSubtitle$1 = styled.p`
|
|
2996
|
-
margin: 0;
|
|
2997
|
-
font-size: 12px;
|
|
2998
|
-
color: #8e8ea9;
|
|
2999
|
-
`;
|
|
3000
|
-
const TableCard$1 = styled.div`
|
|
3001
|
-
background: #ffffff;
|
|
3002
|
-
border: 1px solid #eaeaef;
|
|
3003
|
-
border-radius: 10px;
|
|
3004
|
-
overflow: hidden;
|
|
3005
|
-
`;
|
|
3006
|
-
const ColumnHeader = styled.div`
|
|
3007
|
-
padding: 10px 20px;
|
|
3008
|
-
display: flex;
|
|
3009
|
-
align-items: center;
|
|
3010
|
-
gap: 12px;
|
|
3011
|
-
background: #fafafa;
|
|
3012
|
-
border-bottom: 1px solid #eaeaef;
|
|
3013
|
-
font-size: 10px;
|
|
3014
|
-
font-weight: 700;
|
|
3015
|
-
text-transform: uppercase;
|
|
3016
|
-
letter-spacing: 0.08em;
|
|
3017
|
-
color: #8e8ea9;
|
|
3018
|
-
`;
|
|
3019
|
-
const ColumnHeaderRight = styled.div`
|
|
3020
|
-
margin-left: auto;
|
|
3021
|
-
font-size: 10px;
|
|
3022
|
-
font-weight: 700;
|
|
3023
|
-
text-transform: uppercase;
|
|
3024
|
-
letter-spacing: 0.08em;
|
|
3025
|
-
color: #8e8ea9;
|
|
3026
|
-
`;
|
|
3027
|
-
const UserGroup = styled.div`
|
|
3028
|
-
border-bottom: 1px solid #eaeaef;
|
|
3029
|
-
animation: ${fadeUp$1} 280ms ease both;
|
|
3030
|
-
animation-delay: ${(p) => (p.$i ?? 0) * 30}ms;
|
|
3031
|
-
&:last-child { border-bottom: none; }
|
|
3032
|
-
`;
|
|
3033
|
-
const UserRowHeader = styled.div`
|
|
3034
|
-
padding: 14px 20px;
|
|
3035
|
-
display: flex;
|
|
3036
|
-
align-items: center;
|
|
3037
|
-
gap: 12px;
|
|
3038
|
-
border-bottom: 1px solid #f5f5f9;
|
|
3039
|
-
cursor: default;
|
|
3040
|
-
&:hover { background: #fafafe; }
|
|
3041
|
-
`;
|
|
3042
|
-
const UserInfo = styled.div`
|
|
3043
|
-
flex: 1;
|
|
3044
|
-
display: flex;
|
|
3045
|
-
flex-direction: column;
|
|
3046
|
-
gap: 2px;
|
|
3047
|
-
`;
|
|
3048
|
-
const UserName$1 = styled.span`
|
|
3049
|
-
font-size: 12px;
|
|
3050
|
-
font-weight: 600;
|
|
3051
|
-
color: #32324d;
|
|
3052
|
-
`;
|
|
3053
|
-
const UserEmail = styled.span`
|
|
3054
|
-
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
3055
|
-
font-size: 11px;
|
|
3056
|
-
color: #8e8ea9;
|
|
3057
|
-
`;
|
|
3058
|
-
const SessionCountChip = styled.span`
|
|
3059
|
-
display: inline-flex;
|
|
3060
|
-
align-items: center;
|
|
3061
|
-
padding: 2px 8px;
|
|
3062
|
-
border-radius: 20px;
|
|
3063
|
-
font-size: 10px;
|
|
3064
|
-
font-weight: 700;
|
|
3065
|
-
background: #f0f0ff;
|
|
3066
|
-
color: #4945ff;
|
|
3067
|
-
`;
|
|
3068
|
-
const SessionCard = styled.div`
|
|
3069
|
-
padding: 10px 20px 10px 64px;
|
|
3070
|
-
border-top: 1px solid #f5f5f9;
|
|
3071
|
-
background: #fafafa;
|
|
3072
|
-
display: flex;
|
|
3073
|
-
align-items: flex-start;
|
|
3074
|
-
justify-content: space-between;
|
|
3075
|
-
gap: 16px;
|
|
3076
|
-
&:hover { background: #f5f5ff; }
|
|
3077
|
-
`;
|
|
3078
|
-
const SessionMeta = styled.div`
|
|
3079
|
-
display: flex;
|
|
3080
|
-
flex-direction: column;
|
|
3081
|
-
gap: 3px;
|
|
3082
|
-
flex: 1;
|
|
3083
|
-
min-width: 0;
|
|
3084
|
-
`;
|
|
3085
|
-
const IpChip = styled.span`
|
|
3086
|
-
display: inline-block;
|
|
3087
|
-
background: #f0f0ff;
|
|
3088
|
-
color: #4945ff;
|
|
3089
|
-
border-radius: 5px;
|
|
3090
|
-
padding: 2px 6px;
|
|
3091
|
-
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
3092
|
-
font-size: 11px;
|
|
3093
|
-
font-weight: 600;
|
|
3094
|
-
`;
|
|
3095
|
-
const TimestampText = styled.span`
|
|
3096
|
-
font-size: 11px;
|
|
3097
|
-
color: #8e8ea9;
|
|
3098
|
-
font-variant-numeric: tabular-nums;
|
|
3099
|
-
`;
|
|
3100
|
-
const AgentText = styled.span`
|
|
3101
|
-
font-size: 11px;
|
|
3102
|
-
color: #b8b8c7;
|
|
3103
|
-
overflow: hidden;
|
|
3104
|
-
text-overflow: ellipsis;
|
|
3105
|
-
white-space: nowrap;
|
|
3106
|
-
display: block;
|
|
3107
|
-
max-width: 400px;
|
|
3108
|
-
`;
|
|
3109
|
-
const EmptyState = styled.div`
|
|
3110
|
-
display: flex;
|
|
3111
|
-
justify-content: center;
|
|
3112
|
-
align-items: center;
|
|
3113
|
-
padding: 48px;
|
|
3114
|
-
font-size: 12px;
|
|
3115
|
-
color: #8e8ea9;
|
|
3116
|
-
`;
|
|
3117
|
-
function SessionsPage() {
|
|
3118
|
-
const qc = useQueryClient();
|
|
3119
|
-
const [page, setPage] = useState(1);
|
|
3120
|
-
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
3121
|
-
const [confirmRevokeSessionId, setConfirmRevokeSessionId] = useState(null);
|
|
3122
|
-
const [confirmRevokeMany, setConfirmRevokeMany] = useState(false);
|
|
3123
|
-
const sessionsQuery = useQuery({
|
|
3124
|
-
queryKey: ["dash-all-sessions", page],
|
|
3125
|
-
queryFn: async () => {
|
|
3126
|
-
const result = await client.dash.listAllSessions({});
|
|
3127
|
-
if (result.error)
|
|
3128
|
-
throw new Error(result.error.message ?? "Failed to load sessions");
|
|
3129
|
-
return result.data ?? [];
|
|
3130
|
-
},
|
|
3131
|
-
keepPreviousData: true
|
|
3132
|
-
});
|
|
3133
|
-
const revokeSessionMutation = useMutation({
|
|
3134
|
-
mutationFn: async (sessionId) => {
|
|
3135
|
-
const result = await client.dash.sessions.revoke(
|
|
3136
|
-
{},
|
|
3137
|
-
withContext({ sessionId })
|
|
3138
|
-
);
|
|
3139
|
-
if (result.error)
|
|
3140
|
-
throw new Error(result.error.message ?? "Revoke failed");
|
|
3141
|
-
},
|
|
3142
|
-
onSuccess: () => {
|
|
3143
|
-
setConfirmRevokeSessionId(null);
|
|
3144
|
-
qc.invalidateQueries({ queryKey: ["dash-all-sessions"] });
|
|
3145
|
-
}
|
|
3146
|
-
});
|
|
3147
|
-
const revokeManyMutation = useMutation({
|
|
3148
|
-
mutationFn: async (userIds) => {
|
|
3149
|
-
const result = await client.dash.sessions.revokeMany(
|
|
3150
|
-
{},
|
|
3151
|
-
withContext({ userIds })
|
|
3152
|
-
);
|
|
3153
|
-
if (result.error)
|
|
3154
|
-
throw new Error(result.error.message ?? "Revoke failed");
|
|
3155
|
-
return result.data;
|
|
3156
|
-
},
|
|
3157
|
-
onSuccess: () => {
|
|
3158
|
-
setConfirmRevokeMany(false);
|
|
3159
|
-
setSelected(/* @__PURE__ */ new Set());
|
|
3160
|
-
qc.invalidateQueries({ queryKey: ["dash-all-sessions"] });
|
|
3161
|
-
}
|
|
3162
|
-
});
|
|
3163
|
-
const usersWithSessions = sessionsQuery.data ?? [];
|
|
3164
|
-
const allUserIds = usersWithSessions.map((u) => u.id);
|
|
3165
|
-
const allSelected = allUserIds.length > 0 && allUserIds.every((id) => selected.has(id));
|
|
3166
|
-
const someSelected = selected.size > 0;
|
|
3167
|
-
const toggleSelect = (id) => {
|
|
3168
|
-
setSelected((prev) => {
|
|
3169
|
-
const next = new Set(prev);
|
|
3170
|
-
if (next.has(id)) next.delete(id);
|
|
3171
|
-
else next.add(id);
|
|
3172
|
-
return next;
|
|
3173
|
-
});
|
|
3174
|
-
};
|
|
3175
|
-
const handleSelectAll = () => {
|
|
3176
|
-
if (allSelected) setSelected(/* @__PURE__ */ new Set());
|
|
3177
|
-
else setSelected(new Set(allUserIds));
|
|
3178
|
-
};
|
|
3179
|
-
return /* @__PURE__ */ jsxs(Wrap$1, { "data-testid": "sessions-page", children: [
|
|
3180
|
-
/* @__PURE__ */ jsxs(PageHeader$1, { children: [
|
|
3181
|
-
/* @__PURE__ */ jsxs(TitleBlock$1, { children: [
|
|
3182
|
-
/* @__PURE__ */ jsx(PageTitle$1, { children: "Sessions" }),
|
|
3183
|
-
/* @__PURE__ */ jsx(PageSubtitle$1, { children: usersWithSessions.length > 0 ? `${usersWithSessions.length} user${usersWithSessions.length !== 1 ? "s" : ""} with active sessions` : "Active sessions across all users" })
|
|
3184
|
-
] }),
|
|
3185
|
-
someSelected && /* @__PURE__ */ jsxs(
|
|
3186
|
-
Button,
|
|
3187
|
-
{
|
|
3188
|
-
variant: "danger-light",
|
|
3189
|
-
size: "S",
|
|
3190
|
-
onClick: () => setConfirmRevokeMany(true),
|
|
3191
|
-
"data-testid": "revoke-selected-btn",
|
|
3192
|
-
children: [
|
|
3193
|
-
"Revoke sessions for ",
|
|
3194
|
-
selected.size,
|
|
3195
|
-
" user",
|
|
3196
|
-
selected.size !== 1 ? "s" : ""
|
|
3197
|
-
]
|
|
3198
|
-
}
|
|
3199
|
-
)
|
|
3200
|
-
] }),
|
|
3201
|
-
sessionsQuery.isError && /* @__PURE__ */ jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: sessionsQuery.error instanceof Error ? sessionsQuery.error.message : "An error occurred" }),
|
|
3202
|
-
/* @__PURE__ */ jsx(TableCard$1, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading sessions…" }) }) : usersWithSessions.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { children: "No active sessions" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3203
|
-
/* @__PURE__ */ jsxs(ColumnHeader, { children: [
|
|
3204
|
-
/* @__PURE__ */ jsx(
|
|
3205
|
-
Checkbox,
|
|
3206
|
-
{
|
|
3207
|
-
checked: allSelected,
|
|
3208
|
-
onCheckedChange: handleSelectAll,
|
|
3209
|
-
"aria-label": "Select all users"
|
|
3210
|
-
}
|
|
3211
|
-
),
|
|
3212
|
-
/* @__PURE__ */ jsx("span", { children: "User / Sessions" }),
|
|
3213
|
-
/* @__PURE__ */ jsx(ColumnHeaderRight, { children: "Sessions" })
|
|
3214
|
-
] }),
|
|
3215
|
-
usersWithSessions.map((userRow, i) => /* @__PURE__ */ jsxs(UserGroup, { $i: i, "data-testid": "session-user-row", children: [
|
|
3216
|
-
/* @__PURE__ */ jsxs(UserRowHeader, { children: [
|
|
3850
|
+
),
|
|
3217
3851
|
/* @__PURE__ */ jsx(
|
|
3218
|
-
|
|
3852
|
+
StatItem,
|
|
3219
3853
|
{
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3854
|
+
id: "d-sig",
|
|
3855
|
+
label: "Daily Sign-ups",
|
|
3856
|
+
value: stats.daily.signUps ?? 0,
|
|
3857
|
+
pct: stats.daily.percentage ?? void 0,
|
|
3858
|
+
sparkline: newSpark,
|
|
3859
|
+
color: T.green,
|
|
3860
|
+
delay: 1
|
|
3223
3861
|
}
|
|
3224
3862
|
),
|
|
3225
|
-
/* @__PURE__ */ jsx(
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3863
|
+
/* @__PURE__ */ jsx(
|
|
3864
|
+
StatItem,
|
|
3865
|
+
{
|
|
3866
|
+
id: "w-sig",
|
|
3867
|
+
label: "Weekly Sign-ups",
|
|
3868
|
+
value: stats.weekly.signUps ?? 0,
|
|
3869
|
+
pct: stats.weekly.percentage ?? void 0,
|
|
3870
|
+
sparkline: newSpark,
|
|
3871
|
+
color: T.green,
|
|
3872
|
+
delay: 2
|
|
3873
|
+
}
|
|
3874
|
+
),
|
|
3875
|
+
/* @__PURE__ */ jsx(
|
|
3876
|
+
StatItem,
|
|
3877
|
+
{
|
|
3878
|
+
id: "m-sig",
|
|
3879
|
+
label: "Monthly Sign-ups",
|
|
3880
|
+
value: stats.monthly.signUps ?? 0,
|
|
3881
|
+
pct: stats.monthly.percentage ?? void 0,
|
|
3882
|
+
sparkline: newSpark,
|
|
3883
|
+
color: T.green,
|
|
3884
|
+
delay: 3
|
|
3885
|
+
}
|
|
3886
|
+
),
|
|
3887
|
+
/* @__PURE__ */ jsx(
|
|
3888
|
+
StatItem,
|
|
3889
|
+
{
|
|
3890
|
+
id: "orgs",
|
|
3891
|
+
label: "Organizations",
|
|
3892
|
+
value: orgCount,
|
|
3893
|
+
color: T.purple,
|
|
3894
|
+
delay: 4
|
|
3895
|
+
}
|
|
3896
|
+
)
|
|
3235
3897
|
] }),
|
|
3236
|
-
|
|
3237
|
-
/* @__PURE__ */
|
|
3238
|
-
|
|
3898
|
+
/* @__PURE__ */ jsxs(SectionDivider, { children: [
|
|
3899
|
+
/* @__PURE__ */ jsx(DivLabel, { children: "Growth" }),
|
|
3900
|
+
/* @__PURE__ */ jsx(DivLine, {})
|
|
3901
|
+
] }),
|
|
3902
|
+
/* @__PURE__ */ jsxs(TwoPanel, { children: [
|
|
3903
|
+
/* @__PURE__ */ jsxs(ChartCard, { ref: chartRef, $delay: 5, children: [
|
|
3904
|
+
/* @__PURE__ */ jsxs(ChartHeader, { children: [
|
|
3905
|
+
/* @__PURE__ */ jsx(ChartTitle, { children: "User Growth" }),
|
|
3906
|
+
/* @__PURE__ */ jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxs(
|
|
3907
|
+
SeriesBtn,
|
|
3908
|
+
{
|
|
3909
|
+
$on: activeSeries.has(s.key),
|
|
3910
|
+
$c: s.color,
|
|
3911
|
+
onClick: () => toggleSeries(s.key),
|
|
3912
|
+
children: [
|
|
3913
|
+
/* @__PURE__ */ jsx(SDot, { $c: s.color }),
|
|
3914
|
+
s.label
|
|
3915
|
+
]
|
|
3916
|
+
},
|
|
3917
|
+
s.key
|
|
3918
|
+
)) })
|
|
3919
|
+
] }),
|
|
3920
|
+
/* @__PURE__ */ jsx(HoverInfo, { children: hovRow ? `${hovRow.label} · ${hovRow.totalUsers.toLocaleString()} total · +${hovRow.newUsers.toLocaleString()} new · ${hovRow.activeUsers.toLocaleString()} active` : "Hover the chart to inspect a data point" }),
|
|
3921
|
+
graphQuery.isLoading ? /* @__PURE__ */ jsx(
|
|
3239
3922
|
Flex,
|
|
3240
3923
|
{
|
|
3241
|
-
|
|
3924
|
+
justifyContent: "center",
|
|
3242
3925
|
alignItems: "center",
|
|
3243
|
-
style: {
|
|
3244
|
-
children:
|
|
3245
|
-
session.ipAddress && /* @__PURE__ */ jsx(IpChip, { children: session.ipAddress }),
|
|
3246
|
-
/* @__PURE__ */ jsxs(TimestampText, { children: [
|
|
3247
|
-
"Created ",
|
|
3248
|
-
new Date(session.createdAt).toLocaleString(),
|
|
3249
|
-
" ",
|
|
3250
|
-
"· Expires",
|
|
3251
|
-
" ",
|
|
3252
|
-
new Date(session.expiresAt).toLocaleString()
|
|
3253
|
-
] })
|
|
3254
|
-
]
|
|
3926
|
+
style: { flex: 1, minHeight: 170 },
|
|
3927
|
+
children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
|
|
3255
3928
|
}
|
|
3256
|
-
)
|
|
3257
|
-
|
|
3929
|
+
) : /* @__PURE__ */ jsx(
|
|
3930
|
+
GrowthChart,
|
|
3931
|
+
{
|
|
3932
|
+
data: graphData,
|
|
3933
|
+
hovered: hovIdx,
|
|
3934
|
+
onHover: setHovIdx,
|
|
3935
|
+
activeSeries
|
|
3936
|
+
}
|
|
3937
|
+
)
|
|
3258
3938
|
] }),
|
|
3259
|
-
/* @__PURE__ */
|
|
3260
|
-
|
|
3939
|
+
/* @__PURE__ */ jsxs(
|
|
3940
|
+
FeedCard,
|
|
3261
3941
|
{
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3942
|
+
$delay: 6,
|
|
3943
|
+
style: chartHeight > 0 ? { height: chartHeight } : void 0,
|
|
3944
|
+
children: [
|
|
3945
|
+
/* @__PURE__ */ jsxs(FeedHead, { children: [
|
|
3946
|
+
/* @__PURE__ */ jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recently Active" }),
|
|
3947
|
+
/* @__PURE__ */ jsxs(
|
|
3948
|
+
FeedSelect,
|
|
3949
|
+
{
|
|
3950
|
+
value: feedMode,
|
|
3951
|
+
onChange: (e) => setFeedMode(e.target.value),
|
|
3952
|
+
children: [
|
|
3953
|
+
/* @__PURE__ */ jsx("option", { value: "signups", children: "Recent Sign-ups" }),
|
|
3954
|
+
/* @__PURE__ */ jsx("option", { value: "active", children: "Recently Active" })
|
|
3955
|
+
]
|
|
3956
|
+
}
|
|
3957
|
+
)
|
|
3958
|
+
] }),
|
|
3959
|
+
/* @__PURE__ */ jsx(FeedScroll, { children: feedMode === "signups" ? usersQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : users.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No users yet" }) : users.map((u) => /* @__PURE__ */ jsxs(FeedItem, { children: [
|
|
3960
|
+
/* @__PURE__ */ jsxs(FeedTop, { children: [
|
|
3961
|
+
/* @__PURE__ */ jsxs(
|
|
3962
|
+
Flex,
|
|
3963
|
+
{
|
|
3964
|
+
alignItems: "center",
|
|
3965
|
+
gap: 1,
|
|
3966
|
+
style: { minWidth: 0 },
|
|
3967
|
+
children: [
|
|
3968
|
+
/* @__PURE__ */ jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
|
|
3969
|
+
/* @__PURE__ */ jsx(FeedName, { title: u.name, children: u.name })
|
|
3970
|
+
]
|
|
3971
|
+
}
|
|
3972
|
+
),
|
|
3973
|
+
/* @__PURE__ */ jsx(FeedMeta, { children: relTime(u.createdAt) })
|
|
3974
|
+
] }),
|
|
3975
|
+
/* @__PURE__ */ jsx(FeedEmail, { title: u.email, children: u.email })
|
|
3976
|
+
] }, u.id)) : activeUsersQuery.isLoading || sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : sortedActiveUsers.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No recent activity" }) : sortedActiveUsers.map((u) => /* @__PURE__ */ jsxs(FeedItem, { children: [
|
|
3977
|
+
/* @__PURE__ */ jsxs(FeedTop, { children: [
|
|
3978
|
+
/* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
|
|
3979
|
+
/* @__PURE__ */ jsx(
|
|
3980
|
+
Avatar,
|
|
3981
|
+
{
|
|
3982
|
+
name: u.name,
|
|
3983
|
+
src: u.image ?? void 0,
|
|
3984
|
+
size: 20
|
|
3985
|
+
}
|
|
3986
|
+
),
|
|
3987
|
+
/* @__PURE__ */ jsx(FeedName, { title: u.name, children: u.name })
|
|
3988
|
+
] }),
|
|
3989
|
+
/* @__PURE__ */ jsx(FeedMeta, { children: relTime(
|
|
3990
|
+
lastActiveByUserId.get(String(u.id)) ?? u.createdAt
|
|
3991
|
+
) })
|
|
3992
|
+
] }),
|
|
3993
|
+
/* @__PURE__ */ jsx(FeedEmail, { title: u.email, children: u.email })
|
|
3994
|
+
] }, u.documentId)) })
|
|
3995
|
+
]
|
|
3267
3996
|
}
|
|
3268
3997
|
)
|
|
3269
|
-
] },
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3998
|
+
] }),
|
|
3999
|
+
/* @__PURE__ */ jsxs(SectionDivider, { children: [
|
|
4000
|
+
/* @__PURE__ */ jsx(DivLabel, { children: "Retention & Activity" }),
|
|
4001
|
+
/* @__PURE__ */ jsx(DivLine, {})
|
|
4002
|
+
] }),
|
|
4003
|
+
/* @__PURE__ */ jsxs(TwoPanel, { children: [
|
|
4004
|
+
/* @__PURE__ */ jsxs(RtnCard, { $delay: 7, children: [
|
|
4005
|
+
/* @__PURE__ */ jsxs(ChartHeader, { children: [
|
|
4006
|
+
/* @__PURE__ */ jsx(ChartTitle, { children: "Cohort Retention" }),
|
|
4007
|
+
/* @__PURE__ */ jsx(Flex, { alignItems: "center", gap: 2, children: [
|
|
4008
|
+
{ hue: 142, label: "≥70%" },
|
|
4009
|
+
{ hue: 38, label: "40–70%" },
|
|
4010
|
+
{ hue: 4, label: "<40%" }
|
|
4011
|
+
].map(({ hue, label }) => /* @__PURE__ */ jsxs(
|
|
4012
|
+
Flex,
|
|
4013
|
+
{
|
|
4014
|
+
alignItems: "center",
|
|
4015
|
+
gap: 1,
|
|
4016
|
+
style: { marginLeft: 8 },
|
|
4017
|
+
children: [
|
|
4018
|
+
/* @__PURE__ */ jsx(
|
|
4019
|
+
"span",
|
|
4020
|
+
{
|
|
4021
|
+
style: {
|
|
4022
|
+
width: 8,
|
|
4023
|
+
height: 8,
|
|
4024
|
+
borderRadius: "50%",
|
|
4025
|
+
background: `hsl(${hue},60%,50%)`,
|
|
4026
|
+
display: "inline-block",
|
|
4027
|
+
flexShrink: 0
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
),
|
|
4031
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: T.textMuted }, children: label })
|
|
4032
|
+
]
|
|
4033
|
+
},
|
|
4034
|
+
hue
|
|
4035
|
+
)) })
|
|
4036
|
+
] }),
|
|
4037
|
+
/* @__PURE__ */ jsx(RtnTip, { children: rtnHovRow ? `Cohort ${rtnHovRow.label} · ${rtnHovRow.cohortSize.toLocaleString()} users · active ${rtnHovRow.activeStart} – ${rtnHovRow.activeEnd}` : "Hover a row to see cohort details" }),
|
|
4038
|
+
retentionQuery.isLoading ? /* @__PURE__ */ jsx(
|
|
4039
|
+
Flex,
|
|
4040
|
+
{
|
|
4041
|
+
justifyContent: "center",
|
|
4042
|
+
alignItems: "center",
|
|
4043
|
+
style: { minHeight: 80 },
|
|
4044
|
+
children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
|
|
4045
|
+
}
|
|
4046
|
+
) : rtnData.length === 0 ? /* @__PURE__ */ jsxs(Empty, { children: [
|
|
4047
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 22 }, children: "📉" }),
|
|
4048
|
+
/* @__PURE__ */ jsx("div", { children: "No retention data for this period" })
|
|
4049
|
+
] }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 5 }, children: rtnData.map((row, i) => {
|
|
4050
|
+
const hue = rateHue(row.retentionRate);
|
|
4051
|
+
return /* @__PURE__ */ jsxs(
|
|
4052
|
+
RtnRow,
|
|
4053
|
+
{
|
|
4054
|
+
$hov: rtnHov === i,
|
|
4055
|
+
onMouseEnter: () => setRtnHov(i),
|
|
4056
|
+
onMouseLeave: () => setRtnHov(null),
|
|
4057
|
+
children: [
|
|
4058
|
+
/* @__PURE__ */ jsx(RtnLabel, { children: row.label }),
|
|
4059
|
+
/* @__PURE__ */ jsx(RtnSize, { children: row.cohortSize.toLocaleString() }),
|
|
4060
|
+
/* @__PURE__ */ jsx(RtnTrack, { children: /* @__PURE__ */ jsx(RtnBar, { $w: row.retentionRate, $hue: hue }) }),
|
|
4061
|
+
/* @__PURE__ */ jsxs(RtnPct, { $hue: hue, children: [
|
|
4062
|
+
row.retentionRate.toFixed(1),
|
|
4063
|
+
"%"
|
|
4064
|
+
] })
|
|
4065
|
+
]
|
|
4066
|
+
},
|
|
4067
|
+
row.n
|
|
4068
|
+
);
|
|
4069
|
+
}) })
|
|
4070
|
+
] }),
|
|
4071
|
+
/* @__PURE__ */ jsx(RingGrid, { children: [
|
|
4072
|
+
{
|
|
4073
|
+
label: "Daily Active",
|
|
4074
|
+
value: stats.activeUsers.daily.active,
|
|
4075
|
+
pct: stats.activeUsers.daily.percentage,
|
|
4076
|
+
color: T.amber,
|
|
4077
|
+
sparkline: activeSpark,
|
|
4078
|
+
delay: 8
|
|
4079
|
+
},
|
|
4080
|
+
{
|
|
4081
|
+
label: "Weekly Active",
|
|
4082
|
+
value: stats.activeUsers.weekly.active,
|
|
4083
|
+
pct: stats.activeUsers.weekly.percentage,
|
|
4084
|
+
color: T.green,
|
|
4085
|
+
sparkline: activeSpark,
|
|
4086
|
+
delay: 9
|
|
4087
|
+
},
|
|
4088
|
+
{
|
|
4089
|
+
label: "Monthly Active",
|
|
4090
|
+
value: stats.activeUsers.monthly.active,
|
|
4091
|
+
pct: stats.activeUsers.monthly.percentage,
|
|
4092
|
+
color: T.accent,
|
|
4093
|
+
sparkline: activeSpark,
|
|
4094
|
+
delay: 10
|
|
4095
|
+
}
|
|
4096
|
+
].map(({ label, value, pct, color, delay }) => {
|
|
4097
|
+
const isPos = pct == null || pct >= 0;
|
|
4098
|
+
return /* @__PURE__ */ jsxs(RingCard, { $delay: delay, children: [
|
|
4099
|
+
/* @__PURE__ */ jsx(
|
|
4100
|
+
ProgressRing,
|
|
4101
|
+
{
|
|
4102
|
+
value: value ?? 0,
|
|
4103
|
+
max: activeMax,
|
|
4104
|
+
color,
|
|
4105
|
+
size: 56
|
|
4106
|
+
}
|
|
4107
|
+
),
|
|
4108
|
+
/* @__PURE__ */ jsxs(RingInfo, { children: [
|
|
4109
|
+
/* @__PURE__ */ jsx(RingLabel, { children: label }),
|
|
4110
|
+
/* @__PURE__ */ jsx(RingVal, { children: (value ?? 0).toLocaleString() }),
|
|
4111
|
+
pct != null && /* @__PURE__ */ jsxs(
|
|
4112
|
+
TrendBadge,
|
|
4113
|
+
{
|
|
4114
|
+
$pos: isPos,
|
|
4115
|
+
style: { marginTop: 4, marginBottom: 0 },
|
|
4116
|
+
children: [
|
|
4117
|
+
isPos ? "↑" : "↓",
|
|
4118
|
+
" ",
|
|
4119
|
+
Math.abs(pct).toFixed(1),
|
|
4120
|
+
"%"
|
|
4121
|
+
]
|
|
4122
|
+
}
|
|
4123
|
+
)
|
|
4124
|
+
] })
|
|
4125
|
+
] }, label);
|
|
4126
|
+
}) })
|
|
4127
|
+
] })
|
|
4128
|
+
] })
|
|
4129
|
+
}
|
|
4130
|
+
);
|
|
3316
4131
|
}
|
|
3317
4132
|
function useUsers(options = {}) {
|
|
3318
4133
|
const { limit = 25, offset = 0, search } = options;
|
|
@@ -3323,7 +4138,14 @@ function useUsers(options = {}) {
|
|
|
3323
4138
|
{
|
|
3324
4139
|
field: "email",
|
|
3325
4140
|
operator: "contains",
|
|
3326
|
-
value: search
|
|
4141
|
+
value: search,
|
|
4142
|
+
connector: "OR"
|
|
4143
|
+
},
|
|
4144
|
+
{
|
|
4145
|
+
field: "name",
|
|
4146
|
+
operator: "contains",
|
|
4147
|
+
value: search,
|
|
4148
|
+
connector: "OR"
|
|
3327
4149
|
}
|
|
3328
4150
|
]) : void 0;
|
|
3329
4151
|
const result = await client.dash.listUsers({
|
|
@@ -3552,6 +4374,45 @@ const ReadOnlyCodeInput = styled.div`
|
|
|
3552
4374
|
word-break: break-all;
|
|
3553
4375
|
overflow-wrap: anywhere;
|
|
3554
4376
|
`;
|
|
4377
|
+
const SessionCard = styled.div`
|
|
4378
|
+
display: flex;
|
|
4379
|
+
align-items: flex-start;
|
|
4380
|
+
justify-content: space-between;
|
|
4381
|
+
gap: 12px;
|
|
4382
|
+
padding: 10px 14px;
|
|
4383
|
+
background: white;
|
|
4384
|
+
border: 1px solid #eaeaef;
|
|
4385
|
+
border-radius: 8px;
|
|
4386
|
+
`;
|
|
4387
|
+
const SessionMeta = styled.div`
|
|
4388
|
+
display: flex;
|
|
4389
|
+
flex-direction: column;
|
|
4390
|
+
gap: 3px;
|
|
4391
|
+
flex: 1;
|
|
4392
|
+
min-width: 0;
|
|
4393
|
+
`;
|
|
4394
|
+
const IpChip = styled.span`
|
|
4395
|
+
display: inline-block;
|
|
4396
|
+
background: #f0f0ff;
|
|
4397
|
+
color: #4945ff;
|
|
4398
|
+
border-radius: 5px;
|
|
4399
|
+
padding: 2px 6px;
|
|
4400
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
4401
|
+
font-size: 11px;
|
|
4402
|
+
font-weight: 600;
|
|
4403
|
+
`;
|
|
4404
|
+
const TimestampText = styled.span`
|
|
4405
|
+
font-size: 11px;
|
|
4406
|
+
color: #8e8ea9;
|
|
4407
|
+
`;
|
|
4408
|
+
const AgentText = styled.span`
|
|
4409
|
+
font-size: 11px;
|
|
4410
|
+
color: #b8b8c7;
|
|
4411
|
+
overflow: hidden;
|
|
4412
|
+
text-overflow: ellipsis;
|
|
4413
|
+
white-space: nowrap;
|
|
4414
|
+
display: block;
|
|
4415
|
+
`;
|
|
3555
4416
|
const STANDARD_FIELDS = /* @__PURE__ */ new Set([
|
|
3556
4417
|
"id",
|
|
3557
4418
|
"name",
|
|
@@ -3573,11 +4434,15 @@ function UserDetailDrawer({
|
|
|
3573
4434
|
}) {
|
|
3574
4435
|
const qc = useQueryClient();
|
|
3575
4436
|
const { toggleNotification } = useNotification();
|
|
4437
|
+
const { get, put } = useFetchClient();
|
|
3576
4438
|
const schemaQuery = useModelSchema("user");
|
|
3577
4439
|
const userQuery = useQuery({
|
|
3578
4440
|
queryKey: ["dash-user", userId],
|
|
3579
4441
|
queryFn: async () => {
|
|
3580
|
-
const result = await client.dash.user(
|
|
4442
|
+
const result = await client.dash.user(
|
|
4443
|
+
{},
|
|
4444
|
+
withContext({ userId }, getAuthHeaders())
|
|
4445
|
+
);
|
|
3581
4446
|
if (result.error)
|
|
3582
4447
|
throw new Error(result.error.message ?? "Failed to load user");
|
|
3583
4448
|
return result.data;
|
|
@@ -3588,12 +4453,33 @@ function UserDetailDrawer({
|
|
|
3588
4453
|
queryFn: async () => {
|
|
3589
4454
|
const result = await client.dash.userOrganizations(
|
|
3590
4455
|
{},
|
|
3591
|
-
withContext({ userId })
|
|
4456
|
+
withContext({ userId }, getAuthHeaders())
|
|
3592
4457
|
);
|
|
3593
4458
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
3594
4459
|
return result.data?.organizations ?? [];
|
|
3595
4460
|
}
|
|
3596
4461
|
});
|
|
4462
|
+
const sessionsQuery = useQuery({
|
|
4463
|
+
queryKey: ["dash-user-sessions", userId],
|
|
4464
|
+
queryFn: async () => {
|
|
4465
|
+
const { data } = await get(
|
|
4466
|
+
`/better-auth-dashboard/db?uid=plugin::better-auth.session&filters[userId][$eq]=${userId}&sort[0]=createdAt:desc&pagination[pageSize]=50`
|
|
4467
|
+
);
|
|
4468
|
+
return data.results ?? [];
|
|
4469
|
+
}
|
|
4470
|
+
});
|
|
4471
|
+
const strapiUserQuery = useQuery({
|
|
4472
|
+
queryKey: ["dash-strapi-user", userId],
|
|
4473
|
+
enabled: !!schemaQuery.data,
|
|
4474
|
+
queryFn: async () => {
|
|
4475
|
+
const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
|
|
4476
|
+
const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
|
|
4477
|
+
const { data } = await get(
|
|
4478
|
+
`/better-auth-dashboard/db?uid=plugin::better-auth.user&filters[id][$eq]=${userId}&pagination[pageSize]=1${populateParam}`
|
|
4479
|
+
);
|
|
4480
|
+
return data.results?.[0] ?? null;
|
|
4481
|
+
}
|
|
4482
|
+
});
|
|
3597
4483
|
const [activeTab, setActiveTab] = useState("profile");
|
|
3598
4484
|
const [editName, setEditName] = useState(void 0);
|
|
3599
4485
|
const [editEmail, setEditEmail] = useState(void 0);
|
|
@@ -3604,6 +4490,8 @@ function UserDetailDrawer({
|
|
|
3604
4490
|
const [banReason, setBanReason] = useState("");
|
|
3605
4491
|
const [banExpiresDays, setBanExpiresDays] = useState("");
|
|
3606
4492
|
const [confirmRevokeAll, setConfirmRevokeAll] = useState(false);
|
|
4493
|
+
const [confirmRevokeSessionId, setConfirmRevokeSessionId] = useState(null);
|
|
4494
|
+
const [confirmBan, setConfirmBan] = useState(false);
|
|
3607
4495
|
const [confirmUnban, setConfirmUnban] = useState(false);
|
|
3608
4496
|
const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = useState(null);
|
|
3609
4497
|
const [confirmDisable2FA, setConfirmDisable2FA] = useState(false);
|
|
@@ -3622,26 +4510,33 @@ function UserDetailDrawer({
|
|
|
3622
4510
|
setEditExtra((prev) => ({ ...prev, [name]: value }));
|
|
3623
4511
|
};
|
|
3624
4512
|
const extraData = {
|
|
3625
|
-
...
|
|
4513
|
+
...strapiUserQuery.data ?? {},
|
|
3626
4514
|
...editExtra
|
|
3627
4515
|
};
|
|
3628
4516
|
const updateMutation = useMutation({
|
|
3629
4517
|
mutationFn: async () => {
|
|
3630
|
-
const body = {
|
|
4518
|
+
const body = {};
|
|
3631
4519
|
if (editName !== void 0) body.name = editName;
|
|
3632
4520
|
if (editEmail !== void 0) body.email = editEmail;
|
|
3633
4521
|
if (editEmailVerified !== void 0)
|
|
3634
4522
|
body.emailVerified = editEmailVerified;
|
|
3635
4523
|
if (editImage !== void 0) body.image = editImage;
|
|
3636
|
-
const
|
|
3637
|
-
|
|
3638
|
-
|
|
4524
|
+
const documentId = strapiUserQuery.data?.documentId;
|
|
4525
|
+
if (!documentId) throw new Error("Could not resolve documentId for user");
|
|
4526
|
+
const updateUser = await client.dash.updateUser(
|
|
4527
|
+
{ userId, ...body },
|
|
4528
|
+
withContext({ userId }, getAuthHeaders())
|
|
3639
4529
|
);
|
|
3640
|
-
if (
|
|
3641
|
-
|
|
3642
|
-
|
|
4530
|
+
if (!updateUser.data) throw new Error("Update failed");
|
|
4531
|
+
if (Object.keys(editExtra).length > 0) {
|
|
4532
|
+
await put(
|
|
4533
|
+
`/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
|
|
4534
|
+
editExtra
|
|
4535
|
+
);
|
|
4536
|
+
}
|
|
3643
4537
|
},
|
|
3644
|
-
onSuccess: () => {
|
|
4538
|
+
onSuccess: async () => {
|
|
4539
|
+
await qc.invalidateQueries({ queryKey: ["dash-strapi-user", userId] });
|
|
3645
4540
|
qc.invalidateQueries({ queryKey: ["dash-user", userId] });
|
|
3646
4541
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
3647
4542
|
setEditName(void 0);
|
|
@@ -3662,11 +4557,32 @@ function UserDetailDrawer({
|
|
|
3662
4557
|
});
|
|
3663
4558
|
}
|
|
3664
4559
|
});
|
|
4560
|
+
const revokeSessionMutation = useMutation({
|
|
4561
|
+
mutationFn: async (sessionId) => {
|
|
4562
|
+
const result = await client.dash.sessions.revoke(
|
|
4563
|
+
{},
|
|
4564
|
+
withContext({ sessionId, userId }, getAuthHeaders())
|
|
4565
|
+
);
|
|
4566
|
+
if (result.error)
|
|
4567
|
+
throw new Error(result.error.message ?? "Revoke failed");
|
|
4568
|
+
},
|
|
4569
|
+
onSuccess: () => {
|
|
4570
|
+
setConfirmRevokeSessionId(null);
|
|
4571
|
+
qc.invalidateQueries({ queryKey: ["dash-user-sessions", userId] });
|
|
4572
|
+
toggleNotification({ type: "success", message: "Session revoked" });
|
|
4573
|
+
},
|
|
4574
|
+
onError: (err) => {
|
|
4575
|
+
toggleNotification({
|
|
4576
|
+
type: "danger",
|
|
4577
|
+
message: err.message ?? "Failed to revoke session"
|
|
4578
|
+
});
|
|
4579
|
+
}
|
|
4580
|
+
});
|
|
3665
4581
|
const passwordMutation = useMutation({
|
|
3666
4582
|
mutationFn: async () => {
|
|
3667
4583
|
const result = await client.dash.setPassword(
|
|
3668
4584
|
{ password: newPassword },
|
|
3669
|
-
withContext({ userId })
|
|
4585
|
+
withContext({ userId }, getAuthHeaders())
|
|
3670
4586
|
);
|
|
3671
4587
|
if (result.error)
|
|
3672
4588
|
throw new Error(result.error.message ?? "Password update failed");
|
|
@@ -3694,11 +4610,15 @@ function UserDetailDrawer({
|
|
|
3694
4610
|
const days = parseInt(banExpiresDays, 10);
|
|
3695
4611
|
if (!Number.isNaN(days)) body.banExpires = Date.now() + days * 864e5;
|
|
3696
4612
|
}
|
|
3697
|
-
const result = await client.dash.banUser(
|
|
4613
|
+
const result = await client.dash.banUser(
|
|
4614
|
+
body,
|
|
4615
|
+
withContext({ userId }, getAuthHeaders())
|
|
4616
|
+
);
|
|
3698
4617
|
if (result.error) throw new Error(result.error.message ?? "Ban failed");
|
|
3699
4618
|
return result.data;
|
|
3700
4619
|
},
|
|
3701
4620
|
onSuccess: () => {
|
|
4621
|
+
setConfirmBan(false);
|
|
3702
4622
|
qc.invalidateQueries({ queryKey: ["dash-user", userId] });
|
|
3703
4623
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
3704
4624
|
setBanReason("");
|
|
@@ -3714,7 +4634,10 @@ function UserDetailDrawer({
|
|
|
3714
4634
|
});
|
|
3715
4635
|
const unbanMutation = useMutation({
|
|
3716
4636
|
mutationFn: async () => {
|
|
3717
|
-
const result = await client.dash.unbanUser(
|
|
4637
|
+
const result = await client.dash.unbanUser(
|
|
4638
|
+
{},
|
|
4639
|
+
withContext({ userId }, getAuthHeaders())
|
|
4640
|
+
);
|
|
3718
4641
|
if (result.error) throw new Error(result.error.message ?? "Unban failed");
|
|
3719
4642
|
return result.data;
|
|
3720
4643
|
},
|
|
@@ -3735,7 +4658,7 @@ function UserDetailDrawer({
|
|
|
3735
4658
|
mutationFn: async (providerId) => {
|
|
3736
4659
|
const result = await client.dash.unlinkAccount(
|
|
3737
4660
|
{ providerId },
|
|
3738
|
-
withContext({ userId })
|
|
4661
|
+
withContext({ userId }, getAuthHeaders())
|
|
3739
4662
|
);
|
|
3740
4663
|
if (result.error)
|
|
3741
4664
|
throw new Error(result.error.message ?? "Failed to unlink account");
|
|
@@ -3756,7 +4679,7 @@ function UserDetailDrawer({
|
|
|
3756
4679
|
mutationFn: async () => {
|
|
3757
4680
|
const result = await client.dash.sessions.revokeAll(
|
|
3758
4681
|
{ userId },
|
|
3759
|
-
withContext({})
|
|
4682
|
+
withContext({}, getAuthHeaders())
|
|
3760
4683
|
);
|
|
3761
4684
|
if (result.error)
|
|
3762
4685
|
throw new Error(result.error.message ?? "Revoke failed");
|
|
@@ -3778,7 +4701,7 @@ function UserDetailDrawer({
|
|
|
3778
4701
|
const callbackUrl = new URL(window.location.href, window.location.origin);
|
|
3779
4702
|
const result = await client.dash.sendVerificationEmail(
|
|
3780
4703
|
{ callbackUrl: callbackUrl.toString() },
|
|
3781
|
-
withContext({ userId })
|
|
4704
|
+
withContext({ userId }, getAuthHeaders())
|
|
3782
4705
|
);
|
|
3783
4706
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
3784
4707
|
return result.data;
|
|
@@ -3801,7 +4724,7 @@ function UserDetailDrawer({
|
|
|
3801
4724
|
const callbackUrl = new URL(window.location.href, window.location.origin);
|
|
3802
4725
|
const result = await client.dash.sendResetPasswordEmail(
|
|
3803
4726
|
{ callbackUrl: callbackUrl.toString() },
|
|
3804
|
-
withContext({ userId })
|
|
4727
|
+
withContext({ userId }, getAuthHeaders())
|
|
3805
4728
|
);
|
|
3806
4729
|
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
3807
4730
|
return result.data;
|
|
@@ -3823,7 +4746,7 @@ function UserDetailDrawer({
|
|
|
3823
4746
|
mutationFn: async () => {
|
|
3824
4747
|
const result = await client.dash.enableTwoFactor(
|
|
3825
4748
|
{},
|
|
3826
|
-
withContext({ userId })
|
|
4749
|
+
withContext({ userId }, getAuthHeaders())
|
|
3827
4750
|
);
|
|
3828
4751
|
if (result.error)
|
|
3829
4752
|
throw new Error(result.error.message ?? "Failed to enable 2FA");
|
|
@@ -3848,7 +4771,7 @@ function UserDetailDrawer({
|
|
|
3848
4771
|
mutationFn: async () => {
|
|
3849
4772
|
const result = await client.dash.disableTwoFactor(
|
|
3850
4773
|
{},
|
|
3851
|
-
withContext({ userId })
|
|
4774
|
+
withContext({ userId }, getAuthHeaders())
|
|
3852
4775
|
);
|
|
3853
4776
|
if (result.error)
|
|
3854
4777
|
throw new Error(result.error.message ?? "Failed to disable 2FA");
|
|
@@ -3876,7 +4799,7 @@ function UserDetailDrawer({
|
|
|
3876
4799
|
mutationFn: async () => {
|
|
3877
4800
|
const result = await client.dash.viewTwoFactorTotpUri(
|
|
3878
4801
|
{},
|
|
3879
|
-
withContext({ userId })
|
|
4802
|
+
withContext({ userId }, getAuthHeaders())
|
|
3880
4803
|
);
|
|
3881
4804
|
if (result.error)
|
|
3882
4805
|
throw new Error(result.error.message ?? "Failed to load TOTP URI");
|
|
@@ -3899,7 +4822,7 @@ function UserDetailDrawer({
|
|
|
3899
4822
|
mutationFn: async () => {
|
|
3900
4823
|
const result = await client.dash.viewBackupCodes(
|
|
3901
4824
|
{},
|
|
3902
|
-
withContext({ userId })
|
|
4825
|
+
withContext({ userId }, getAuthHeaders())
|
|
3903
4826
|
);
|
|
3904
4827
|
if (result.error)
|
|
3905
4828
|
throw new Error(result.error.message ?? "Failed to load backup codes");
|
|
@@ -3922,7 +4845,7 @@ function UserDetailDrawer({
|
|
|
3922
4845
|
mutationFn: async () => {
|
|
3923
4846
|
const result = await client.dash.generateBackupCodes(
|
|
3924
4847
|
{},
|
|
3925
|
-
withContext({ userId })
|
|
4848
|
+
withContext({ userId }, getAuthHeaders())
|
|
3926
4849
|
);
|
|
3927
4850
|
if (result.error)
|
|
3928
4851
|
throw new Error(
|
|
@@ -4118,7 +5041,15 @@ function UserDetailDrawer({
|
|
|
4118
5041
|
}
|
|
4119
5042
|
)
|
|
4120
5043
|
] })
|
|
4121
|
-
] })
|
|
5044
|
+
] }),
|
|
5045
|
+
/* @__PURE__ */ jsx(
|
|
5046
|
+
EditViewSidePanels,
|
|
5047
|
+
{
|
|
5048
|
+
model: "plugin::better-auth.user",
|
|
5049
|
+
documentId: strapiUserQuery.data?.documentId,
|
|
5050
|
+
document: strapiUserQuery.data ?? void 0
|
|
5051
|
+
}
|
|
5052
|
+
)
|
|
4122
5053
|
] })
|
|
4123
5054
|
] }) }),
|
|
4124
5055
|
/* @__PURE__ */ jsx(Tabs.Content, { value: "security", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
|
|
@@ -4314,7 +5245,8 @@ function UserDetailDrawer({
|
|
|
4314
5245
|
] })
|
|
4315
5246
|
] })
|
|
4316
5247
|
] }) }),
|
|
4317
|
-
banEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap:
|
|
5248
|
+
banEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxs(FormSection, { children: [
|
|
5249
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Ban status" }),
|
|
4318
5250
|
/* @__PURE__ */ jsxs(WarnCard, { children: [
|
|
4319
5251
|
/* @__PURE__ */ jsx(
|
|
4320
5252
|
Typography,
|
|
@@ -4329,7 +5261,7 @@ function UserDetailDrawer({
|
|
|
4329
5261
|
"Reason: ",
|
|
4330
5262
|
user.banReason
|
|
4331
5263
|
] }),
|
|
4332
|
-
user.banExpires
|
|
5264
|
+
user.banExpires ? /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
|
|
4333
5265
|
"Expires:",
|
|
4334
5266
|
" ",
|
|
4335
5267
|
new Date(user.banExpires).toLocaleDateString(
|
|
@@ -4341,8 +5273,7 @@ function UserDetailDrawer({
|
|
|
4341
5273
|
day: "numeric"
|
|
4342
5274
|
}
|
|
4343
5275
|
)
|
|
4344
|
-
] }),
|
|
4345
|
-
!user.banExpires && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
|
|
5276
|
+
] }) : /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
|
|
4346
5277
|
] }),
|
|
4347
5278
|
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
|
|
4348
5279
|
Button,
|
|
@@ -4352,27 +5283,28 @@ function UserDetailDrawer({
|
|
|
4352
5283
|
children: "Lift ban"
|
|
4353
5284
|
}
|
|
4354
5285
|
) })
|
|
4355
|
-
] }) : /* @__PURE__ */ jsxs(
|
|
5286
|
+
] }) : /* @__PURE__ */ jsxs(FormSection, { children: [
|
|
5287
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Apply ban" }),
|
|
5288
|
+
/* @__PURE__ */ jsxs(
|
|
5289
|
+
Field.Root,
|
|
5290
|
+
{
|
|
5291
|
+
hint: "Optional — will be shown to the user",
|
|
5292
|
+
style: { width: "100%" },
|
|
5293
|
+
children: [
|
|
5294
|
+
/* @__PURE__ */ jsx(Field.Label, { children: "Reason" }),
|
|
5295
|
+
/* @__PURE__ */ jsx(
|
|
5296
|
+
TextInput,
|
|
5297
|
+
{
|
|
5298
|
+
value: banReason,
|
|
5299
|
+
onChange: (e) => setBanReason(e.target.value),
|
|
5300
|
+
placeholder: "e.g. Violated terms of service"
|
|
5301
|
+
}
|
|
5302
|
+
),
|
|
5303
|
+
/* @__PURE__ */ jsx(Field.Hint, {})
|
|
5304
|
+
]
|
|
5305
|
+
}
|
|
5306
|
+
),
|
|
4356
5307
|
/* @__PURE__ */ jsxs(Grid.Root, { gap: 4, children: [
|
|
4357
|
-
/* @__PURE__ */ jsx(Grid.Item, { col: 12, children: /* @__PURE__ */ jsxs(
|
|
4358
|
-
Field.Root,
|
|
4359
|
-
{
|
|
4360
|
-
hint: "Optional — will be shown to the user",
|
|
4361
|
-
style: { width: "100%" },
|
|
4362
|
-
children: [
|
|
4363
|
-
/* @__PURE__ */ jsx(Field.Label, { children: "Reason" }),
|
|
4364
|
-
/* @__PURE__ */ jsx(
|
|
4365
|
-
TextInput,
|
|
4366
|
-
{
|
|
4367
|
-
value: banReason,
|
|
4368
|
-
onChange: (e) => setBanReason(e.target.value),
|
|
4369
|
-
placeholder: "e.g. Violated terms of service"
|
|
4370
|
-
}
|
|
4371
|
-
),
|
|
4372
|
-
/* @__PURE__ */ jsx(Field.Hint, {})
|
|
4373
|
-
]
|
|
4374
|
-
}
|
|
4375
|
-
) }),
|
|
4376
5308
|
/* @__PURE__ */ jsx(Grid.Item, { col: 6, children: /* @__PURE__ */ jsxs(
|
|
4377
5309
|
Field.Root,
|
|
4378
5310
|
{
|
|
@@ -4393,24 +5325,13 @@ function UserDetailDrawer({
|
|
|
4393
5325
|
]
|
|
4394
5326
|
}
|
|
4395
5327
|
) }),
|
|
4396
|
-
/* @__PURE__ */ jsx(Grid.Item, { col: 6, children:
|
|
4397
|
-
Flex,
|
|
4398
|
-
{
|
|
4399
|
-
direction: "column",
|
|
4400
|
-
justifyContent: "flex-end",
|
|
4401
|
-
style: { height: "100%", paddingBottom: 4 },
|
|
4402
|
-
children: /* @__PURE__ */ jsxs(PreviewPill, { children: [
|
|
4403
|
-
"⏱ Expires ",
|
|
4404
|
-
banExpiryPreview
|
|
4405
|
-
] })
|
|
4406
|
-
}
|
|
4407
|
-
) : /* @__PURE__ */ jsx(
|
|
5328
|
+
/* @__PURE__ */ jsx(Grid.Item, { col: 6, children: /* @__PURE__ */ jsx(
|
|
4408
5329
|
Flex,
|
|
4409
5330
|
{
|
|
4410
5331
|
direction: "column",
|
|
4411
5332
|
justifyContent: "flex-end",
|
|
4412
5333
|
style: { height: "100%", paddingBottom: 4 },
|
|
4413
|
-
children: /* @__PURE__ */ jsx(PreviewPill, { children: "♾ Permanent ban" })
|
|
5334
|
+
children: /* @__PURE__ */ jsx(PreviewPill, { children: banExpiryPreview ? `⏱ Expires ${banExpiryPreview}` : "♾ Permanent ban" })
|
|
4414
5335
|
}
|
|
4415
5336
|
) })
|
|
4416
5337
|
] }),
|
|
@@ -4418,31 +5339,68 @@ function UserDetailDrawer({
|
|
|
4418
5339
|
Button,
|
|
4419
5340
|
{
|
|
4420
5341
|
variant: "danger",
|
|
4421
|
-
|
|
4422
|
-
onClick: () => banMutation.mutate(),
|
|
5342
|
+
onClick: () => setConfirmBan(true),
|
|
4423
5343
|
children: "Ban user"
|
|
4424
5344
|
}
|
|
4425
5345
|
) })
|
|
4426
5346
|
] }) }) }),
|
|
4427
|
-
/* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", children: /* @__PURE__ */
|
|
4428
|
-
/* @__PURE__ */
|
|
4429
|
-
|
|
4430
|
-
/* @__PURE__ */ jsxs(
|
|
4431
|
-
/* @__PURE__ */
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
5347
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
|
|
5348
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
5349
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Session management" }),
|
|
5350
|
+
/* @__PURE__ */ jsxs(AccountRow, { children: [
|
|
5351
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
|
|
5352
|
+
/* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
|
|
5353
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
|
|
5354
|
+
] }),
|
|
5355
|
+
/* @__PURE__ */ jsx(
|
|
5356
|
+
Button,
|
|
5357
|
+
{
|
|
5358
|
+
variant: "danger-light",
|
|
5359
|
+
size: "S",
|
|
5360
|
+
onClick: () => setConfirmRevokeAll(true),
|
|
5361
|
+
style: { flexShrink: 0 },
|
|
5362
|
+
children: "Revoke all"
|
|
5363
|
+
}
|
|
5364
|
+
)
|
|
5365
|
+
] })
|
|
5366
|
+
] }),
|
|
5367
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
5368
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Active sessions" }),
|
|
5369
|
+
sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading sessions…" }) }) : (sessionsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "No active sessions." }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxs(SessionCard, { children: [
|
|
5370
|
+
/* @__PURE__ */ jsxs(SessionMeta, { children: [
|
|
5371
|
+
/* @__PURE__ */ jsxs(
|
|
5372
|
+
Flex,
|
|
5373
|
+
{
|
|
5374
|
+
gap: 2,
|
|
5375
|
+
alignItems: "center",
|
|
5376
|
+
style: { flexWrap: "wrap" },
|
|
5377
|
+
children: [
|
|
5378
|
+
session.ipAddress && /* @__PURE__ */ jsx(IpChip, { children: session.ipAddress }),
|
|
5379
|
+
/* @__PURE__ */ jsxs(TimestampText, { children: [
|
|
5380
|
+
"Created",
|
|
5381
|
+
" ",
|
|
5382
|
+
new Date(session.createdAt).toLocaleString(),
|
|
5383
|
+
" · Expires",
|
|
5384
|
+
" ",
|
|
5385
|
+
new Date(session.expiresAt).toLocaleString()
|
|
5386
|
+
] })
|
|
5387
|
+
]
|
|
5388
|
+
}
|
|
5389
|
+
),
|
|
5390
|
+
session.userAgent && /* @__PURE__ */ jsx(AgentText, { children: session.userAgent })
|
|
5391
|
+
] }),
|
|
5392
|
+
/* @__PURE__ */ jsx(
|
|
5393
|
+
IconButton,
|
|
5394
|
+
{
|
|
5395
|
+
label: "Revoke session",
|
|
5396
|
+
onClick: () => setConfirmRevokeSessionId(String(session.id)),
|
|
5397
|
+
style: { flexShrink: 0 },
|
|
5398
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
5399
|
+
}
|
|
5400
|
+
)
|
|
5401
|
+
] }, session.documentId)) })
|
|
4444
5402
|
] })
|
|
4445
|
-
] }) })
|
|
5403
|
+
] }) }),
|
|
4446
5404
|
/* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxs(FormSection, { children: [
|
|
4447
5405
|
/* @__PURE__ */ jsx(SectionLabel, { children: "Memberships" }),
|
|
4448
5406
|
orgsQuery.isLoading ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral500", children: "Loading…" }) : (orgsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Not a member of any organizations." }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: (orgsQuery.data ?? []).map(
|
|
@@ -4479,6 +5437,29 @@ function UserDetailDrawer({
|
|
|
4479
5437
|
onCancel: () => setConfirmRevokeAll(false)
|
|
4480
5438
|
}
|
|
4481
5439
|
),
|
|
5440
|
+
confirmRevokeSessionId && /* @__PURE__ */ jsx(
|
|
5441
|
+
ConfirmDialog,
|
|
5442
|
+
{
|
|
5443
|
+
title: "Revoke session",
|
|
5444
|
+
message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
|
|
5445
|
+
confirmLabel: "Revoke",
|
|
5446
|
+
loading: revokeSessionMutation.isLoading,
|
|
5447
|
+
onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
|
|
5448
|
+
onCancel: () => setConfirmRevokeSessionId(null)
|
|
5449
|
+
}
|
|
5450
|
+
),
|
|
5451
|
+
confirmBan && /* @__PURE__ */ jsx(
|
|
5452
|
+
ConfirmDialog,
|
|
5453
|
+
{
|
|
5454
|
+
title: "Ban user",
|
|
5455
|
+
message: banReason ? `Ban this user for the following reason: "${banReason}"? They will be prevented from signing in.` : "Are you sure you want to ban this user? They will be prevented from signing in.",
|
|
5456
|
+
confirmLabel: "Ban user",
|
|
5457
|
+
variant: "danger",
|
|
5458
|
+
loading: banMutation.isLoading,
|
|
5459
|
+
onConfirm: () => banMutation.mutate(),
|
|
5460
|
+
onCancel: () => setConfirmBan(false)
|
|
5461
|
+
}
|
|
5462
|
+
),
|
|
4482
5463
|
confirmUnban && /* @__PURE__ */ jsx(
|
|
4483
5464
|
ConfirmDialog,
|
|
4484
5465
|
{
|
|
@@ -4708,7 +5689,9 @@ const Toolbar = styled.div`
|
|
|
4708
5689
|
gap: 12px;
|
|
4709
5690
|
`;
|
|
4710
5691
|
function UsersPage({ config }) {
|
|
5692
|
+
const rbac = useRBAC(PERMISSIONS.user);
|
|
4711
5693
|
const qc = useQueryClient();
|
|
5694
|
+
const { toggleNotification } = useNotification();
|
|
4712
5695
|
const banEnabled = hasPlugin(config, "admin");
|
|
4713
5696
|
const emailVerificationEnabled = config.emailVerification.sendVerificationEmailEnabled;
|
|
4714
5697
|
const twoFactorEnabled = hasPlugin(config, "two-factor");
|
|
@@ -4732,7 +5715,10 @@ function UsersPage({ config }) {
|
|
|
4732
5715
|
const pageCount = Math.ceil(total / PAGE_SIZE);
|
|
4733
5716
|
const deleteMutation = useMutation({
|
|
4734
5717
|
mutationFn: async (userId) => {
|
|
4735
|
-
const result = await client.dash.deleteUser(
|
|
5718
|
+
const result = await client.dash.deleteUser(
|
|
5719
|
+
{},
|
|
5720
|
+
withContext({ userId }, getAuthHeaders())
|
|
5721
|
+
);
|
|
4736
5722
|
if (result.error)
|
|
4737
5723
|
throw new Error(result.error.message ?? "Delete failed");
|
|
4738
5724
|
},
|
|
@@ -4740,38 +5726,65 @@ function UsersPage({ config }) {
|
|
|
4740
5726
|
setConfirmDelete(null);
|
|
4741
5727
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
4742
5728
|
qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
|
|
5729
|
+
toggleNotification({ type: "success", message: "User deleted" });
|
|
5730
|
+
},
|
|
5731
|
+
onError: (err) => {
|
|
5732
|
+
toggleNotification({
|
|
5733
|
+
type: "danger",
|
|
5734
|
+
message: err.message ?? "Failed to delete user"
|
|
5735
|
+
});
|
|
4743
5736
|
}
|
|
4744
5737
|
});
|
|
4745
5738
|
const deleteManyMutation = useMutation({
|
|
4746
5739
|
mutationFn: async (userIds) => {
|
|
4747
5740
|
const result = await client.dash.deleteManyUsers(
|
|
4748
5741
|
{},
|
|
4749
|
-
withContext({ userIds })
|
|
5742
|
+
withContext({ userIds }, getAuthHeaders())
|
|
4750
5743
|
);
|
|
4751
5744
|
if (result.error)
|
|
4752
5745
|
throw new Error(result.error.message ?? "Delete failed");
|
|
4753
5746
|
return result.data;
|
|
4754
5747
|
},
|
|
4755
|
-
onSuccess: () => {
|
|
5748
|
+
onSuccess: (_data, userIds) => {
|
|
4756
5749
|
setConfirmDeleteMany(false);
|
|
4757
5750
|
setSelected(/* @__PURE__ */ new Set());
|
|
4758
5751
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
4759
5752
|
qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
|
|
5753
|
+
toggleNotification({
|
|
5754
|
+
type: "success",
|
|
5755
|
+
message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} deleted`
|
|
5756
|
+
});
|
|
5757
|
+
},
|
|
5758
|
+
onError: (err) => {
|
|
5759
|
+
toggleNotification({
|
|
5760
|
+
type: "danger",
|
|
5761
|
+
message: err.message ?? "Failed to delete users"
|
|
5762
|
+
});
|
|
4760
5763
|
}
|
|
4761
5764
|
});
|
|
4762
5765
|
const banManyMutation = useMutation({
|
|
4763
5766
|
mutationFn: async (userIds) => {
|
|
4764
5767
|
const result = await client.dash.banManyUsers(
|
|
4765
5768
|
{},
|
|
4766
|
-
withContext({ userIds })
|
|
5769
|
+
withContext({ userIds }, getAuthHeaders())
|
|
4767
5770
|
);
|
|
4768
5771
|
if (result.error) throw new Error(result.error.message ?? "Ban failed");
|
|
4769
5772
|
return result.data;
|
|
4770
5773
|
},
|
|
4771
|
-
onSuccess: () => {
|
|
5774
|
+
onSuccess: (_data, userIds) => {
|
|
4772
5775
|
setConfirmBanMany(false);
|
|
4773
5776
|
setSelected(/* @__PURE__ */ new Set());
|
|
4774
5777
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
5778
|
+
toggleNotification({
|
|
5779
|
+
type: "success",
|
|
5780
|
+
message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} banned`
|
|
5781
|
+
});
|
|
5782
|
+
},
|
|
5783
|
+
onError: (err) => {
|
|
5784
|
+
toggleNotification({
|
|
5785
|
+
type: "danger",
|
|
5786
|
+
message: err.message ?? "Failed to ban users"
|
|
5787
|
+
});
|
|
4775
5788
|
}
|
|
4776
5789
|
});
|
|
4777
5790
|
const toggleSelect = (id) => {
|
|
@@ -4796,334 +5809,244 @@ function UsersPage({ config }) {
|
|
|
4796
5809
|
setSearch(searchInput);
|
|
4797
5810
|
setPage(1);
|
|
4798
5811
|
};
|
|
4799
|
-
return /* @__PURE__ */
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
) }),
|
|
4878
|
-
/* @__PURE__ */ jsx(TH, { children: "Name" }),
|
|
4879
|
-
/* @__PURE__ */ jsx(TH, { children: "Email" }),
|
|
4880
|
-
/* @__PURE__ */ jsx(TH, { children: "Status" }),
|
|
4881
|
-
/* @__PURE__ */ jsx(TH, { children: "Created" }),
|
|
4882
|
-
/* @__PURE__ */ jsx(THActions, {})
|
|
4883
|
-
] }) }),
|
|
4884
|
-
/* @__PURE__ */ jsx("tbody", { children: users.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
|
|
4885
|
-
TD,
|
|
4886
|
-
{
|
|
4887
|
-
colSpan: 6,
|
|
4888
|
-
style: {
|
|
4889
|
-
textAlign: "center",
|
|
4890
|
-
padding: "40px",
|
|
4891
|
-
color: "#8e8ea9"
|
|
4892
|
-
},
|
|
4893
|
-
"data-testid": "users-empty",
|
|
4894
|
-
children: search ? `No users matching "${search}"` : "No users found"
|
|
4895
|
-
}
|
|
4896
|
-
) }) : users.map((user, i) => /* @__PURE__ */ jsxs(
|
|
4897
|
-
TR,
|
|
4898
|
-
{
|
|
4899
|
-
$selected: selected.has(user.id),
|
|
4900
|
-
$i: i,
|
|
4901
|
-
"data-testid": "user-row",
|
|
4902
|
-
children: [
|
|
4903
|
-
/* @__PURE__ */ jsx(TDCheck, { children: /* @__PURE__ */ jsx(
|
|
5812
|
+
return /* @__PURE__ */ jsx(
|
|
5813
|
+
Page.Protect,
|
|
5814
|
+
{
|
|
5815
|
+
permissions: PERMISSIONS.user.filter(
|
|
5816
|
+
(p) => p.action === `plugin::${PLUGIN_ID}.user.read`
|
|
5817
|
+
),
|
|
5818
|
+
children: /* @__PURE__ */ jsxs(Wrap, { "data-testid": "users-page", children: [
|
|
5819
|
+
/* @__PURE__ */ jsxs(PageHeader, { children: [
|
|
5820
|
+
/* @__PURE__ */ jsxs(TitleBlock, { children: [
|
|
5821
|
+
/* @__PURE__ */ jsx(PageTitle, { children: "Users" }),
|
|
5822
|
+
/* @__PURE__ */ jsxs(PageSubtitle, { children: [
|
|
5823
|
+
total.toLocaleString(),
|
|
5824
|
+
" total",
|
|
5825
|
+
data?.onlineUsers ? ` · ${data.onlineUsers} online` : ""
|
|
5826
|
+
] })
|
|
5827
|
+
] }),
|
|
5828
|
+
rbac.allowedActions.canCreate && /* @__PURE__ */ jsx(
|
|
5829
|
+
Button,
|
|
5830
|
+
{
|
|
5831
|
+
startIcon: /* @__PURE__ */ jsx(Plus, {}),
|
|
5832
|
+
onClick: () => setShowCreate(true),
|
|
5833
|
+
"data-testid": "create-user-btn",
|
|
5834
|
+
children: "Create user"
|
|
5835
|
+
}
|
|
5836
|
+
)
|
|
5837
|
+
] }),
|
|
5838
|
+
/* @__PURE__ */ jsxs(Toolbar, { children: [
|
|
5839
|
+
/* @__PURE__ */ jsx(SearchForm, { onSubmit: handleSearch, children: /* @__PURE__ */ jsx(
|
|
5840
|
+
Searchbar,
|
|
5841
|
+
{
|
|
5842
|
+
clearLabel: "Clear",
|
|
5843
|
+
name: "search",
|
|
5844
|
+
placeholder: "Search by email…",
|
|
5845
|
+
value: searchInput,
|
|
5846
|
+
onChange: (e) => setSearchInput(e.target.value),
|
|
5847
|
+
onClear: () => {
|
|
5848
|
+
setSearchInput("");
|
|
5849
|
+
setSearch("");
|
|
5850
|
+
setPage(1);
|
|
5851
|
+
},
|
|
5852
|
+
"data-testid": "user-search",
|
|
5853
|
+
children: "Search users"
|
|
5854
|
+
}
|
|
5855
|
+
) }),
|
|
5856
|
+
someSelected && /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
5857
|
+
rbac.allowedActions.canDelete && /* @__PURE__ */ jsxs(
|
|
5858
|
+
Button,
|
|
5859
|
+
{
|
|
5860
|
+
variant: "danger-light",
|
|
5861
|
+
size: "S",
|
|
5862
|
+
onClick: () => setConfirmDeleteMany(true),
|
|
5863
|
+
"data-testid": "delete-selected-btn",
|
|
5864
|
+
children: [
|
|
5865
|
+
"Delete ",
|
|
5866
|
+
selected.size,
|
|
5867
|
+
" selected"
|
|
5868
|
+
]
|
|
5869
|
+
}
|
|
5870
|
+
),
|
|
5871
|
+
banEnabled && rbac.allowedActions.canUpdate && /* @__PURE__ */ jsxs(
|
|
5872
|
+
Button,
|
|
5873
|
+
{
|
|
5874
|
+
variant: "secondary",
|
|
5875
|
+
size: "S",
|
|
5876
|
+
onClick: () => setConfirmBanMany(true),
|
|
5877
|
+
children: [
|
|
5878
|
+
"Ban ",
|
|
5879
|
+
selected.size,
|
|
5880
|
+
" selected"
|
|
5881
|
+
]
|
|
5882
|
+
}
|
|
5883
|
+
)
|
|
5884
|
+
] })
|
|
5885
|
+
] }),
|
|
5886
|
+
isError && /* @__PURE__ */ jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: error?.message }),
|
|
5887
|
+
/* @__PURE__ */ jsx(TableCard, { children: isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading users…" }) }) : /* @__PURE__ */ jsxs(Table, { children: [
|
|
5888
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
5889
|
+
(rbac.allowedActions.canDelete || rbac.allowedActions.canUpdate) && /* @__PURE__ */ jsx(THCheck, { children: /* @__PURE__ */ jsx(
|
|
4904
5890
|
Checkbox,
|
|
4905
5891
|
{
|
|
4906
|
-
checked:
|
|
4907
|
-
onCheckedChange:
|
|
4908
|
-
"aria-label":
|
|
5892
|
+
checked: allSelected,
|
|
5893
|
+
onCheckedChange: handleSelectAll,
|
|
5894
|
+
"aria-label": "Select all"
|
|
4909
5895
|
}
|
|
4910
5896
|
) }),
|
|
4911
|
-
/* @__PURE__ */ jsx(
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
),
|
|
4920
|
-
/* @__PURE__ */ jsx(UserName, { children: user.name })
|
|
4921
|
-
] }) }),
|
|
4922
|
-
/* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsx(MonoText, { children: user.email }) }),
|
|
4923
|
-
/* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
|
|
4924
|
-
user.emailVerified ? /* @__PURE__ */ jsx(StatusChip, { $variant: "verified", children: "Verified" }) : /* @__PURE__ */ jsx(StatusChip, { $variant: "unverified", children: "Unverified" }),
|
|
4925
|
-
user.banned && /* @__PURE__ */ jsx(StatusChip, { $variant: "banned", children: "Banned" })
|
|
4926
|
-
] }) }),
|
|
4927
|
-
/* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsx(DateText, { children: new Date(user.createdAt).toLocaleDateString() }) }),
|
|
4928
|
-
/* @__PURE__ */ jsx(TDActions, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", children: [
|
|
4929
|
-
/* @__PURE__ */ jsx(
|
|
4930
|
-
IconButton,
|
|
4931
|
-
{
|
|
4932
|
-
label: "Edit user",
|
|
4933
|
-
onClick: () => setDetailUserId(user.id),
|
|
4934
|
-
"data-testid": "edit-user-btn",
|
|
4935
|
-
children: /* @__PURE__ */ jsx(Pencil, {})
|
|
4936
|
-
}
|
|
4937
|
-
),
|
|
4938
|
-
/* @__PURE__ */ jsx(
|
|
4939
|
-
IconButton,
|
|
4940
|
-
{
|
|
4941
|
-
label: "Delete user",
|
|
4942
|
-
onClick: () => setConfirmDelete(user.id),
|
|
4943
|
-
"data-testid": "delete-user-btn",
|
|
4944
|
-
children: /* @__PURE__ */ jsx(Trash, {})
|
|
4945
|
-
}
|
|
4946
|
-
)
|
|
4947
|
-
] }) })
|
|
4948
|
-
]
|
|
4949
|
-
},
|
|
4950
|
-
user.id
|
|
4951
|
-
)) })
|
|
4952
|
-
] }) }),
|
|
4953
|
-
pageCount > 1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsx(
|
|
4954
|
-
Pagination,
|
|
4955
|
-
{
|
|
4956
|
-
activePage: page,
|
|
4957
|
-
pageCount,
|
|
4958
|
-
onChangePage: setPage
|
|
4959
|
-
}
|
|
4960
|
-
) }),
|
|
4961
|
-
showCreate && /* @__PURE__ */ jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
|
|
4962
|
-
detailUserId && /* @__PURE__ */ jsx(
|
|
4963
|
-
UserDetailDrawer,
|
|
4964
|
-
{
|
|
4965
|
-
userId: detailUserId,
|
|
4966
|
-
banEnabled,
|
|
4967
|
-
emailVerificationEnabled,
|
|
4968
|
-
twoFactorEnabled,
|
|
4969
|
-
onClose: () => setDetailUserId(null)
|
|
4970
|
-
}
|
|
4971
|
-
),
|
|
4972
|
-
confirmDelete && /* @__PURE__ */ jsx(
|
|
4973
|
-
ConfirmDialog,
|
|
4974
|
-
{
|
|
4975
|
-
title: "Delete user",
|
|
4976
|
-
message: "Are you sure you want to delete this user? This action cannot be undone.",
|
|
4977
|
-
confirmLabel: "Delete",
|
|
4978
|
-
loading: deleteMutation.isLoading,
|
|
4979
|
-
onConfirm: () => deleteMutation.mutate(confirmDelete),
|
|
4980
|
-
onCancel: () => setConfirmDelete(null)
|
|
4981
|
-
}
|
|
4982
|
-
),
|
|
4983
|
-
confirmDeleteMany && /* @__PURE__ */ jsx(
|
|
4984
|
-
ConfirmDialog,
|
|
4985
|
-
{
|
|
4986
|
-
title: `Delete ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
|
|
4987
|
-
message: `Are you sure you want to delete ${selected.size} user${selected.size !== 1 ? "s" : ""}? This action cannot be undone.`,
|
|
4988
|
-
confirmLabel: "Delete all",
|
|
4989
|
-
loading: deleteManyMutation.isLoading,
|
|
4990
|
-
onConfirm: () => deleteManyMutation.mutate([...selected]),
|
|
4991
|
-
onCancel: () => setConfirmDeleteMany(false)
|
|
4992
|
-
}
|
|
4993
|
-
),
|
|
4994
|
-
confirmBanMany && /* @__PURE__ */ jsx(
|
|
4995
|
-
ConfirmDialog,
|
|
4996
|
-
{
|
|
4997
|
-
title: `Ban ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
|
|
4998
|
-
message: `Are you sure you want to ban ${selected.size} user${selected.size !== 1 ? "s" : ""}? They will be prevented from signing in.`,
|
|
4999
|
-
confirmLabel: "Ban all",
|
|
5000
|
-
variant: "danger",
|
|
5001
|
-
loading: banManyMutation.isLoading,
|
|
5002
|
-
onConfirm: () => banManyMutation.mutate([...selected]),
|
|
5003
|
-
onCancel: () => setConfirmBanMany(false)
|
|
5004
|
-
}
|
|
5005
|
-
)
|
|
5006
|
-
] });
|
|
5007
|
-
}
|
|
5008
|
-
const Accent = styled.div`
|
|
5009
|
-
height: 3px;
|
|
5010
|
-
background: linear-gradient(90deg, #4945ff 0%, #7b79ff 55%, #9593ff 100%);
|
|
5011
|
-
flex-shrink: 0;
|
|
5012
|
-
`;
|
|
5013
|
-
const BrandIcon = styled.div`
|
|
5014
|
-
width: 30px;
|
|
5015
|
-
height: 30px;
|
|
5016
|
-
border-radius: 7px;
|
|
5017
|
-
background: linear-gradient(135deg, #4945ff 0%, #7b79ff 100%);
|
|
5018
|
-
display: flex;
|
|
5019
|
-
align-items: center;
|
|
5020
|
-
justify-content: center;
|
|
5021
|
-
color: white;
|
|
5022
|
-
font-size: 12px;
|
|
5023
|
-
font-weight: 900;
|
|
5024
|
-
flex-shrink: 0;
|
|
5025
|
-
letter-spacing: -0.06em;
|
|
5026
|
-
user-select: none;
|
|
5027
|
-
`;
|
|
5028
|
-
const PathTag = styled.code`
|
|
5029
|
-
padding: 2px 8px;
|
|
5030
|
-
background: #f0f0ff;
|
|
5031
|
-
border: 1px solid #d9d8ff;
|
|
5032
|
-
border-radius: 4px;
|
|
5033
|
-
font-size: 11px;
|
|
5034
|
-
color: #4945ff;
|
|
5035
|
-
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
5036
|
-
font-weight: 500;
|
|
5037
|
-
`;
|
|
5038
|
-
function App() {
|
|
5039
|
-
const { data: config, isLoading, isError, error } = useDashConfig();
|
|
5040
|
-
const orgEnabled = hasPlugin(config, "organization");
|
|
5041
|
-
const orgOptionsQuery = useQuery({
|
|
5042
|
-
queryKey: ["dash-org-options"],
|
|
5043
|
-
queryFn: async () => {
|
|
5044
|
-
const result = await client.dash.organization.options();
|
|
5045
|
-
if (result.error) return { teamsEnabled: false };
|
|
5046
|
-
return result.data ?? { teamsEnabled: false };
|
|
5047
|
-
},
|
|
5048
|
-
enabled: orgEnabled
|
|
5049
|
-
});
|
|
5050
|
-
const teamsEnabled = orgEnabled ? orgOptionsQuery.data?.teamsEnabled ?? false : false;
|
|
5051
|
-
if (isLoading) {
|
|
5052
|
-
return /* @__PURE__ */ jsx(Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsx(Loader, { children: "Loading Better Auth dashboard…" }) });
|
|
5053
|
-
}
|
|
5054
|
-
if (isError) {
|
|
5055
|
-
return /* @__PURE__ */ jsx(Box, { padding: 6, children: /* @__PURE__ */ jsx(
|
|
5056
|
-
Alert,
|
|
5057
|
-
{
|
|
5058
|
-
closeLabel: "Close",
|
|
5059
|
-
title: "Error loading configuration",
|
|
5060
|
-
variant: "danger",
|
|
5061
|
-
children: error?.message
|
|
5062
|
-
}
|
|
5063
|
-
) });
|
|
5064
|
-
}
|
|
5065
|
-
if (!config) return null;
|
|
5066
|
-
return /* @__PURE__ */ jsx(Box, { background: "neutral100", minHeight: "100vh", "data-testid": "dashboard-root", children: /* @__PURE__ */ jsxs(Tabs.Root, { defaultValue: "overview", children: [
|
|
5067
|
-
/* @__PURE__ */ jsxs(
|
|
5068
|
-
Box,
|
|
5069
|
-
{
|
|
5070
|
-
background: "neutral0",
|
|
5071
|
-
borderColor: "neutral150",
|
|
5072
|
-
borderStyle: "solid",
|
|
5073
|
-
borderWidth: "1px",
|
|
5074
|
-
style: { overflow: "hidden" },
|
|
5075
|
-
children: [
|
|
5076
|
-
/* @__PURE__ */ jsx(Accent, {}),
|
|
5077
|
-
/* @__PURE__ */ jsxs(
|
|
5078
|
-
Box,
|
|
5897
|
+
/* @__PURE__ */ jsx(TH, { children: "Name" }),
|
|
5898
|
+
/* @__PURE__ */ jsx(TH, { children: "Email" }),
|
|
5899
|
+
/* @__PURE__ */ jsx(TH, { children: "Status" }),
|
|
5900
|
+
/* @__PURE__ */ jsx(TH, { children: "Created" }),
|
|
5901
|
+
/* @__PURE__ */ jsx(THActions, {})
|
|
5902
|
+
] }) }),
|
|
5903
|
+
/* @__PURE__ */ jsx("tbody", { children: users.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
|
|
5904
|
+
TD,
|
|
5079
5905
|
{
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5906
|
+
colSpan: 6,
|
|
5907
|
+
style: {
|
|
5908
|
+
textAlign: "center",
|
|
5909
|
+
padding: "40px",
|
|
5910
|
+
color: "#8e8ea9"
|
|
5911
|
+
},
|
|
5912
|
+
"data-testid": "users-empty",
|
|
5913
|
+
children: search ? `No users matching "${search}"` : "No users found"
|
|
5914
|
+
}
|
|
5915
|
+
) }) : users.map((user, i) => /* @__PURE__ */ jsxs(
|
|
5916
|
+
TR,
|
|
5917
|
+
{
|
|
5918
|
+
$selected: selected.has(user.id),
|
|
5919
|
+
$i: i,
|
|
5920
|
+
"data-testid": "user-row",
|
|
5084
5921
|
children: [
|
|
5085
|
-
/* @__PURE__ */
|
|
5086
|
-
|
|
5922
|
+
(rbac.allowedActions.canDelete || rbac.allowedActions.canUpdate) && /* @__PURE__ */ jsx(TDCheck, { children: /* @__PURE__ */ jsx(
|
|
5923
|
+
Checkbox,
|
|
5087
5924
|
{
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
children: [
|
|
5092
|
-
/* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
|
|
5093
|
-
/* @__PURE__ */ jsx(BrandIcon, { children: "BA" }),
|
|
5094
|
-
/* @__PURE__ */ jsxs(Box, { children: [
|
|
5095
|
-
/* @__PURE__ */ jsx(Typography, { variant: "beta", textColor: "neutral800", children: "Better Auth" }),
|
|
5096
|
-
/* @__PURE__ */ jsx(Box, { paddingTop: "2px", children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Authentication Dashboard" }) })
|
|
5097
|
-
] })
|
|
5098
|
-
] }),
|
|
5099
|
-
/* @__PURE__ */ jsx(PathTag, { children: config.basePath })
|
|
5100
|
-
]
|
|
5925
|
+
checked: selected.has(user.id),
|
|
5926
|
+
onCheckedChange: () => toggleSelect(user.id),
|
|
5927
|
+
"aria-label": `Select ${user.name}`
|
|
5101
5928
|
}
|
|
5102
|
-
),
|
|
5103
|
-
/* @__PURE__ */
|
|
5104
|
-
/* @__PURE__ */ jsx(
|
|
5105
|
-
|
|
5106
|
-
orgEnabled && /* @__PURE__ */ jsx(
|
|
5107
|
-
Tabs.Trigger,
|
|
5929
|
+
) }),
|
|
5930
|
+
/* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
|
|
5931
|
+
/* @__PURE__ */ jsx(
|
|
5932
|
+
Avatar,
|
|
5108
5933
|
{
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5934
|
+
name: user.name ?? "",
|
|
5935
|
+
src: user.image,
|
|
5936
|
+
size: 28
|
|
5112
5937
|
}
|
|
5113
5938
|
),
|
|
5114
|
-
/* @__PURE__ */ jsx(
|
|
5115
|
-
] })
|
|
5939
|
+
/* @__PURE__ */ jsx(UserName, { children: user.name })
|
|
5940
|
+
] }) }),
|
|
5941
|
+
/* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsx(MonoText, { children: user.email }) }),
|
|
5942
|
+
/* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
|
|
5943
|
+
user.emailVerified ? /* @__PURE__ */ jsx(StatusChip, { $variant: "verified", children: "Verified" }) : /* @__PURE__ */ jsx(StatusChip, { $variant: "unverified", children: "Unverified" }),
|
|
5944
|
+
user.banned && /* @__PURE__ */ jsx(StatusChip, { $variant: "banned", children: "Banned" })
|
|
5945
|
+
] }) }),
|
|
5946
|
+
/* @__PURE__ */ jsx(TD, { children: /* @__PURE__ */ jsx(DateText, { children: new Date(user.createdAt).toLocaleDateString() }) }),
|
|
5947
|
+
/* @__PURE__ */ jsx(TDActions, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", children: [
|
|
5948
|
+
rbac.allowedActions.canUpdate && /* @__PURE__ */ jsx(
|
|
5949
|
+
IconButton,
|
|
5950
|
+
{
|
|
5951
|
+
label: "Edit user",
|
|
5952
|
+
onClick: () => setDetailUserId(user.id),
|
|
5953
|
+
"data-testid": "edit-user-btn",
|
|
5954
|
+
children: /* @__PURE__ */ jsx(Pencil, {})
|
|
5955
|
+
}
|
|
5956
|
+
),
|
|
5957
|
+
rbac.allowedActions.canDelete && /* @__PURE__ */ jsx(
|
|
5958
|
+
IconButton,
|
|
5959
|
+
{
|
|
5960
|
+
label: "Delete user",
|
|
5961
|
+
onClick: () => setConfirmDelete(user.id),
|
|
5962
|
+
"data-testid": "delete-user-btn",
|
|
5963
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
5964
|
+
}
|
|
5965
|
+
)
|
|
5966
|
+
] }) })
|
|
5116
5967
|
]
|
|
5968
|
+
},
|
|
5969
|
+
user.id
|
|
5970
|
+
)) })
|
|
5971
|
+
] }) }),
|
|
5972
|
+
pageCount > 1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
5973
|
+
/* @__PURE__ */ jsx(
|
|
5974
|
+
Button,
|
|
5975
|
+
{
|
|
5976
|
+
variant: "tertiary",
|
|
5977
|
+
size: "S",
|
|
5978
|
+
disabled: page === 1,
|
|
5979
|
+
onClick: () => setPage((p) => p - 1),
|
|
5980
|
+
children: "Previous"
|
|
5981
|
+
}
|
|
5982
|
+
),
|
|
5983
|
+
/* @__PURE__ */ jsx(
|
|
5984
|
+
Button,
|
|
5985
|
+
{
|
|
5986
|
+
variant: "tertiary",
|
|
5987
|
+
size: "S",
|
|
5988
|
+
disabled: page >= pageCount,
|
|
5989
|
+
onClick: () => setPage((p) => p + 1),
|
|
5990
|
+
children: "Next"
|
|
5117
5991
|
}
|
|
5118
5992
|
)
|
|
5119
|
-
]
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5993
|
+
] }) }),
|
|
5994
|
+
rbac.allowedActions.canCreate && showCreate && /* @__PURE__ */ jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
|
|
5995
|
+
rbac.allowedActions.canUpdate && detailUserId && /* @__PURE__ */ jsx(
|
|
5996
|
+
UserDetailDrawer,
|
|
5997
|
+
{
|
|
5998
|
+
userId: detailUserId,
|
|
5999
|
+
banEnabled,
|
|
6000
|
+
emailVerificationEnabled,
|
|
6001
|
+
twoFactorEnabled,
|
|
6002
|
+
onClose: () => setDetailUserId(null)
|
|
6003
|
+
}
|
|
6004
|
+
),
|
|
6005
|
+
rbac.allowedActions.canDelete && confirmDelete && /* @__PURE__ */ jsx(
|
|
6006
|
+
ConfirmDialog,
|
|
6007
|
+
{
|
|
6008
|
+
title: "Delete user",
|
|
6009
|
+
message: "Are you sure you want to delete this user? This action cannot be undone.",
|
|
6010
|
+
confirmLabel: "Delete",
|
|
6011
|
+
loading: deleteMutation.isLoading,
|
|
6012
|
+
onConfirm: () => deleteMutation.mutate(confirmDelete),
|
|
6013
|
+
onCancel: () => setConfirmDelete(null)
|
|
6014
|
+
}
|
|
6015
|
+
),
|
|
6016
|
+
rbac.allowedActions.canDelete && confirmDeleteMany && /* @__PURE__ */ jsx(
|
|
6017
|
+
ConfirmDialog,
|
|
6018
|
+
{
|
|
6019
|
+
title: `Delete ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
|
|
6020
|
+
message: `Are you sure you want to delete ${selected.size} user${selected.size !== 1 ? "s" : ""}? This action cannot be undone.`,
|
|
6021
|
+
confirmLabel: "Delete all",
|
|
6022
|
+
loading: deleteManyMutation.isLoading,
|
|
6023
|
+
onConfirm: () => deleteManyMutation.mutate([...selected]),
|
|
6024
|
+
onCancel: () => setConfirmDeleteMany(false)
|
|
6025
|
+
}
|
|
6026
|
+
),
|
|
6027
|
+
rbac.allowedActions.canUpdate && confirmBanMany && /* @__PURE__ */ jsx(
|
|
6028
|
+
ConfirmDialog,
|
|
6029
|
+
{
|
|
6030
|
+
title: `Ban ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
|
|
6031
|
+
message: `Are you sure you want to ban ${selected.size} user${selected.size !== 1 ? "s" : ""}? They will be prevented from signing in.`,
|
|
6032
|
+
confirmLabel: "Ban all",
|
|
6033
|
+
variant: "danger",
|
|
6034
|
+
loading: banManyMutation.isLoading,
|
|
6035
|
+
onConfirm: () => banManyMutation.mutate([...selected]),
|
|
6036
|
+
onCancel: () => setConfirmBanMany(false)
|
|
6037
|
+
}
|
|
6038
|
+
)
|
|
6039
|
+
] })
|
|
6040
|
+
}
|
|
6041
|
+
);
|
|
6042
|
+
}
|
|
6043
|
+
function UsersRoute() {
|
|
6044
|
+
const { config } = useOutletContext();
|
|
6045
|
+
return /* @__PURE__ */ jsx(UsersPage, { config });
|
|
6046
|
+
}
|
|
6047
|
+
function OrganizationsRoute() {
|
|
6048
|
+
const { teamsEnabled } = useOutletContext();
|
|
6049
|
+
return /* @__PURE__ */ jsx(OrganizationsPage, { teamsEnabled });
|
|
5127
6050
|
}
|
|
5128
6051
|
function Root() {
|
|
5129
6052
|
const queryClient = useMemo(
|
|
@@ -5138,7 +6061,12 @@ function Root() {
|
|
|
5138
6061
|
}),
|
|
5139
6062
|
[]
|
|
5140
6063
|
);
|
|
5141
|
-
return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(
|
|
6064
|
+
return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(Routes, { children: /* @__PURE__ */ jsxs(Route, { path: "/", element: /* @__PURE__ */ jsx(ProtectedApp, {}), children: [
|
|
6065
|
+
/* @__PURE__ */ jsx(Route, { index: true, element: /* @__PURE__ */ jsx(Navigate, { to: "overview", replace: true }) }),
|
|
6066
|
+
/* @__PURE__ */ jsx(Route, { path: "overview", element: /* @__PURE__ */ jsx(OverviewPage, {}) }),
|
|
6067
|
+
/* @__PURE__ */ jsx(Route, { path: "users", element: /* @__PURE__ */ jsx(UsersRoute, {}) }),
|
|
6068
|
+
/* @__PURE__ */ jsx(Route, { path: "organizations", element: /* @__PURE__ */ jsx(OrganizationsRoute, {}) })
|
|
6069
|
+
] }) }) });
|
|
5142
6070
|
}
|
|
5143
6071
|
export {
|
|
5144
6072
|
Root,
|