@strapi-community/plugin-better-auth-dashboard 1.0.0-alpha.1 → 1.0.0-alpha.3
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/dist/admin/{Root-Bl4iPGDu.js → Root-BnRbzS-u.js} +1285 -854
- package/dist/admin/{Root-hwPhIfaT.mjs → Root-DBjGZL7H.mjs} +1287 -856
- package/dist/admin/index-BpruO4vo.mjs +67 -0
- package/dist/admin/index-xZ2FHX3i.js +66 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +99 -2
- package/dist/server/index.mjs +99 -2
- package/package.json +1 -1
- package/dist/admin/index-A9PUvldu.js +0 -26
- package/dist/admin/index-Cvcysa5M.mjs +0 -27
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState, useMemo } from "react";
|
|
2
|
+
import { useEffect, useState, useRef, useMemo } from "react";
|
|
3
3
|
import { useQuery, useQueryClient, useMutation, QueryClient, QueryClientProvider } from "react-query";
|
|
4
4
|
import { Modal, Typography, Button, Portal, Flex, IconButton, Box, Field, TextInput, Combobox, ComboboxOption, Checkbox, SingleSelect, SingleSelectOption, NumberInput, JSONInput, Textarea, Alert, Tabs, Grid, SearchForm, Searchbar, Loader, Badge, Pagination } from "@strapi/design-system";
|
|
5
5
|
import styled, { keyframes } from "styled-components";
|
|
6
6
|
import { dashClient } from "@better-auth/infra/client";
|
|
7
7
|
import { createAuthClient, InferPlugin } from "better-auth/client";
|
|
8
|
-
import { Cross, Images,
|
|
8
|
+
import { Cross, Images, Trash, Plus, Pencil } from "@strapi/icons";
|
|
9
9
|
import { useNotification, useFetchClient } from "@strapi/strapi/admin";
|
|
10
|
-
import { g as getMediaLibraryComponent } from "./index-
|
|
10
|
+
import { g as getMediaLibraryComponent, P as PluginIcon } from "./index-BpruO4vo.mjs";
|
|
11
11
|
const dashPathMethods = () => ({
|
|
12
12
|
id: "dash-path-methods",
|
|
13
13
|
pathMethods: {
|
|
@@ -454,7 +454,7 @@ function UserCombobox({
|
|
|
454
454
|
]) : void 0;
|
|
455
455
|
const result = await client.dash.listUsers({
|
|
456
456
|
query: {
|
|
457
|
-
limit: 20,
|
|
457
|
+
limit: search ? 100 : 20,
|
|
458
458
|
offset: 0,
|
|
459
459
|
sortBy: "createdAt",
|
|
460
460
|
sortOrder: "desc",
|
|
@@ -629,6 +629,159 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
|
|
|
629
629
|
function slugify(str) {
|
|
630
630
|
return str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
631
631
|
}
|
|
632
|
+
const EntryRow = styled.div`
|
|
633
|
+
display: flex;
|
|
634
|
+
align-items: center;
|
|
635
|
+
justify-content: space-between;
|
|
636
|
+
gap: 8px;
|
|
637
|
+
padding: 8px 12px;
|
|
638
|
+
width: 100%;
|
|
639
|
+
box-sizing: border-box;
|
|
640
|
+
background: #ffffff;
|
|
641
|
+
border: 1px solid ${({ theme }) => theme.colors?.neutral200 ?? "#dcdce4"};
|
|
642
|
+
border-radius: 4px;
|
|
643
|
+
min-width: 0;
|
|
644
|
+
`;
|
|
645
|
+
function getDisplayLabel(doc) {
|
|
646
|
+
for (const key of ["name", "title", "email", "username", "label", "slug"]) {
|
|
647
|
+
const v = doc[key];
|
|
648
|
+
if (typeof v === "string" && v.trim()) return v;
|
|
649
|
+
}
|
|
650
|
+
return String(doc.documentId ?? doc.id ?? "");
|
|
651
|
+
}
|
|
652
|
+
function normalizeValue(val, cache) {
|
|
653
|
+
if (!val) return [];
|
|
654
|
+
if (typeof val === "object" && !Array.isArray(val) && val !== null && "set" in val) {
|
|
655
|
+
const items2 = val.set ?? [];
|
|
656
|
+
return items2.filter((item) => item?.documentId).map((item) => ({
|
|
657
|
+
documentId: item.documentId,
|
|
658
|
+
label: cache.get(item.documentId) ?? item.documentId
|
|
659
|
+
}));
|
|
660
|
+
}
|
|
661
|
+
const items = Array.isArray(val) ? val : [val];
|
|
662
|
+
return items.filter(
|
|
663
|
+
(item) => typeof item === "object" && item !== null && "documentId" in item
|
|
664
|
+
).map((item) => ({
|
|
665
|
+
documentId: String(item.documentId),
|
|
666
|
+
label: getDisplayLabel(item)
|
|
667
|
+
}));
|
|
668
|
+
}
|
|
669
|
+
function RelationField({
|
|
670
|
+
name,
|
|
671
|
+
label,
|
|
672
|
+
attribute,
|
|
673
|
+
value,
|
|
674
|
+
onChange,
|
|
675
|
+
readOnly = false
|
|
676
|
+
}) {
|
|
677
|
+
const { get } = useFetchClient();
|
|
678
|
+
const cache = useRef(/* @__PURE__ */ new Map());
|
|
679
|
+
const [search, setSearch] = useState("");
|
|
680
|
+
const [addKey, setAddKey] = useState(0);
|
|
681
|
+
const target = attribute.target ?? "";
|
|
682
|
+
const relationType = attribute.relation ?? "manyToOne";
|
|
683
|
+
const isMulti = relationType === "oneToMany" || relationType === "manyToMany";
|
|
684
|
+
const currentDocs = normalizeValue(value, cache.current);
|
|
685
|
+
const selectedIds = new Set(currentDocs.map((d) => d.documentId));
|
|
686
|
+
const { data: searchResults = [], isLoading } = useQuery({
|
|
687
|
+
queryKey: ["relation-search", target, search],
|
|
688
|
+
queryFn: async () => {
|
|
689
|
+
const params = new URLSearchParams({
|
|
690
|
+
uid: target,
|
|
691
|
+
"pagination[pageSize]": "50"
|
|
692
|
+
});
|
|
693
|
+
if (search) params.set("filters[name][$containsi]", search);
|
|
694
|
+
const { data } = await get(
|
|
695
|
+
`/better-auth-dashboard/db?${params}`
|
|
696
|
+
);
|
|
697
|
+
const docs = data.results ?? [];
|
|
698
|
+
for (const doc of docs) {
|
|
699
|
+
if (doc.documentId) {
|
|
700
|
+
cache.current.set(String(doc.documentId), getDisplayLabel(doc));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return docs.map((doc) => ({
|
|
704
|
+
documentId: String(doc.documentId ?? ""),
|
|
705
|
+
label: getDisplayLabel(doc)
|
|
706
|
+
})).filter((d) => d.documentId);
|
|
707
|
+
},
|
|
708
|
+
keepPreviousData: true,
|
|
709
|
+
enabled: !readOnly
|
|
710
|
+
});
|
|
711
|
+
const availableOptions = searchResults.filter(
|
|
712
|
+
(r) => !selectedIds.has(r.documentId)
|
|
713
|
+
);
|
|
714
|
+
const commit = (ids) => {
|
|
715
|
+
onChange(name, { set: ids.map((id) => ({ documentId: id })) });
|
|
716
|
+
};
|
|
717
|
+
const handleSelect = (val) => {
|
|
718
|
+
if (!val) return;
|
|
719
|
+
const opt = searchResults.find((r) => r.documentId === val);
|
|
720
|
+
if (opt) cache.current.set(val, opt.label);
|
|
721
|
+
if (isMulti) {
|
|
722
|
+
if (selectedIds.has(val)) return;
|
|
723
|
+
commit([...currentDocs.map((d) => d.documentId), val]);
|
|
724
|
+
} else {
|
|
725
|
+
commit([val]);
|
|
726
|
+
}
|
|
727
|
+
setSearch("");
|
|
728
|
+
setAddKey((k) => k + 1);
|
|
729
|
+
};
|
|
730
|
+
const handleRemove = (docId) => {
|
|
731
|
+
commit(
|
|
732
|
+
currentDocs.filter((d) => d.documentId !== docId).map((d) => d.documentId)
|
|
733
|
+
);
|
|
734
|
+
};
|
|
735
|
+
return /* @__PURE__ */ jsxs(Field.Root, { style: { width: "100%" }, children: [
|
|
736
|
+
/* @__PURE__ */ jsx(Field.Label, { children: label }),
|
|
737
|
+
!readOnly && /* @__PURE__ */ jsx(
|
|
738
|
+
Combobox,
|
|
739
|
+
{
|
|
740
|
+
value: "",
|
|
741
|
+
onChange: (val) => handleSelect(val),
|
|
742
|
+
onInputChange: (e) => setSearch(e.target.value),
|
|
743
|
+
loading: isLoading,
|
|
744
|
+
placeholder: "Search…",
|
|
745
|
+
children: availableOptions.map((opt) => /* @__PURE__ */ jsx(ComboboxOption, { value: opt.documentId, children: opt.label }, opt.documentId))
|
|
746
|
+
},
|
|
747
|
+
addKey
|
|
748
|
+
),
|
|
749
|
+
currentDocs.length > 0 && /* @__PURE__ */ jsx(
|
|
750
|
+
Flex,
|
|
751
|
+
{
|
|
752
|
+
direction: "column",
|
|
753
|
+
gap: 1,
|
|
754
|
+
style: { marginTop: 8, width: "100%" },
|
|
755
|
+
children: currentDocs.map((doc) => /* @__PURE__ */ jsxs(EntryRow, { children: [
|
|
756
|
+
/* @__PURE__ */ jsx(
|
|
757
|
+
Typography,
|
|
758
|
+
{
|
|
759
|
+
variant: "omega",
|
|
760
|
+
textColor: "neutral800",
|
|
761
|
+
style: {
|
|
762
|
+
overflow: "hidden",
|
|
763
|
+
textOverflow: "ellipsis",
|
|
764
|
+
whiteSpace: "nowrap",
|
|
765
|
+
flex: 1,
|
|
766
|
+
minWidth: 0
|
|
767
|
+
},
|
|
768
|
+
children: doc.label
|
|
769
|
+
}
|
|
770
|
+
),
|
|
771
|
+
!readOnly && /* @__PURE__ */ jsx(
|
|
772
|
+
IconButton,
|
|
773
|
+
{
|
|
774
|
+
label: "Remove",
|
|
775
|
+
size: "S",
|
|
776
|
+
onClick: () => handleRemove(doc.documentId),
|
|
777
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
778
|
+
}
|
|
779
|
+
)
|
|
780
|
+
] }, doc.documentId))
|
|
781
|
+
}
|
|
782
|
+
)
|
|
783
|
+
] });
|
|
784
|
+
}
|
|
632
785
|
function makeLabel(name) {
|
|
633
786
|
return name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
634
787
|
}
|
|
@@ -752,24 +905,15 @@ function DynamicField({
|
|
|
752
905
|
] });
|
|
753
906
|
}
|
|
754
907
|
if (type === "relation") {
|
|
755
|
-
return /* @__PURE__ */
|
|
756
|
-
|
|
908
|
+
return /* @__PURE__ */ jsx(
|
|
909
|
+
RelationField,
|
|
757
910
|
{
|
|
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
|
-
]
|
|
911
|
+
name,
|
|
912
|
+
label,
|
|
913
|
+
attribute,
|
|
914
|
+
value,
|
|
915
|
+
onChange,
|
|
916
|
+
readOnly
|
|
773
917
|
}
|
|
774
918
|
);
|
|
775
919
|
}
|
|
@@ -840,6 +984,8 @@ function useModelSchema(model) {
|
|
|
840
984
|
if (SYSTEM_FIELDS.has(name)) continue;
|
|
841
985
|
if (attr.type === "relation" && typeof attr.target === "string" && (attr.target.startsWith("plugin::users-permissions") || attr.target.startsWith("admin::")))
|
|
842
986
|
continue;
|
|
987
|
+
const baOptions = attr.pluginOptions?.["better-auth"];
|
|
988
|
+
if (baOptions?.managed === true) continue;
|
|
843
989
|
attributes[name] = attr;
|
|
844
990
|
}
|
|
845
991
|
return attributes;
|
|
@@ -930,6 +1076,14 @@ function OrganizationDetail({
|
|
|
930
1076
|
return result.data ?? [];
|
|
931
1077
|
}
|
|
932
1078
|
});
|
|
1079
|
+
const invitationsQuery = useQuery({
|
|
1080
|
+
queryKey: ["dash-org-invitations", organizationId],
|
|
1081
|
+
queryFn: async () => {
|
|
1082
|
+
const result = await client.dash.organization[organizationId].invitations({}, withContext({ organizationId }));
|
|
1083
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1084
|
+
return result.data ?? [];
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
933
1087
|
const [activeTab, setActiveTab] = useState("details");
|
|
934
1088
|
const [editName, setEditName] = useState(void 0);
|
|
935
1089
|
const [editSlug, setEditSlug] = useState(void 0);
|
|
@@ -942,6 +1096,9 @@ function OrganizationDetail({
|
|
|
942
1096
|
const [confirmDeleteSsoId, setConfirmDeleteSsoId] = useState(
|
|
943
1097
|
null
|
|
944
1098
|
);
|
|
1099
|
+
const [inviteEmail, setInviteEmail] = useState("");
|
|
1100
|
+
const [inviteRole, setInviteRole] = useState("member");
|
|
1101
|
+
const [confirmCancelInvitationId, setConfirmCancelInvitationId] = useState(null);
|
|
945
1102
|
const handleExtraChange = (name, value) => {
|
|
946
1103
|
setEditExtra((prev) => ({ ...prev, [name]: value }));
|
|
947
1104
|
};
|
|
@@ -1112,9 +1269,74 @@ function OrganizationDetail({
|
|
|
1112
1269
|
});
|
|
1113
1270
|
}
|
|
1114
1271
|
});
|
|
1272
|
+
const inviteMemberMutation = useMutation({
|
|
1273
|
+
mutationFn: async () => {
|
|
1274
|
+
const result = await client.dash.organization.inviteMember(
|
|
1275
|
+
{ email: inviteEmail, role: inviteRole, invitedBy: "" },
|
|
1276
|
+
withContext({ organizationId })
|
|
1277
|
+
);
|
|
1278
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1279
|
+
return result.data;
|
|
1280
|
+
},
|
|
1281
|
+
onSuccess: () => {
|
|
1282
|
+
qc.invalidateQueries({
|
|
1283
|
+
queryKey: ["dash-org-invitations", organizationId]
|
|
1284
|
+
});
|
|
1285
|
+
setInviteEmail("");
|
|
1286
|
+
setInviteRole("member");
|
|
1287
|
+
toggleNotification({ type: "success", message: "Invitation sent" });
|
|
1288
|
+
},
|
|
1289
|
+
onError: (err) => {
|
|
1290
|
+
toggleNotification({
|
|
1291
|
+
type: "danger",
|
|
1292
|
+
message: err.message ?? "Failed to invite"
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
const cancelInvitationMutation = useMutation({
|
|
1297
|
+
mutationFn: async (invitationId) => {
|
|
1298
|
+
const result = await client.dash.organization.cancelInvitation(
|
|
1299
|
+
{ invitationId },
|
|
1300
|
+
withContext({ organizationId })
|
|
1301
|
+
);
|
|
1302
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1303
|
+
},
|
|
1304
|
+
onSuccess: () => {
|
|
1305
|
+
setConfirmCancelInvitationId(null);
|
|
1306
|
+
qc.invalidateQueries({
|
|
1307
|
+
queryKey: ["dash-org-invitations", organizationId]
|
|
1308
|
+
});
|
|
1309
|
+
toggleNotification({ type: "success", message: "Invitation cancelled" });
|
|
1310
|
+
},
|
|
1311
|
+
onError: (err) => {
|
|
1312
|
+
toggleNotification({
|
|
1313
|
+
type: "danger",
|
|
1314
|
+
message: err.message ?? "Failed to cancel"
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
const resendInvitationMutation = useMutation({
|
|
1319
|
+
mutationFn: async (invitationId) => {
|
|
1320
|
+
const result = await client.dash.organization.resendInvitation(
|
|
1321
|
+
{ invitationId },
|
|
1322
|
+
withContext({ organizationId })
|
|
1323
|
+
);
|
|
1324
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1325
|
+
},
|
|
1326
|
+
onSuccess: () => {
|
|
1327
|
+
toggleNotification({ type: "success", message: "Invitation resent" });
|
|
1328
|
+
},
|
|
1329
|
+
onError: (err) => {
|
|
1330
|
+
toggleNotification({
|
|
1331
|
+
type: "danger",
|
|
1332
|
+
message: err.message ?? "Failed to resend"
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1115
1336
|
const members = membersQuery.data ?? [];
|
|
1116
1337
|
const teams = teamsQuery.data ?? [];
|
|
1117
1338
|
const ssoProviders = ssoQuery.data ?? [];
|
|
1339
|
+
const invitations = invitationsQuery.data ?? [];
|
|
1118
1340
|
const hasOrgEdits = editName !== void 0 || editSlug !== void 0 || editLogo !== void 0 || Object.keys(editExtra).length > 0;
|
|
1119
1341
|
const customFields = Object.entries(schemaQuery.data ?? {}).filter(([name]) => !STANDARD_ORG_FIELDS.has(name)).map(([name, attribute]) => ({ name, attribute }));
|
|
1120
1342
|
const detailsFooter = activeTab === "details" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -1171,6 +1393,11 @@ function OrganizationDetail({
|
|
|
1171
1393
|
teams.length,
|
|
1172
1394
|
")"
|
|
1173
1395
|
] }),
|
|
1396
|
+
/* @__PURE__ */ jsxs(Tabs.Trigger, { value: "invitations", children: [
|
|
1397
|
+
"Invitations (",
|
|
1398
|
+
invitations.length,
|
|
1399
|
+
")"
|
|
1400
|
+
] }),
|
|
1174
1401
|
/* @__PURE__ */ jsxs(Tabs.Trigger, { value: "sso", children: [
|
|
1175
1402
|
"SSO (",
|
|
1176
1403
|
ssoProviders.length,
|
|
@@ -1426,7 +1653,127 @@ function OrganizationDetail({
|
|
|
1426
1653
|
},
|
|
1427
1654
|
provider.id
|
|
1428
1655
|
)) })
|
|
1429
|
-
] }) }) })
|
|
1656
|
+
] }) }) }),
|
|
1657
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "invitations", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
|
|
1658
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
1659
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Invite member by email" }),
|
|
1660
|
+
/* @__PURE__ */ jsxs(Grid.Root, { gap: 3, children: [
|
|
1661
|
+
/* @__PURE__ */ jsx(Grid.Item, { col: 8, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
|
|
1662
|
+
/* @__PURE__ */ jsx(Field.Label, { children: "Email address" }),
|
|
1663
|
+
/* @__PURE__ */ jsx(
|
|
1664
|
+
TextInput,
|
|
1665
|
+
{
|
|
1666
|
+
type: "email",
|
|
1667
|
+
value: inviteEmail,
|
|
1668
|
+
onChange: (e) => setInviteEmail(e.target.value),
|
|
1669
|
+
placeholder: "user@example.com"
|
|
1670
|
+
}
|
|
1671
|
+
)
|
|
1672
|
+
] }) }),
|
|
1673
|
+
/* @__PURE__ */ jsx(Grid.Item, { col: 4, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
|
|
1674
|
+
/* @__PURE__ */ jsx(Field.Label, { children: "Role" }),
|
|
1675
|
+
/* @__PURE__ */ jsxs(
|
|
1676
|
+
SingleSelect,
|
|
1677
|
+
{
|
|
1678
|
+
value: inviteRole,
|
|
1679
|
+
onChange: (v) => setInviteRole(String(v)),
|
|
1680
|
+
"aria-label": "Invite role",
|
|
1681
|
+
children: [
|
|
1682
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "member", children: "Member" }),
|
|
1683
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "admin", children: "Admin" }),
|
|
1684
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "owner", children: "Owner" })
|
|
1685
|
+
]
|
|
1686
|
+
}
|
|
1687
|
+
)
|
|
1688
|
+
] }) })
|
|
1689
|
+
] }),
|
|
1690
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
|
|
1691
|
+
Button,
|
|
1692
|
+
{
|
|
1693
|
+
size: "S",
|
|
1694
|
+
startIcon: /* @__PURE__ */ jsx(Plus, {}),
|
|
1695
|
+
disabled: !inviteEmail,
|
|
1696
|
+
loading: inviteMemberMutation.isLoading,
|
|
1697
|
+
onClick: () => inviteMemberMutation.mutate(),
|
|
1698
|
+
children: "Send invitation"
|
|
1699
|
+
}
|
|
1700
|
+
) })
|
|
1701
|
+
] }),
|
|
1702
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
1703
|
+
/* @__PURE__ */ jsxs(SectionLabel, { children: [
|
|
1704
|
+
"Invitations (",
|
|
1705
|
+
invitations.length,
|
|
1706
|
+
")"
|
|
1707
|
+
] }),
|
|
1708
|
+
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) => {
|
|
1709
|
+
const statusColor = {
|
|
1710
|
+
pending: "#f59e0b",
|
|
1711
|
+
accepted: "#5cb176",
|
|
1712
|
+
rejected: "#d02b20",
|
|
1713
|
+
canceled: "#8e8ea9"
|
|
1714
|
+
};
|
|
1715
|
+
const color = statusColor[inv.status] ?? "#8e8ea9";
|
|
1716
|
+
const isPending = inv.status === "pending";
|
|
1717
|
+
return /* @__PURE__ */ jsxs(AccountRow, { children: [
|
|
1718
|
+
/* @__PURE__ */ jsxs(
|
|
1719
|
+
Flex,
|
|
1720
|
+
{
|
|
1721
|
+
direction: "column",
|
|
1722
|
+
gap: 1,
|
|
1723
|
+
alignItems: "flex-start",
|
|
1724
|
+
style: { flex: 1 },
|
|
1725
|
+
children: [
|
|
1726
|
+
/* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: inv.user?.name ?? inv.email }),
|
|
1727
|
+
/* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
|
|
1728
|
+
inv.email,
|
|
1729
|
+
" · role: ",
|
|
1730
|
+
inv.role
|
|
1731
|
+
] }),
|
|
1732
|
+
/* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
|
|
1733
|
+
"Expires",
|
|
1734
|
+
" ",
|
|
1735
|
+
new Date(inv.expiresAt).toLocaleDateString()
|
|
1736
|
+
] })
|
|
1737
|
+
]
|
|
1738
|
+
}
|
|
1739
|
+
),
|
|
1740
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
|
|
1741
|
+
/* @__PURE__ */ jsx(
|
|
1742
|
+
MonoChip,
|
|
1743
|
+
{
|
|
1744
|
+
style: {
|
|
1745
|
+
color,
|
|
1746
|
+
borderColor: color,
|
|
1747
|
+
background: `${color}18`
|
|
1748
|
+
},
|
|
1749
|
+
children: inv.status
|
|
1750
|
+
}
|
|
1751
|
+
),
|
|
1752
|
+
isPending && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1753
|
+
/* @__PURE__ */ jsx(
|
|
1754
|
+
Button,
|
|
1755
|
+
{
|
|
1756
|
+
size: "S",
|
|
1757
|
+
variant: "secondary",
|
|
1758
|
+
loading: resendInvitationMutation.isLoading,
|
|
1759
|
+
onClick: () => resendInvitationMutation.mutate(inv.id),
|
|
1760
|
+
children: "Resend"
|
|
1761
|
+
}
|
|
1762
|
+
),
|
|
1763
|
+
/* @__PURE__ */ jsx(
|
|
1764
|
+
IconButton,
|
|
1765
|
+
{
|
|
1766
|
+
label: "Cancel invitation",
|
|
1767
|
+
onClick: () => setConfirmCancelInvitationId(inv.id),
|
|
1768
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
1769
|
+
}
|
|
1770
|
+
)
|
|
1771
|
+
] })
|
|
1772
|
+
] })
|
|
1773
|
+
] }, inv.id);
|
|
1774
|
+
}) })
|
|
1775
|
+
] })
|
|
1776
|
+
] }) })
|
|
1430
1777
|
] }),
|
|
1431
1778
|
confirmRemoveMemberId && /* @__PURE__ */ jsx(
|
|
1432
1779
|
ConfirmDialog,
|
|
@@ -1460,6 +1807,17 @@ function OrganizationDetail({
|
|
|
1460
1807
|
onConfirm: () => deleteSsoMutation.mutate(confirmDeleteSsoId),
|
|
1461
1808
|
onCancel: () => setConfirmDeleteSsoId(null)
|
|
1462
1809
|
}
|
|
1810
|
+
),
|
|
1811
|
+
confirmCancelInvitationId && /* @__PURE__ */ jsx(
|
|
1812
|
+
ConfirmDialog,
|
|
1813
|
+
{
|
|
1814
|
+
title: "Cancel invitation",
|
|
1815
|
+
message: "Are you sure you want to cancel this invitation? The invite link will no longer work.",
|
|
1816
|
+
confirmLabel: "Cancel invitation",
|
|
1817
|
+
loading: cancelInvitationMutation.isLoading,
|
|
1818
|
+
onConfirm: () => cancelInvitationMutation.mutate(confirmCancelInvitationId),
|
|
1819
|
+
onCancel: () => setConfirmCancelInvitationId(null)
|
|
1820
|
+
}
|
|
1463
1821
|
)
|
|
1464
1822
|
]
|
|
1465
1823
|
}
|
|
@@ -1605,12 +1963,12 @@ function TeamRow({
|
|
|
1605
1963
|
)
|
|
1606
1964
|
] });
|
|
1607
1965
|
}
|
|
1608
|
-
const PAGE_SIZE$
|
|
1609
|
-
const fadeUp$
|
|
1966
|
+
const PAGE_SIZE$1 = 25;
|
|
1967
|
+
const fadeUp$2 = keyframes`
|
|
1610
1968
|
from { opacity: 0; transform: translateY(6px); }
|
|
1611
1969
|
to { opacity: 1; transform: translateY(0); }
|
|
1612
1970
|
`;
|
|
1613
|
-
const Wrap$
|
|
1971
|
+
const Wrap$2 = styled.div`
|
|
1614
1972
|
padding: 28px 32px;
|
|
1615
1973
|
background: #f6f6f9;
|
|
1616
1974
|
min-height: 100%;
|
|
@@ -1618,39 +1976,39 @@ const Wrap$3 = styled.div`
|
|
|
1618
1976
|
flex-direction: column;
|
|
1619
1977
|
gap: 20px;
|
|
1620
1978
|
`;
|
|
1621
|
-
const PageHeader$
|
|
1979
|
+
const PageHeader$1 = styled.div`
|
|
1622
1980
|
display: flex;
|
|
1623
1981
|
justify-content: space-between;
|
|
1624
1982
|
align-items: flex-start;
|
|
1625
1983
|
`;
|
|
1626
|
-
const TitleBlock$
|
|
1984
|
+
const TitleBlock$1 = styled.div`
|
|
1627
1985
|
display: flex;
|
|
1628
1986
|
flex-direction: column;
|
|
1629
1987
|
gap: 4px;
|
|
1630
1988
|
`;
|
|
1631
|
-
const PageTitle$
|
|
1989
|
+
const PageTitle$1 = styled.h1`
|
|
1632
1990
|
margin: 0;
|
|
1633
1991
|
font-size: 22px;
|
|
1634
1992
|
font-weight: 800;
|
|
1635
1993
|
color: #32324d;
|
|
1636
1994
|
letter-spacing: -0.03em;
|
|
1637
1995
|
`;
|
|
1638
|
-
const PageSubtitle$
|
|
1996
|
+
const PageSubtitle$1 = styled.p`
|
|
1639
1997
|
margin: 0;
|
|
1640
1998
|
font-size: 12px;
|
|
1641
1999
|
color: #8e8ea9;
|
|
1642
2000
|
`;
|
|
1643
|
-
const TableCard$
|
|
2001
|
+
const TableCard$1 = styled.div`
|
|
1644
2002
|
background: #ffffff;
|
|
1645
2003
|
border: 1px solid #eaeaef;
|
|
1646
2004
|
border-radius: 10px;
|
|
1647
2005
|
overflow: hidden;
|
|
1648
2006
|
`;
|
|
1649
|
-
const Table$
|
|
2007
|
+
const Table$1 = styled.table`
|
|
1650
2008
|
width: 100%;
|
|
1651
2009
|
border-collapse: collapse;
|
|
1652
2010
|
`;
|
|
1653
|
-
const TH$
|
|
2011
|
+
const TH$1 = styled.th`
|
|
1654
2012
|
text-align: left;
|
|
1655
2013
|
padding: 10px 14px;
|
|
1656
2014
|
font-size: 10px;
|
|
@@ -1664,13 +2022,13 @@ const TH$2 = styled.th`
|
|
|
1664
2022
|
&:first-child { padding-left: 20px; }
|
|
1665
2023
|
&:last-child { padding-right: 20px; }
|
|
1666
2024
|
`;
|
|
1667
|
-
const THCheck$1 = styled(TH$
|
|
2025
|
+
const THCheck$1 = styled(TH$1)`
|
|
1668
2026
|
width: 44px;
|
|
1669
2027
|
`;
|
|
1670
|
-
const THActions$1 = styled(TH$
|
|
2028
|
+
const THActions$1 = styled(TH$1)`
|
|
1671
2029
|
width: 80px;
|
|
1672
2030
|
`;
|
|
1673
|
-
const TD$
|
|
2031
|
+
const TD$1 = styled.td`
|
|
1674
2032
|
padding: 11px 14px;
|
|
1675
2033
|
font-size: 12px;
|
|
1676
2034
|
color: #32324d;
|
|
@@ -1679,14 +2037,14 @@ const TD$2 = styled.td`
|
|
|
1679
2037
|
&:first-child { padding-left: 20px; }
|
|
1680
2038
|
&:last-child { padding-right: 20px; }
|
|
1681
2039
|
`;
|
|
1682
|
-
const TDCheck$1 = styled(TD$
|
|
2040
|
+
const TDCheck$1 = styled(TD$1)`
|
|
1683
2041
|
width: 44px;
|
|
1684
2042
|
`;
|
|
1685
|
-
const TDActions$1 = styled(TD$
|
|
2043
|
+
const TDActions$1 = styled(TD$1)`
|
|
1686
2044
|
width: 80px;
|
|
1687
2045
|
`;
|
|
1688
|
-
const TR$
|
|
1689
|
-
animation: ${fadeUp$
|
|
2046
|
+
const TR$1 = styled.tr`
|
|
2047
|
+
animation: ${fadeUp$2} 280ms ease both;
|
|
1690
2048
|
animation-delay: ${(p) => (p.$i ?? 0) * 25}ms;
|
|
1691
2049
|
background: ${(p) => p.$selected ? "#f0f0ff" : "transparent"};
|
|
1692
2050
|
transition: background 120ms ease;
|
|
@@ -1782,13 +2140,13 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1782
2140
|
const [detailOrgId, setDetailOrgId] = useState(null);
|
|
1783
2141
|
const [confirmDelete, setConfirmDelete] = useState(null);
|
|
1784
2142
|
const [confirmDeleteMany, setConfirmDeleteMany] = useState(false);
|
|
1785
|
-
const offset = (page - 1) * PAGE_SIZE$
|
|
2143
|
+
const offset = (page - 1) * PAGE_SIZE$1;
|
|
1786
2144
|
const orgsQuery = useQuery({
|
|
1787
2145
|
queryKey: ["dash-organizations", page, search],
|
|
1788
2146
|
queryFn: async () => {
|
|
1789
2147
|
const result = await client.dash.listOrganizations({
|
|
1790
2148
|
query: {
|
|
1791
|
-
limit: PAGE_SIZE$
|
|
2149
|
+
limit: PAGE_SIZE$1,
|
|
1792
2150
|
offset,
|
|
1793
2151
|
sortBy: "createdAt",
|
|
1794
2152
|
sortOrder: "desc",
|
|
@@ -1833,7 +2191,7 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1833
2191
|
});
|
|
1834
2192
|
const orgs = orgsQuery.data && "organizations" in orgsQuery.data ? orgsQuery.data.organizations : [];
|
|
1835
2193
|
const total = orgsQuery.data && "total" in orgsQuery.data ? orgsQuery.data.total : 0;
|
|
1836
|
-
const pageCount = Math.ceil(total / PAGE_SIZE$
|
|
2194
|
+
const pageCount = Math.ceil(total / PAGE_SIZE$1);
|
|
1837
2195
|
const allSelected = orgs.length > 0 && orgs.every((o) => selected.has(o.id));
|
|
1838
2196
|
const someSelected = selected.size > 0;
|
|
1839
2197
|
const toggleSelect = (id) => {
|
|
@@ -1853,11 +2211,11 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1853
2211
|
setSearch(searchInput);
|
|
1854
2212
|
setPage(1);
|
|
1855
2213
|
};
|
|
1856
|
-
return /* @__PURE__ */ jsxs(Wrap$
|
|
1857
|
-
/* @__PURE__ */ jsxs(PageHeader$
|
|
1858
|
-
/* @__PURE__ */ jsxs(TitleBlock$
|
|
1859
|
-
/* @__PURE__ */ jsx(PageTitle$
|
|
1860
|
-
/* @__PURE__ */ jsxs(PageSubtitle$
|
|
2214
|
+
return /* @__PURE__ */ jsxs(Wrap$2, { "data-testid": "organizations-page", children: [
|
|
2215
|
+
/* @__PURE__ */ jsxs(PageHeader$1, { children: [
|
|
2216
|
+
/* @__PURE__ */ jsxs(TitleBlock$1, { children: [
|
|
2217
|
+
/* @__PURE__ */ jsx(PageTitle$1, { children: "Organizations" }),
|
|
2218
|
+
/* @__PURE__ */ jsxs(PageSubtitle$1, { children: [
|
|
1861
2219
|
total.toLocaleString(),
|
|
1862
2220
|
" total"
|
|
1863
2221
|
] })
|
|
@@ -1905,7 +2263,7 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1905
2263
|
)
|
|
1906
2264
|
] }),
|
|
1907
2265
|
orgsQuery.isError && /* @__PURE__ */ jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: orgsQuery.error instanceof Error ? orgsQuery.error.message : "An error occurred" }),
|
|
1908
|
-
/* @__PURE__ */ jsx(TableCard$
|
|
2266
|
+
/* @__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: [
|
|
1909
2267
|
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
1910
2268
|
/* @__PURE__ */ jsx(THCheck$1, { children: /* @__PURE__ */ jsx(
|
|
1911
2269
|
Checkbox,
|
|
@@ -1915,14 +2273,14 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1915
2273
|
"aria-label": "Select all"
|
|
1916
2274
|
}
|
|
1917
2275
|
) }),
|
|
1918
|
-
/* @__PURE__ */ jsx(TH$
|
|
1919
|
-
/* @__PURE__ */ jsx(TH$
|
|
1920
|
-
/* @__PURE__ */ jsx(TH$
|
|
1921
|
-
/* @__PURE__ */ jsx(TH$
|
|
2276
|
+
/* @__PURE__ */ jsx(TH$1, { children: "Name" }),
|
|
2277
|
+
/* @__PURE__ */ jsx(TH$1, { children: "Slug" }),
|
|
2278
|
+
/* @__PURE__ */ jsx(TH$1, { children: "Members" }),
|
|
2279
|
+
/* @__PURE__ */ jsx(TH$1, { children: "Created" }),
|
|
1922
2280
|
/* @__PURE__ */ jsx(THActions$1, {})
|
|
1923
2281
|
] }) }),
|
|
1924
2282
|
/* @__PURE__ */ jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
|
|
1925
|
-
TD$
|
|
2283
|
+
TD$1,
|
|
1926
2284
|
{
|
|
1927
2285
|
colSpan: 6,
|
|
1928
2286
|
style: {
|
|
@@ -1934,7 +2292,7 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1934
2292
|
children: search ? `No organizations matching "${search}"` : "No organizations found"
|
|
1935
2293
|
}
|
|
1936
2294
|
) }) : orgs.map((org, i) => /* @__PURE__ */ jsxs(
|
|
1937
|
-
TR$
|
|
2295
|
+
TR$1,
|
|
1938
2296
|
{
|
|
1939
2297
|
$selected: selected.has(org.id),
|
|
1940
2298
|
$i: i,
|
|
@@ -1948,13 +2306,13 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1948
2306
|
"aria-label": `Select ${org.name}`
|
|
1949
2307
|
}
|
|
1950
2308
|
) }),
|
|
1951
|
-
/* @__PURE__ */ jsx(TD$
|
|
2309
|
+
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
|
|
1952
2310
|
/* @__PURE__ */ jsx(OrgAvatar, { name: org.name, logo: org.logo }),
|
|
1953
2311
|
/* @__PURE__ */ jsx(OrgName, { children: org.name })
|
|
1954
2312
|
] }) }),
|
|
1955
|
-
/* @__PURE__ */ jsx(TD$
|
|
1956
|
-
/* @__PURE__ */ jsx(TD$
|
|
1957
|
-
/* @__PURE__ */ jsx(TD$
|
|
2313
|
+
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(SlugChip, { children: org.slug }) }),
|
|
2314
|
+
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(CountChip, { children: org.memberCount }) }),
|
|
2315
|
+
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
|
|
1958
2316
|
/* @__PURE__ */ jsx(TDActions$1, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", children: [
|
|
1959
2317
|
/* @__PURE__ */ jsx(
|
|
1960
2318
|
IconButton,
|
|
@@ -2101,95 +2459,143 @@ function Avatar({
|
|
|
2101
2459
|
const color = getColor(name || "?");
|
|
2102
2460
|
return /* @__PURE__ */ jsx(Circle, { $bg: color.bg, $fg: color.fg, $size: size, children: getInitials(name || "?") });
|
|
2103
2461
|
}
|
|
2104
|
-
const
|
|
2105
|
-
|
|
2462
|
+
const T = {
|
|
2463
|
+
bg: "#f6f6f9",
|
|
2464
|
+
bgCard: "#ffffff",
|
|
2465
|
+
border: "#eaeaef",
|
|
2466
|
+
borderHover: "rgba(73,69,255,0.35)",
|
|
2467
|
+
accent: "#4945ff",
|
|
2468
|
+
green: "#5cb176",
|
|
2469
|
+
greenDim: "rgba(92,177,118,0.12)",
|
|
2470
|
+
amber: "#d9822f",
|
|
2471
|
+
red: "#d02b20",
|
|
2472
|
+
redDim: "rgba(208,43,32,0.1)",
|
|
2473
|
+
purple: "#8460b8",
|
|
2474
|
+
textPrimary: "#32324d",
|
|
2475
|
+
textSecondary: "#666687",
|
|
2476
|
+
textMuted: "#b8b8c7",
|
|
2477
|
+
mono: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`
|
|
2478
|
+
};
|
|
2479
|
+
const fadeUp$1 = keyframes`
|
|
2480
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
2106
2481
|
to { opacity: 1; transform: translateY(0); }
|
|
2107
2482
|
`;
|
|
2108
|
-
const Wrap$
|
|
2109
|
-
padding: 28px 32px;
|
|
2110
|
-
background:
|
|
2483
|
+
const Wrap$1 = styled.div`
|
|
2484
|
+
padding: 28px 32px 48px;
|
|
2485
|
+
background: ${T.bg};
|
|
2111
2486
|
min-height: 100%;
|
|
2112
2487
|
display: flex;
|
|
2113
2488
|
flex-direction: column;
|
|
2114
|
-
gap:
|
|
2489
|
+
gap: 22px;
|
|
2115
2490
|
`;
|
|
2116
|
-
const
|
|
2117
|
-
display:
|
|
2118
|
-
|
|
2119
|
-
gap:
|
|
2120
|
-
|
|
2121
|
-
@media (max-width: 768px) { grid-template-columns: repeat(2, 1fr); }
|
|
2491
|
+
const SectionDivider = styled.div`
|
|
2492
|
+
display: flex;
|
|
2493
|
+
align-items: center;
|
|
2494
|
+
gap: 10px;
|
|
2495
|
+
margin-bottom: -6px;
|
|
2122
2496
|
`;
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2497
|
+
const DivLabel = styled.span`
|
|
2498
|
+
font-size: 9px;
|
|
2499
|
+
font-weight: 700;
|
|
2500
|
+
letter-spacing: 0.14em;
|
|
2501
|
+
text-transform: uppercase;
|
|
2502
|
+
color: ${T.textMuted};
|
|
2503
|
+
white-space: nowrap;
|
|
2128
2504
|
`;
|
|
2129
|
-
const
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2505
|
+
const DivLine = styled.div`
|
|
2506
|
+
flex: 1;
|
|
2507
|
+
height: 1px;
|
|
2508
|
+
background: ${T.border};
|
|
2133
2509
|
`;
|
|
2134
2510
|
const Card = styled.div`
|
|
2135
|
-
background:
|
|
2136
|
-
border: 1px solid
|
|
2137
|
-
border-radius:
|
|
2511
|
+
background: ${T.bgCard};
|
|
2512
|
+
border: 1px solid ${T.border};
|
|
2513
|
+
border-radius: 12px;
|
|
2138
2514
|
overflow: hidden;
|
|
2139
2515
|
position: relative;
|
|
2140
|
-
animation: ${fadeUp$
|
|
2516
|
+
animation: ${fadeUp$1} 380ms ease both;
|
|
2141
2517
|
animation-delay: ${(p) => (p.$delay ?? 0) * 55}ms;
|
|
2142
|
-
transition: border-color
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2518
|
+
transition: border-color 200ms, box-shadow 200ms;
|
|
2519
|
+
&:hover {
|
|
2520
|
+
border-color: ${T.borderHover};
|
|
2521
|
+
box-shadow: 0 0 0 1px ${T.borderHover}, 0 8px 32px rgba(124,109,250,0.07);
|
|
2522
|
+
}
|
|
2523
|
+
`;
|
|
2524
|
+
const PillGroup = styled.div`
|
|
2525
|
+
display: flex;
|
|
2526
|
+
background: rgba(0,0,0,0.03);
|
|
2527
|
+
border: 1px solid ${T.border};
|
|
2528
|
+
border-radius: 9px;
|
|
2529
|
+
padding: 3px;
|
|
2530
|
+
gap: 2px;
|
|
2531
|
+
`;
|
|
2532
|
+
const Pill = styled.button`
|
|
2533
|
+
appearance: none;
|
|
2534
|
+
border: none;
|
|
2535
|
+
cursor: pointer;
|
|
2536
|
+
padding: 5px 16px;
|
|
2537
|
+
border-radius: 6px;
|
|
2538
|
+
font-size: 11px;
|
|
2539
|
+
font-weight: 600;
|
|
2540
|
+
letter-spacing: 0.03em;
|
|
2541
|
+
transition: background 180ms, color 180ms;
|
|
2542
|
+
background: ${(p) => p.$active ? T.accent : "transparent"};
|
|
2543
|
+
color: ${(p) => p.$active ? "#fff" : T.textSecondary};
|
|
2155
2544
|
&:hover {
|
|
2156
|
-
|
|
2157
|
-
|
|
2545
|
+
background: ${(p) => p.$active ? T.accent : "rgba(0,0,0,0.05)"};
|
|
2546
|
+
color: ${(p) => p.$active ? "#fff" : T.textPrimary};
|
|
2158
2547
|
}
|
|
2159
2548
|
`;
|
|
2160
|
-
const
|
|
2161
|
-
|
|
2549
|
+
const StatGrid = styled.div`
|
|
2550
|
+
display: grid;
|
|
2551
|
+
grid-template-columns: repeat(5, 1fr);
|
|
2552
|
+
gap: 10px;
|
|
2553
|
+
@media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
|
|
2554
|
+
`;
|
|
2555
|
+
const StatCard = styled(Card)`
|
|
2556
|
+
padding: 18px 20px 0;
|
|
2557
|
+
&::after {
|
|
2558
|
+
content: '';
|
|
2559
|
+
position: absolute;
|
|
2560
|
+
top: 0; left: 0; right: 0;
|
|
2561
|
+
height: 2px;
|
|
2562
|
+
background: ${(p) => p.$accent};
|
|
2563
|
+
opacity: 0.75;
|
|
2564
|
+
}
|
|
2162
2565
|
`;
|
|
2163
2566
|
const StatLabel = styled.div`
|
|
2164
|
-
font-size:
|
|
2567
|
+
font-size: 9px;
|
|
2165
2568
|
font-weight: 700;
|
|
2166
|
-
letter-spacing: 0.
|
|
2569
|
+
letter-spacing: 0.1em;
|
|
2167
2570
|
text-transform: uppercase;
|
|
2168
|
-
color:
|
|
2169
|
-
margin-bottom:
|
|
2571
|
+
color: ${T.textMuted};
|
|
2572
|
+
margin-bottom: 10px;
|
|
2170
2573
|
`;
|
|
2171
2574
|
const StatValue = styled.div`
|
|
2172
2575
|
font-size: 30px;
|
|
2173
2576
|
font-weight: 800;
|
|
2174
|
-
color:
|
|
2577
|
+
color: ${T.textPrimary};
|
|
2175
2578
|
line-height: 1;
|
|
2176
|
-
letter-spacing: -0.
|
|
2579
|
+
letter-spacing: -0.045em;
|
|
2177
2580
|
font-variant-numeric: tabular-nums;
|
|
2178
2581
|
margin-bottom: 10px;
|
|
2179
2582
|
`;
|
|
2180
2583
|
const TrendBadge = styled.span`
|
|
2181
2584
|
display: inline-flex;
|
|
2182
2585
|
align-items: center;
|
|
2183
|
-
gap:
|
|
2184
|
-
padding: 2px
|
|
2586
|
+
gap: 3px;
|
|
2587
|
+
padding: 2px 8px;
|
|
2185
2588
|
border-radius: 20px;
|
|
2186
|
-
font-size:
|
|
2589
|
+
font-size: 9px;
|
|
2187
2590
|
font-weight: 700;
|
|
2188
|
-
|
|
2189
|
-
|
|
2591
|
+
letter-spacing: 0.03em;
|
|
2592
|
+
background: ${(p) => p.$pos ? T.greenDim : T.redDim};
|
|
2593
|
+
color: ${(p) => p.$pos ? T.green : T.red};
|
|
2594
|
+
margin-bottom: 12px;
|
|
2190
2595
|
`;
|
|
2191
2596
|
const SparkWrap = styled.div`
|
|
2192
|
-
height:
|
|
2597
|
+
height: 44px;
|
|
2598
|
+
margin: 0 -20px;
|
|
2193
2599
|
`;
|
|
2194
2600
|
const ChartCard = styled(Card)`
|
|
2195
2601
|
padding: 20px 20px 12px;
|
|
@@ -2201,31 +2607,42 @@ const ChartHeader = styled.div`
|
|
|
2201
2607
|
display: flex;
|
|
2202
2608
|
align-items: center;
|
|
2203
2609
|
justify-content: space-between;
|
|
2610
|
+
gap: 12px;
|
|
2204
2611
|
flex-shrink: 0;
|
|
2205
2612
|
`;
|
|
2206
2613
|
const ChartTitle = styled.div`
|
|
2207
|
-
font-size:
|
|
2614
|
+
font-size: 12px;
|
|
2208
2615
|
font-weight: 700;
|
|
2209
|
-
color:
|
|
2616
|
+
color: ${T.textPrimary};
|
|
2617
|
+
letter-spacing: 0.01em;
|
|
2210
2618
|
`;
|
|
2211
|
-
const
|
|
2619
|
+
const SeriesRow = styled.div`
|
|
2212
2620
|
display: flex;
|
|
2213
2621
|
align-items: center;
|
|
2214
|
-
gap:
|
|
2622
|
+
gap: 6px;
|
|
2215
2623
|
`;
|
|
2216
|
-
const
|
|
2624
|
+
const SeriesBtn = styled.button`
|
|
2625
|
+
appearance: none;
|
|
2626
|
+
border: 1px solid ${(p) => p.$on ? `${p.$c}55` : T.border};
|
|
2627
|
+
background: ${(p) => p.$on ? `${p.$c}18` : "transparent"};
|
|
2628
|
+
color: ${(p) => p.$on ? p.$c : T.textMuted};
|
|
2629
|
+
font-size: 10px;
|
|
2630
|
+
font-weight: 600;
|
|
2631
|
+
padding: 3px 10px 3px 8px;
|
|
2632
|
+
border-radius: 6px;
|
|
2633
|
+
cursor: pointer;
|
|
2217
2634
|
display: flex;
|
|
2218
2635
|
align-items: center;
|
|
2219
2636
|
gap: 5px;
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2637
|
+
transition: all 160ms;
|
|
2638
|
+
&:hover {
|
|
2639
|
+
border-color: ${(p) => `${p.$c}88`};
|
|
2640
|
+
color: ${(p) => p.$c};
|
|
2641
|
+
background: ${(p) => `${p.$c}22`};
|
|
2642
|
+
}
|
|
2225
2643
|
`;
|
|
2226
|
-
const
|
|
2227
|
-
width:
|
|
2228
|
-
height: 8px;
|
|
2644
|
+
const SDot = styled.span`
|
|
2645
|
+
width: 6px; height: 6px;
|
|
2229
2646
|
border-radius: 50%;
|
|
2230
2647
|
background: ${(p) => p.$c};
|
|
2231
2648
|
display: inline-block;
|
|
@@ -2233,140 +2650,188 @@ const LegendDot = styled.span`
|
|
|
2233
2650
|
`;
|
|
2234
2651
|
const HoverInfo = styled.div`
|
|
2235
2652
|
font-size: 11px;
|
|
2236
|
-
color:
|
|
2653
|
+
color: ${T.textSecondary};
|
|
2237
2654
|
font-variant-numeric: tabular-nums;
|
|
2238
|
-
|
|
2655
|
+
font-family: ${T.mono};
|
|
2656
|
+
min-height: 15px;
|
|
2657
|
+
`;
|
|
2658
|
+
const TwoPanel = styled.div`
|
|
2659
|
+
display: grid;
|
|
2660
|
+
grid-template-columns: ${(p) => p.$ratio ?? "1fr 288px"};
|
|
2661
|
+
gap: 10px;
|
|
2662
|
+
align-items: stretch;
|
|
2663
|
+
@media (max-width: 900px) { grid-template-columns: 1fr; }
|
|
2239
2664
|
`;
|
|
2240
2665
|
const FeedCard = styled(Card)`
|
|
2241
2666
|
display: flex;
|
|
2242
2667
|
flex-direction: column;
|
|
2243
|
-
|
|
2668
|
+
overflow: hidden;
|
|
2244
2669
|
`;
|
|
2245
|
-
const
|
|
2246
|
-
padding: 16px
|
|
2247
|
-
border-bottom: 1px solid
|
|
2248
|
-
|
|
2249
|
-
font-size: 10px;
|
|
2670
|
+
const FeedHead = styled.div`
|
|
2671
|
+
padding: 13px 16px 9px;
|
|
2672
|
+
border-bottom: 1px solid ${T.border};
|
|
2673
|
+
font-size: 11px;
|
|
2250
2674
|
font-weight: 700;
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2675
|
+
color: ${T.textPrimary};
|
|
2676
|
+
flex-shrink: 0;
|
|
2677
|
+
display: flex;
|
|
2678
|
+
justify-content: space-between;
|
|
2679
|
+
align-items: center;
|
|
2254
2680
|
`;
|
|
2255
|
-
const
|
|
2681
|
+
const FeedSelect = styled.select`
|
|
2682
|
+
appearance: none;
|
|
2683
|
+
border: 1px solid ${T.border};
|
|
2684
|
+
border-radius: 6px;
|
|
2685
|
+
background: ${T.bgCard};
|
|
2686
|
+
color: ${T.textSecondary};
|
|
2687
|
+
font-size: 10px;
|
|
2688
|
+
font-weight: 600;
|
|
2689
|
+
padding: 3px 22px 3px 8px;
|
|
2690
|
+
cursor: pointer;
|
|
2691
|
+
outline: none;
|
|
2692
|
+
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");
|
|
2693
|
+
background-repeat: no-repeat;
|
|
2694
|
+
background-position: right 7px center;
|
|
2695
|
+
transition: border-color 160ms;
|
|
2696
|
+
&:hover, &:focus { border-color: ${T.accent}; color: ${T.textPrimary}; }
|
|
2697
|
+
`;
|
|
2698
|
+
const FeedScroll = styled.div`
|
|
2256
2699
|
flex: 1;
|
|
2257
2700
|
overflow-y: auto;
|
|
2258
|
-
|
|
2259
|
-
&::-webkit-scrollbar { width:
|
|
2260
|
-
&::-webkit-scrollbar-
|
|
2261
|
-
&::-webkit-scrollbar-thumb { background: #d9d8ff; border-radius: 2px; }
|
|
2701
|
+
min-height: 0;
|
|
2702
|
+
&::-webkit-scrollbar { width: 4px; }
|
|
2703
|
+
&::-webkit-scrollbar-thumb { background: ${T.textMuted}; border-radius: 2px; }
|
|
2262
2704
|
`;
|
|
2263
2705
|
const FeedItem = styled.div`
|
|
2706
|
+
padding: 9px 14px;
|
|
2707
|
+
border-bottom: 1px solid #f0f0f5;
|
|
2264
2708
|
display: flex;
|
|
2265
|
-
|
|
2266
|
-
gap:
|
|
2267
|
-
|
|
2268
|
-
&:hover { background: #
|
|
2709
|
+
flex-direction: column;
|
|
2710
|
+
gap: 3px;
|
|
2711
|
+
transition: background 130ms;
|
|
2712
|
+
&:hover { background: #fafaff; }
|
|
2713
|
+
&:last-child { border-bottom: none; }
|
|
2269
2714
|
`;
|
|
2270
|
-
const
|
|
2271
|
-
|
|
2272
|
-
|
|
2715
|
+
const FeedTop = styled.div`
|
|
2716
|
+
display: flex;
|
|
2717
|
+
align-items: center;
|
|
2718
|
+
justify-content: space-between;
|
|
2719
|
+
gap: 6px;
|
|
2273
2720
|
`;
|
|
2274
|
-
const FeedName = styled.
|
|
2275
|
-
font-size:
|
|
2721
|
+
const FeedName = styled.span`
|
|
2722
|
+
font-size: 11px;
|
|
2276
2723
|
font-weight: 600;
|
|
2277
|
-
color:
|
|
2278
|
-
white-space: nowrap;
|
|
2724
|
+
color: ${T.textPrimary};
|
|
2279
2725
|
overflow: hidden;
|
|
2280
2726
|
text-overflow: ellipsis;
|
|
2727
|
+
white-space: nowrap;
|
|
2281
2728
|
`;
|
|
2282
|
-
const
|
|
2729
|
+
const FeedEmail = styled.span`
|
|
2283
2730
|
font-size: 10px;
|
|
2284
|
-
color:
|
|
2285
|
-
white-space: nowrap;
|
|
2731
|
+
color: ${T.textSecondary};
|
|
2286
2732
|
overflow: hidden;
|
|
2287
2733
|
text-overflow: ellipsis;
|
|
2288
|
-
|
|
2734
|
+
white-space: nowrap;
|
|
2735
|
+
display: block;
|
|
2289
2736
|
`;
|
|
2290
|
-
const
|
|
2291
|
-
font-size:
|
|
2292
|
-
color:
|
|
2737
|
+
const FeedMeta = styled.span`
|
|
2738
|
+
font-size: 9px;
|
|
2739
|
+
color: ${T.textMuted};
|
|
2293
2740
|
white-space: nowrap;
|
|
2294
2741
|
font-variant-numeric: tabular-nums;
|
|
2295
|
-
margin-top: 1px;
|
|
2296
2742
|
`;
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2743
|
+
const RtnCard = styled(Card)`
|
|
2744
|
+
padding: 20px;
|
|
2745
|
+
display: flex;
|
|
2746
|
+
flex-direction: column;
|
|
2747
|
+
gap: 12px;
|
|
2300
2748
|
`;
|
|
2301
|
-
const
|
|
2302
|
-
|
|
2303
|
-
|
|
2749
|
+
const RtnRow = styled.div`
|
|
2750
|
+
display: flex;
|
|
2751
|
+
align-items: center;
|
|
2752
|
+
gap: 10px;
|
|
2753
|
+
padding: 4px 6px;
|
|
2754
|
+
border-radius: 7px;
|
|
2755
|
+
transition: background 130ms;
|
|
2756
|
+
background: ${(p) => p.$hov ? "#f0f0f6" : "transparent"};
|
|
2757
|
+
cursor: default;
|
|
2304
2758
|
`;
|
|
2305
|
-
const
|
|
2306
|
-
text-align: left;
|
|
2307
|
-
padding: 10px 14px;
|
|
2759
|
+
const RtnLabel = styled.span`
|
|
2308
2760
|
font-size: 10px;
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
text-
|
|
2312
|
-
letter-spacing: 0.06em;
|
|
2313
|
-
border-bottom: 1px solid #eaeaef;
|
|
2314
|
-
background: #fafafa;
|
|
2761
|
+
color: ${T.textSecondary};
|
|
2762
|
+
min-width: 80px;
|
|
2763
|
+
text-align: right;
|
|
2315
2764
|
white-space: nowrap;
|
|
2316
|
-
&:first-child { padding-left: 20px; }
|
|
2317
|
-
&:last-child { padding-right: 20px; }
|
|
2318
2765
|
`;
|
|
2319
|
-
const
|
|
2320
|
-
|
|
2321
|
-
|
|
2766
|
+
const RtnSize = styled.span`
|
|
2767
|
+
font-size: 9px;
|
|
2768
|
+
color: ${T.textMuted};
|
|
2769
|
+
min-width: 52px;
|
|
2770
|
+
text-align: right;
|
|
2771
|
+
white-space: nowrap;
|
|
2772
|
+
font-variant-numeric: tabular-nums;
|
|
2322
2773
|
`;
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
border-
|
|
2328
|
-
|
|
2329
|
-
&:first-child { padding-left: 20px; }
|
|
2330
|
-
&:last-child { padding-right: 20px; }
|
|
2774
|
+
const RtnTrack = styled.div`
|
|
2775
|
+
flex: 1;
|
|
2776
|
+
height: 10px;
|
|
2777
|
+
background: #eaeaef;
|
|
2778
|
+
border-radius: 4px;
|
|
2779
|
+
overflow: hidden;
|
|
2331
2780
|
`;
|
|
2332
|
-
const
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2781
|
+
const RtnBar = styled.div`
|
|
2782
|
+
width: ${(p) => Math.max(p.$w, 0.5)}%;
|
|
2783
|
+
height: 100%;
|
|
2784
|
+
background: hsl(${(p) => p.$hue}, 60%, 50%);
|
|
2785
|
+
border-radius: 4px;
|
|
2786
|
+
transition: width 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
2787
|
+
`;
|
|
2788
|
+
const RtnPct = styled.span`
|
|
2338
2789
|
font-size: 10px;
|
|
2339
2790
|
font-weight: 700;
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2791
|
+
color: hsl(${(p) => p.$hue}, 60%, 60%);
|
|
2792
|
+
min-width: 38px;
|
|
2793
|
+
text-align: right;
|
|
2794
|
+
font-variant-numeric: tabular-nums;
|
|
2795
|
+
font-family: ${T.mono};
|
|
2796
|
+
`;
|
|
2797
|
+
const RtnTip = styled.div`
|
|
2798
|
+
font-size: 10px;
|
|
2799
|
+
color: ${T.textSecondary};
|
|
2800
|
+
min-height: 14px;
|
|
2801
|
+
font-variant-numeric: tabular-nums;
|
|
2802
|
+
padding: 0 6px;
|
|
2351
2803
|
`;
|
|
2352
|
-
const
|
|
2804
|
+
const RingGrid = styled.div`
|
|
2353
2805
|
display: flex;
|
|
2354
|
-
|
|
2806
|
+
flex-direction: column;
|
|
2355
2807
|
gap: 10px;
|
|
2356
|
-
margin-bottom: -8px;
|
|
2357
2808
|
`;
|
|
2358
|
-
const
|
|
2359
|
-
|
|
2809
|
+
const RingCard = styled(Card)`
|
|
2810
|
+
padding: 18px 20px;
|
|
2811
|
+
display: flex;
|
|
2812
|
+
align-items: center;
|
|
2813
|
+
gap: 16px;
|
|
2814
|
+
flex: 1;
|
|
2815
|
+
`;
|
|
2816
|
+
const RingInfo = styled.div`
|
|
2817
|
+
display: flex;
|
|
2818
|
+
flex-direction: column;
|
|
2819
|
+
gap: 4px;
|
|
2820
|
+
`;
|
|
2821
|
+
const RingVal = styled.div`
|
|
2822
|
+
font-size: 24px;
|
|
2823
|
+
font-weight: 800;
|
|
2824
|
+
color: ${T.textPrimary};
|
|
2825
|
+
letter-spacing: -0.04em;
|
|
2826
|
+
font-variant-numeric: tabular-nums;
|
|
2827
|
+
line-height: 1;
|
|
2828
|
+
`;
|
|
2829
|
+
const RingLabel = styled.div`
|
|
2830
|
+
font-size: 9px;
|
|
2360
2831
|
font-weight: 700;
|
|
2361
2832
|
text-transform: uppercase;
|
|
2362
|
-
letter-spacing: 0.
|
|
2363
|
-
color:
|
|
2364
|
-
white-space: nowrap;
|
|
2365
|
-
`;
|
|
2366
|
-
const DivLine = styled.div`
|
|
2367
|
-
flex: 1;
|
|
2368
|
-
height: 1px;
|
|
2369
|
-
background: #eaeaef;
|
|
2833
|
+
letter-spacing: 0.1em;
|
|
2834
|
+
color: ${T.textMuted};
|
|
2370
2835
|
`;
|
|
2371
2836
|
const Empty = styled.div`
|
|
2372
2837
|
display: flex;
|
|
@@ -2374,32 +2839,52 @@ const Empty = styled.div`
|
|
|
2374
2839
|
align-items: center;
|
|
2375
2840
|
justify-content: center;
|
|
2376
2841
|
padding: 32px;
|
|
2377
|
-
color:
|
|
2842
|
+
color: ${T.textMuted};
|
|
2378
2843
|
font-size: 12px;
|
|
2379
|
-
gap:
|
|
2844
|
+
gap: 8px;
|
|
2380
2845
|
`;
|
|
2846
|
+
function useCountUp(target, duration = 850) {
|
|
2847
|
+
const [count, setCount] = useState(0);
|
|
2848
|
+
const prevRef = useRef(0);
|
|
2849
|
+
useEffect(() => {
|
|
2850
|
+
let raf;
|
|
2851
|
+
let start = 0;
|
|
2852
|
+
const from = prevRef.current;
|
|
2853
|
+
const diff = target - from;
|
|
2854
|
+
const step = (ts) => {
|
|
2855
|
+
if (!start) start = ts;
|
|
2856
|
+
const progress = Math.min((ts - start) / duration, 1);
|
|
2857
|
+
const eased = 1 - (1 - progress) ** 3;
|
|
2858
|
+
setCount(Math.round(from + eased * diff));
|
|
2859
|
+
if (progress < 1) raf = requestAnimationFrame(step);
|
|
2860
|
+
else prevRef.current = target;
|
|
2861
|
+
};
|
|
2862
|
+
raf = requestAnimationFrame(step);
|
|
2863
|
+
return () => cancelAnimationFrame(raf);
|
|
2864
|
+
}, [target, duration]);
|
|
2865
|
+
return count;
|
|
2866
|
+
}
|
|
2381
2867
|
function Sparkline({
|
|
2382
2868
|
data,
|
|
2383
2869
|
color,
|
|
2384
2870
|
id
|
|
2385
2871
|
}) {
|
|
2386
2872
|
const W = 300;
|
|
2387
|
-
const H =
|
|
2873
|
+
const H = 44;
|
|
2388
2874
|
if (data.length < 2) return /* @__PURE__ */ jsx("svg", { width: "100%", height: H });
|
|
2389
2875
|
const min = Math.min(...data);
|
|
2390
2876
|
const max = Math.max(...data, min + 1);
|
|
2391
|
-
const range = max - min;
|
|
2392
2877
|
const pts = data.map((v, i) => ({
|
|
2393
2878
|
x: i / (data.length - 1) * W,
|
|
2394
|
-
y: H -
|
|
2879
|
+
y: H - 3 - (v - min) / (max - min) * (H - 8)
|
|
2395
2880
|
}));
|
|
2396
|
-
const line = pts.reduce((
|
|
2881
|
+
const line = pts.reduce((a, p, i) => {
|
|
2397
2882
|
if (i === 0) return `M ${p.x} ${p.y}`;
|
|
2398
2883
|
const pr = pts[i - 1];
|
|
2399
2884
|
const cx = (pr.x + p.x) / 2;
|
|
2400
|
-
return `${
|
|
2885
|
+
return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
|
|
2401
2886
|
}, "");
|
|
2402
|
-
const area = `${line} L ${pts
|
|
2887
|
+
const area = `${line} L ${pts.at(-1).x} ${H} L 0 ${H} Z`;
|
|
2403
2888
|
const gid = `spk-${id}`;
|
|
2404
2889
|
return /* @__PURE__ */ jsxs(
|
|
2405
2890
|
"svg",
|
|
@@ -2408,10 +2893,10 @@ function Sparkline({
|
|
|
2408
2893
|
height: H,
|
|
2409
2894
|
viewBox: `0 0 ${W} ${H}`,
|
|
2410
2895
|
preserveAspectRatio: "none",
|
|
2411
|
-
"aria-hidden":
|
|
2896
|
+
"aria-hidden": true,
|
|
2412
2897
|
children: [
|
|
2413
2898
|
/* @__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.
|
|
2899
|
+
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.32" }),
|
|
2415
2900
|
/* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
|
|
2416
2901
|
] }) }),
|
|
2417
2902
|
/* @__PURE__ */ jsx("path", { d: area, fill: `url(#${gid})` }),
|
|
@@ -2420,57 +2905,58 @@ function Sparkline({
|
|
|
2420
2905
|
{
|
|
2421
2906
|
d: line,
|
|
2422
2907
|
stroke: color,
|
|
2423
|
-
strokeWidth: "
|
|
2908
|
+
strokeWidth: "1.6",
|
|
2424
2909
|
fill: "none",
|
|
2425
|
-
strokeLinecap: "round"
|
|
2426
|
-
strokeLinejoin: "round"
|
|
2910
|
+
strokeLinecap: "round"
|
|
2427
2911
|
}
|
|
2428
2912
|
)
|
|
2429
2913
|
]
|
|
2430
2914
|
}
|
|
2431
2915
|
);
|
|
2432
2916
|
}
|
|
2433
|
-
const
|
|
2434
|
-
{ key: "totalUsers", color:
|
|
2435
|
-
{ key: "newUsers", color:
|
|
2436
|
-
{ key: "activeUsers", color:
|
|
2917
|
+
const ALL_SERIES = [
|
|
2918
|
+
{ key: "totalUsers", color: T.accent, label: "Total" },
|
|
2919
|
+
{ key: "newUsers", color: T.green, label: "New" },
|
|
2920
|
+
{ key: "activeUsers", color: T.amber, label: "Active" }
|
|
2437
2921
|
];
|
|
2438
|
-
function
|
|
2922
|
+
function GrowthChart({
|
|
2439
2923
|
data,
|
|
2440
2924
|
hovered,
|
|
2441
|
-
onHover
|
|
2925
|
+
onHover,
|
|
2926
|
+
activeSeries
|
|
2442
2927
|
}) {
|
|
2443
2928
|
const W = 600;
|
|
2444
|
-
const H =
|
|
2445
|
-
const PL =
|
|
2929
|
+
const H = 200;
|
|
2930
|
+
const PL = 40;
|
|
2446
2931
|
const PR = 12;
|
|
2447
2932
|
const PT = 12;
|
|
2448
2933
|
const PB = 28;
|
|
2449
2934
|
const CW = W - PL - PR;
|
|
2450
2935
|
const CH = H - PT - PB;
|
|
2451
|
-
if (data.length
|
|
2936
|
+
if (!data.length) {
|
|
2452
2937
|
return /* @__PURE__ */ jsxs(Empty, { children: [
|
|
2453
2938
|
/* @__PURE__ */ jsx("div", { style: { fontSize: 24 }, children: "📊" }),
|
|
2454
2939
|
/* @__PURE__ */ jsx("div", { children: "No growth data for this period" })
|
|
2455
2940
|
] });
|
|
2456
2941
|
}
|
|
2457
|
-
const
|
|
2942
|
+
const active = ALL_SERIES.filter((s) => activeSeries.has(s.key));
|
|
2943
|
+
const allVals = data.flatMap((d) => active.map((s) => d[s.key]));
|
|
2458
2944
|
const maxV = Math.max(...allVals, 10);
|
|
2459
2945
|
const yMax = Math.ceil(maxV * 1.15);
|
|
2460
2946
|
const xp = (i) => PL + i / Math.max(data.length - 1, 1) * CW;
|
|
2461
2947
|
const yp = (v) => PT + (1 - v / yMax) * CH;
|
|
2462
2948
|
const smooth = (vals) => {
|
|
2463
2949
|
const pts = vals.map((v, i) => ({ x: xp(i), y: yp(v) }));
|
|
2464
|
-
const path = pts.reduce((
|
|
2950
|
+
const path = pts.reduce((a, p, i) => {
|
|
2465
2951
|
if (i === 0) return `M ${p.x} ${p.y}`;
|
|
2466
2952
|
const pr = pts[i - 1];
|
|
2467
2953
|
const cx = (pr.x + p.x) / 2;
|
|
2468
|
-
return `${
|
|
2954
|
+
return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
|
|
2469
2955
|
}, "");
|
|
2470
|
-
const area = `${path} L ${pts
|
|
2956
|
+
const area = `${path} L ${pts.at(-1).x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
|
|
2471
2957
|
return { pts, path, area };
|
|
2472
2958
|
};
|
|
2473
|
-
const lines =
|
|
2959
|
+
const lines = active.map((s) => ({
|
|
2474
2960
|
...s,
|
|
2475
2961
|
...smooth(data.map((d) => d[s.key]))
|
|
2476
2962
|
}));
|
|
@@ -2493,17 +2979,17 @@ function AreaChart({
|
|
|
2493
2979
|
/* @__PURE__ */ jsx("defs", { children: lines.map((s) => /* @__PURE__ */ jsxs(
|
|
2494
2980
|
"linearGradient",
|
|
2495
2981
|
{
|
|
2496
|
-
id: `ag-${s.
|
|
2982
|
+
id: `ag-${s.key}`,
|
|
2497
2983
|
x1: "0",
|
|
2498
2984
|
y1: "0",
|
|
2499
2985
|
x2: "0",
|
|
2500
2986
|
y2: "1",
|
|
2501
2987
|
children: [
|
|
2502
|
-
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.
|
|
2988
|
+
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.18" }),
|
|
2503
2989
|
/* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: s.color, stopOpacity: "0" })
|
|
2504
2990
|
]
|
|
2505
2991
|
},
|
|
2506
|
-
s.
|
|
2992
|
+
s.key
|
|
2507
2993
|
)) }),
|
|
2508
2994
|
yTicks.map((t) => /* @__PURE__ */ jsxs("g", { children: [
|
|
2509
2995
|
/* @__PURE__ */ jsx(
|
|
@@ -2523,7 +3009,7 @@ function AreaChart({
|
|
|
2523
3009
|
x: PL - 6,
|
|
2524
3010
|
y: t.y + 4,
|
|
2525
3011
|
textAnchor: "end",
|
|
2526
|
-
fill:
|
|
3012
|
+
fill: T.textMuted,
|
|
2527
3013
|
fontSize: "9",
|
|
2528
3014
|
children: t.v >= 1e3 ? `${(t.v / 1e3).toFixed(1)}k` : t.v
|
|
2529
3015
|
}
|
|
@@ -2540,25 +3026,18 @@ function AreaChart({
|
|
|
2540
3026
|
strokeWidth: "1"
|
|
2541
3027
|
}
|
|
2542
3028
|
),
|
|
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
|
-
)),
|
|
3029
|
+
lines.map((s) => /* @__PURE__ */ jsx("path", { d: s.area, fill: `url(#ag-${s.key})` }, `area-${s.key}`)),
|
|
2551
3030
|
lines.map((s) => /* @__PURE__ */ jsx(
|
|
2552
3031
|
"path",
|
|
2553
3032
|
{
|
|
2554
3033
|
d: s.path,
|
|
2555
3034
|
stroke: s.color,
|
|
2556
|
-
strokeWidth: "
|
|
3035
|
+
strokeWidth: "2",
|
|
2557
3036
|
fill: "none",
|
|
2558
3037
|
strokeLinecap: "round",
|
|
2559
3038
|
strokeLinejoin: "round"
|
|
2560
3039
|
},
|
|
2561
|
-
`
|
|
3040
|
+
`line-${s.key}`
|
|
2562
3041
|
)),
|
|
2563
3042
|
data.map((d, i) => {
|
|
2564
3043
|
if (i % step !== 0 && i !== data.length - 1) return null;
|
|
@@ -2568,7 +3047,7 @@ function AreaChart({
|
|
|
2568
3047
|
x: xp(i),
|
|
2569
3048
|
y: H - 6,
|
|
2570
3049
|
textAnchor: "middle",
|
|
2571
|
-
fill:
|
|
3050
|
+
fill: T.textMuted,
|
|
2572
3051
|
fontSize: "9",
|
|
2573
3052
|
children: d.label
|
|
2574
3053
|
},
|
|
@@ -2578,7 +3057,7 @@ function AreaChart({
|
|
|
2578
3057
|
data.map((d, i) => {
|
|
2579
3058
|
const isHov = hovered === i;
|
|
2580
3059
|
return (
|
|
2581
|
-
// biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
|
|
3060
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
|
|
2582
3061
|
/* @__PURE__ */ jsxs(
|
|
2583
3062
|
"g",
|
|
2584
3063
|
{
|
|
@@ -2603,10 +3082,9 @@ function AreaChart({
|
|
|
2603
3082
|
y1: PT,
|
|
2604
3083
|
x2: xp(i),
|
|
2605
3084
|
y2: PT + CH,
|
|
2606
|
-
stroke: "
|
|
3085
|
+
stroke: "rgba(50,50,77,0.2)",
|
|
2607
3086
|
strokeWidth: "1",
|
|
2608
|
-
strokeDasharray: "3 3"
|
|
2609
|
-
opacity: "0.35"
|
|
3087
|
+
strokeDasharray: "3 3"
|
|
2610
3088
|
}
|
|
2611
3089
|
),
|
|
2612
3090
|
lines.map((s) => /* @__PURE__ */ jsx(
|
|
@@ -2616,11 +3094,11 @@ function AreaChart({
|
|
|
2616
3094
|
cy: yp(d[s.key]),
|
|
2617
3095
|
r: isHov ? 4 : 2.5,
|
|
2618
3096
|
fill: s.color,
|
|
2619
|
-
stroke:
|
|
3097
|
+
stroke: T.bg,
|
|
2620
3098
|
strokeWidth: isHov ? 2 : 1,
|
|
2621
|
-
opacity: isHov ? 1 : 0.
|
|
3099
|
+
opacity: isHov ? 1 : 0.5
|
|
2622
3100
|
},
|
|
2623
|
-
s.
|
|
3101
|
+
s.key
|
|
2624
3102
|
))
|
|
2625
3103
|
]
|
|
2626
3104
|
},
|
|
@@ -2632,23 +3110,70 @@ function AreaChart({
|
|
|
2632
3110
|
}
|
|
2633
3111
|
);
|
|
2634
3112
|
}
|
|
3113
|
+
function ProgressRing({
|
|
3114
|
+
value,
|
|
3115
|
+
max,
|
|
3116
|
+
color,
|
|
3117
|
+
size = 56
|
|
3118
|
+
}) {
|
|
3119
|
+
const r = (size - 9) / 2;
|
|
3120
|
+
const circ = 2 * Math.PI * r;
|
|
3121
|
+
const pct = Math.min(value / Math.max(max, 1), 1);
|
|
3122
|
+
const offset = circ * (1 - pct);
|
|
3123
|
+
return /* @__PURE__ */ jsxs(
|
|
3124
|
+
"svg",
|
|
3125
|
+
{
|
|
3126
|
+
width: size,
|
|
3127
|
+
height: size,
|
|
3128
|
+
style: { flexShrink: 0 },
|
|
3129
|
+
"aria-hidden": true,
|
|
3130
|
+
children: [
|
|
3131
|
+
/* @__PURE__ */ jsx(
|
|
3132
|
+
"circle",
|
|
3133
|
+
{
|
|
3134
|
+
cx: size / 2,
|
|
3135
|
+
cy: size / 2,
|
|
3136
|
+
r,
|
|
3137
|
+
fill: "none",
|
|
3138
|
+
stroke: "#e5e7eb",
|
|
3139
|
+
strokeWidth: "7"
|
|
3140
|
+
}
|
|
3141
|
+
),
|
|
3142
|
+
/* @__PURE__ */ jsx(
|
|
3143
|
+
"circle",
|
|
3144
|
+
{
|
|
3145
|
+
cx: size / 2,
|
|
3146
|
+
cy: size / 2,
|
|
3147
|
+
r,
|
|
3148
|
+
fill: "none",
|
|
3149
|
+
stroke: color,
|
|
3150
|
+
strokeWidth: "7",
|
|
3151
|
+
strokeDasharray: `${circ} ${circ}`,
|
|
3152
|
+
strokeDashoffset: offset,
|
|
3153
|
+
strokeLinecap: "round",
|
|
3154
|
+
transform: `rotate(-90 ${size / 2} ${size / 2})`,
|
|
3155
|
+
style: {
|
|
3156
|
+
transition: "stroke-dashoffset 0.9s cubic-bezier(0.4,0,0.2,1)"
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
)
|
|
3160
|
+
]
|
|
3161
|
+
}
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
2635
3164
|
function relTime(date) {
|
|
2636
|
-
const
|
|
2637
|
-
const
|
|
2638
|
-
if (
|
|
2639
|
-
if (
|
|
2640
|
-
const
|
|
2641
|
-
if (
|
|
2642
|
-
return `${Math.floor(
|
|
3165
|
+
const diff = Date.now() - new Date(date).getTime();
|
|
3166
|
+
const mins = Math.floor(diff / 6e4);
|
|
3167
|
+
if (mins < 1) return "just now";
|
|
3168
|
+
if (mins < 60) return `${mins}m`;
|
|
3169
|
+
const hrs = Math.floor(mins / 60);
|
|
3170
|
+
if (hrs < 24) return `${hrs}h`;
|
|
3171
|
+
return `${Math.floor(hrs / 24)}d`;
|
|
2643
3172
|
}
|
|
2644
|
-
function
|
|
2645
|
-
return
|
|
2646
|
-
month: "short",
|
|
2647
|
-
day: "numeric",
|
|
2648
|
-
year: "numeric"
|
|
2649
|
-
});
|
|
3173
|
+
function rateHue(r) {
|
|
3174
|
+
return r >= 70 ? 142 : r >= 40 ? 38 : 4;
|
|
2650
3175
|
}
|
|
2651
|
-
function
|
|
3176
|
+
function StatItem({
|
|
2652
3177
|
id,
|
|
2653
3178
|
label,
|
|
2654
3179
|
value,
|
|
@@ -2657,26 +3182,39 @@ function StatCardItem({
|
|
|
2657
3182
|
color,
|
|
2658
3183
|
delay = 0
|
|
2659
3184
|
}) {
|
|
3185
|
+
const animated = useCountUp(value);
|
|
2660
3186
|
const isPos = pct === void 0 || pct >= 0;
|
|
2661
|
-
return /* @__PURE__ */ jsxs(
|
|
2662
|
-
/* @__PURE__ */
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
"%"
|
|
2670
|
-
] })
|
|
3187
|
+
return /* @__PURE__ */ jsxs(StatCard, { $delay: delay, $accent: color, children: [
|
|
3188
|
+
/* @__PURE__ */ jsx(StatLabel, { children: label }),
|
|
3189
|
+
/* @__PURE__ */ jsx(StatValue, { children: animated.toLocaleString() }),
|
|
3190
|
+
pct !== void 0 && /* @__PURE__ */ jsxs(TrendBadge, { $pos: isPos, children: [
|
|
3191
|
+
isPos ? "↑" : "↓",
|
|
3192
|
+
" ",
|
|
3193
|
+
Math.abs(pct).toFixed(1),
|
|
3194
|
+
"%"
|
|
2671
3195
|
] }),
|
|
2672
3196
|
sparkline && sparkline.length > 1 && /* @__PURE__ */ jsx(SparkWrap, { children: /* @__PURE__ */ jsx(Sparkline, { data: sparkline, color, id }) })
|
|
2673
3197
|
] });
|
|
2674
3198
|
}
|
|
2675
3199
|
function OverviewPage() {
|
|
3200
|
+
const { get } = useFetchClient();
|
|
2676
3201
|
const [period, setPeriod] = useState(
|
|
2677
3202
|
"weekly"
|
|
2678
3203
|
);
|
|
3204
|
+
const [feedMode, setFeedMode] = useState("signups");
|
|
2679
3205
|
const [hovIdx, setHovIdx] = useState(null);
|
|
3206
|
+
const [rtnHov, setRtnHov] = useState(null);
|
|
3207
|
+
const [activeSeries, setActiveSeries] = useState(
|
|
3208
|
+
() => /* @__PURE__ */ new Set(["totalUsers", "newUsers", "activeUsers"])
|
|
3209
|
+
);
|
|
3210
|
+
const toggleSeries = (key) => {
|
|
3211
|
+
setActiveSeries((prev) => {
|
|
3212
|
+
const next = new Set(prev);
|
|
3213
|
+
if (next.has(key) && next.size > 1) next.delete(key);
|
|
3214
|
+
else next.add(key);
|
|
3215
|
+
return next;
|
|
3216
|
+
});
|
|
3217
|
+
};
|
|
2680
3218
|
const statsQuery = useQuery({
|
|
2681
3219
|
queryKey: ["dash-user-stats"],
|
|
2682
3220
|
queryFn: async () => {
|
|
@@ -2693,12 +3231,10 @@ function OverviewPage() {
|
|
|
2693
3231
|
return r.data;
|
|
2694
3232
|
}
|
|
2695
3233
|
});
|
|
2696
|
-
const
|
|
2697
|
-
queryKey: ["dash-
|
|
3234
|
+
const retentionQuery = useQuery({
|
|
3235
|
+
queryKey: ["dash-user-retention", period],
|
|
2698
3236
|
queryFn: async () => {
|
|
2699
|
-
const r = await client.dash.
|
|
2700
|
-
query: { page: 1, limit: 10 }
|
|
2701
|
-
});
|
|
3237
|
+
const r = await client.dash.userRetentionData({ query: { period } });
|
|
2702
3238
|
if (r.error) throw new Error(r.error.message ?? "Failed");
|
|
2703
3239
|
return r.data;
|
|
2704
3240
|
}
|
|
@@ -2707,12 +3243,22 @@ function OverviewPage() {
|
|
|
2707
3243
|
queryKey: ["dash-recent-users"],
|
|
2708
3244
|
queryFn: async () => {
|
|
2709
3245
|
const r = await client.dash.listUsers({
|
|
2710
|
-
query: {
|
|
3246
|
+
query: { limit: 12, offset: 0, sortBy: "createdAt", sortOrder: "desc" }
|
|
2711
3247
|
});
|
|
2712
3248
|
if (r.error) throw new Error(r.error.message ?? "Failed");
|
|
2713
3249
|
return r.data;
|
|
2714
3250
|
}
|
|
2715
3251
|
});
|
|
3252
|
+
const sessionsQuery = useQuery({
|
|
3253
|
+
queryKey: ["dash-recent-sessions"],
|
|
3254
|
+
queryFn: async () => {
|
|
3255
|
+
const { data } = await get(
|
|
3256
|
+
"/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=createdAt:desc"
|
|
3257
|
+
);
|
|
3258
|
+
return data.results ?? [];
|
|
3259
|
+
},
|
|
3260
|
+
refetchInterval: 3e4
|
|
3261
|
+
});
|
|
2716
3262
|
const orgsQuery = useQuery({
|
|
2717
3263
|
queryKey: ["dash-orgs-count"],
|
|
2718
3264
|
queryFn: async () => {
|
|
@@ -2724,594 +3270,344 @@ function OverviewPage() {
|
|
|
2724
3270
|
}
|
|
2725
3271
|
});
|
|
2726
3272
|
if (statsQuery.isLoading) {
|
|
2727
|
-
return /* @__PURE__ */ jsx(
|
|
3273
|
+
return /* @__PURE__ */ jsx(
|
|
3274
|
+
Flex,
|
|
3275
|
+
{
|
|
3276
|
+
justifyContent: "center",
|
|
3277
|
+
alignItems: "center",
|
|
3278
|
+
padding: 12,
|
|
3279
|
+
style: { background: T.bg, minHeight: "100%" },
|
|
3280
|
+
children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
|
|
3281
|
+
}
|
|
3282
|
+
);
|
|
2728
3283
|
}
|
|
2729
3284
|
const stats = statsQuery.data;
|
|
2730
3285
|
if (!stats) return null;
|
|
2731
3286
|
const graphData = graphQuery.data?.data ?? [];
|
|
2732
|
-
const
|
|
2733
|
-
const
|
|
3287
|
+
const rtnData = retentionQuery.data?.data ?? [];
|
|
3288
|
+
const users = usersQuery.data?.users ?? [];
|
|
3289
|
+
const sessions = sessionsQuery.data ?? [];
|
|
2734
3290
|
const orgCount = orgsQuery.data?.total ?? 0;
|
|
2735
3291
|
const totalSpark = graphData.map((d) => d.totalUsers);
|
|
2736
3292
|
const newSpark = graphData.map((d) => d.newUsers);
|
|
2737
3293
|
const activeSpark = graphData.map((d) => d.activeUsers);
|
|
2738
3294
|
const hovRow = hovIdx !== null ? graphData[hovIdx] : null;
|
|
2739
|
-
|
|
3295
|
+
const rtnHovRow = rtnHov !== null ? rtnData[rtnHov] : null;
|
|
3296
|
+
const activeMax = Math.max(
|
|
3297
|
+
stats.activeUsers.daily.active ?? 0,
|
|
3298
|
+
stats.activeUsers.weekly.active ?? 0,
|
|
3299
|
+
stats.activeUsers.monthly.active ?? 0,
|
|
3300
|
+
1
|
|
3301
|
+
);
|
|
3302
|
+
return /* @__PURE__ */ jsxs(Wrap$1, { "data-testid": "overview-page", children: [
|
|
2740
3303
|
/* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-end", children: [
|
|
2741
|
-
/* @__PURE__ */ jsxs(
|
|
2742
|
-
/* @__PURE__ */ jsx(
|
|
2743
|
-
|
|
3304
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
3305
|
+
/* @__PURE__ */ jsx(
|
|
3306
|
+
"div",
|
|
3307
|
+
{
|
|
3308
|
+
style: {
|
|
3309
|
+
fontSize: 22,
|
|
3310
|
+
fontWeight: 800,
|
|
3311
|
+
color: T.textPrimary,
|
|
3312
|
+
letterSpacing: "-0.03em"
|
|
3313
|
+
},
|
|
3314
|
+
children: "Overview"
|
|
3315
|
+
}
|
|
3316
|
+
),
|
|
3317
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: T.textSecondary, marginTop: 4 }, children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
|
|
2744
3318
|
weekday: "long",
|
|
2745
3319
|
year: "numeric",
|
|
2746
3320
|
month: "long",
|
|
2747
3321
|
day: "numeric"
|
|
2748
|
-
}) })
|
|
3322
|
+
}) })
|
|
2749
3323
|
] }),
|
|
2750
|
-
/* @__PURE__ */ jsx(
|
|
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
|
-
) })
|
|
3324
|
+
/* @__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)) })
|
|
2764
3325
|
] }),
|
|
2765
|
-
/* @__PURE__ */ jsxs(
|
|
2766
|
-
/* @__PURE__ */ jsx(DivLabel, { children: "
|
|
3326
|
+
/* @__PURE__ */ jsxs(SectionDivider, { children: [
|
|
3327
|
+
/* @__PURE__ */ jsx(DivLabel, { children: "Metrics" }),
|
|
2767
3328
|
/* @__PURE__ */ jsx(DivLine, {})
|
|
2768
3329
|
] }),
|
|
2769
|
-
/* @__PURE__ */ jsxs(
|
|
3330
|
+
/* @__PURE__ */ jsxs(StatGrid, { children: [
|
|
2770
3331
|
/* @__PURE__ */ jsx(
|
|
2771
|
-
|
|
3332
|
+
StatItem,
|
|
2772
3333
|
{
|
|
2773
3334
|
id: "total",
|
|
2774
3335
|
label: "Total Users",
|
|
2775
3336
|
value: stats.total ?? 0,
|
|
2776
3337
|
sparkline: totalSpark,
|
|
2777
|
-
color:
|
|
3338
|
+
color: T.accent,
|
|
2778
3339
|
delay: 0
|
|
2779
3340
|
}
|
|
2780
3341
|
),
|
|
2781
3342
|
/* @__PURE__ */ jsx(
|
|
2782
|
-
|
|
3343
|
+
StatItem,
|
|
2783
3344
|
{
|
|
2784
3345
|
id: "d-sig",
|
|
2785
3346
|
label: "Daily Sign-ups",
|
|
2786
3347
|
value: stats.daily.signUps ?? 0,
|
|
2787
3348
|
pct: stats.daily.percentage ?? void 0,
|
|
2788
3349
|
sparkline: newSpark,
|
|
2789
|
-
color:
|
|
3350
|
+
color: T.green,
|
|
2790
3351
|
delay: 1
|
|
2791
3352
|
}
|
|
2792
3353
|
),
|
|
2793
3354
|
/* @__PURE__ */ jsx(
|
|
2794
|
-
|
|
3355
|
+
StatItem,
|
|
2795
3356
|
{
|
|
2796
3357
|
id: "w-sig",
|
|
2797
3358
|
label: "Weekly Sign-ups",
|
|
2798
3359
|
value: stats.weekly.signUps ?? 0,
|
|
2799
3360
|
pct: stats.weekly.percentage ?? void 0,
|
|
2800
3361
|
sparkline: newSpark,
|
|
2801
|
-
color:
|
|
3362
|
+
color: T.green,
|
|
2802
3363
|
delay: 2
|
|
2803
3364
|
}
|
|
2804
|
-
),
|
|
2805
|
-
/* @__PURE__ */ jsx(
|
|
2806
|
-
|
|
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
|
-
}
|
|
2816
|
-
),
|
|
2817
|
-
/* @__PURE__ */ jsx(
|
|
2818
|
-
StatCardItem,
|
|
2819
|
-
{
|
|
2820
|
-
id: "orgs",
|
|
2821
|
-
label: "Organizations",
|
|
2822
|
-
value: orgCount,
|
|
2823
|
-
color: "#9E6BF9",
|
|
2824
|
-
delay: 4
|
|
2825
|
-
}
|
|
2826
|
-
)
|
|
2827
|
-
] }),
|
|
2828
|
-
/* @__PURE__ */ jsxs(MainRow, { children: [
|
|
2829
|
-
/* @__PURE__ */ jsxs(ChartCard, { $delay: 5, children: [
|
|
2830
|
-
/* @__PURE__ */ jsxs(ChartHeader, { children: [
|
|
2831
|
-
/* @__PURE__ */ jsx(ChartTitle, { children: "User Growth" }),
|
|
2832
|
-
/* @__PURE__ */ jsx(LegendRow, { children: SERIES.map((s) => /* @__PURE__ */ jsxs(LegendItem, { children: [
|
|
2833
|
-
/* @__PURE__ */ jsx(LegendDot, { $c: s.color }),
|
|
2834
|
-
s.label
|
|
2835
|
-
] }, s.color)) })
|
|
2836
|
-
] }),
|
|
2837
|
-
/* @__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" }),
|
|
2838
|
-
graphQuery.isLoading ? /* @__PURE__ */ jsx(
|
|
2839
|
-
Flex,
|
|
2840
|
-
{
|
|
2841
|
-
justifyContent: "center",
|
|
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: [
|
|
2851
|
-
/* @__PURE__ */ jsx(
|
|
2852
|
-
Avatar,
|
|
2853
|
-
{
|
|
2854
|
-
name: s.user?.name ?? "?",
|
|
2855
|
-
src: s.user?.image,
|
|
2856
|
-
size: 28
|
|
2857
|
-
}
|
|
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,
|
|
3365
|
+
),
|
|
3366
|
+
/* @__PURE__ */ jsx(
|
|
3367
|
+
StatItem,
|
|
3187
3368
|
{
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3369
|
+
id: "m-sig",
|
|
3370
|
+
label: "Monthly Sign-ups",
|
|
3371
|
+
value: stats.monthly.signUps ?? 0,
|
|
3372
|
+
pct: stats.monthly.percentage ?? void 0,
|
|
3373
|
+
sparkline: newSpark,
|
|
3374
|
+
color: T.green,
|
|
3375
|
+
delay: 3
|
|
3376
|
+
}
|
|
3377
|
+
),
|
|
3378
|
+
/* @__PURE__ */ jsx(
|
|
3379
|
+
StatItem,
|
|
3380
|
+
{
|
|
3381
|
+
id: "orgs",
|
|
3382
|
+
label: "Organizations",
|
|
3383
|
+
value: orgCount,
|
|
3384
|
+
color: T.purple,
|
|
3385
|
+
delay: 4
|
|
3198
3386
|
}
|
|
3199
3387
|
)
|
|
3200
3388
|
] }),
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
/* @__PURE__ */
|
|
3204
|
-
|
|
3205
|
-
|
|
3389
|
+
/* @__PURE__ */ jsxs(SectionDivider, { children: [
|
|
3390
|
+
/* @__PURE__ */ jsx(DivLabel, { children: "Growth" }),
|
|
3391
|
+
/* @__PURE__ */ jsx(DivLine, {})
|
|
3392
|
+
] }),
|
|
3393
|
+
/* @__PURE__ */ jsxs(TwoPanel, { children: [
|
|
3394
|
+
/* @__PURE__ */ jsxs(ChartCard, { $delay: 5, children: [
|
|
3395
|
+
/* @__PURE__ */ jsxs(ChartHeader, { children: [
|
|
3396
|
+
/* @__PURE__ */ jsx(ChartTitle, { children: "User Growth" }),
|
|
3397
|
+
/* @__PURE__ */ jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxs(
|
|
3398
|
+
SeriesBtn,
|
|
3399
|
+
{
|
|
3400
|
+
$on: activeSeries.has(s.key),
|
|
3401
|
+
$c: s.color,
|
|
3402
|
+
onClick: () => toggleSeries(s.key),
|
|
3403
|
+
children: [
|
|
3404
|
+
/* @__PURE__ */ jsx(SDot, { $c: s.color }),
|
|
3405
|
+
s.label
|
|
3406
|
+
]
|
|
3407
|
+
},
|
|
3408
|
+
s.key
|
|
3409
|
+
)) })
|
|
3410
|
+
] }),
|
|
3411
|
+
/* @__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" }),
|
|
3412
|
+
graphQuery.isLoading ? /* @__PURE__ */ jsx(
|
|
3413
|
+
Flex,
|
|
3206
3414
|
{
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3415
|
+
justifyContent: "center",
|
|
3416
|
+
alignItems: "center",
|
|
3417
|
+
style: { flex: 1, minHeight: 170 },
|
|
3418
|
+
children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
|
|
3210
3419
|
}
|
|
3211
|
-
)
|
|
3212
|
-
|
|
3213
|
-
|
|
3420
|
+
) : /* @__PURE__ */ jsx(
|
|
3421
|
+
GrowthChart,
|
|
3422
|
+
{
|
|
3423
|
+
data: graphData,
|
|
3424
|
+
hovered: hovIdx,
|
|
3425
|
+
onHover: setHovIdx,
|
|
3426
|
+
activeSeries
|
|
3427
|
+
}
|
|
3428
|
+
)
|
|
3214
3429
|
] }),
|
|
3215
|
-
|
|
3216
|
-
/* @__PURE__ */ jsxs(
|
|
3217
|
-
/* @__PURE__ */ jsx(
|
|
3218
|
-
|
|
3430
|
+
/* @__PURE__ */ jsxs(FeedCard, { $delay: 6, children: [
|
|
3431
|
+
/* @__PURE__ */ jsxs(FeedHead, { children: [
|
|
3432
|
+
/* @__PURE__ */ jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recent Active" }),
|
|
3433
|
+
/* @__PURE__ */ jsxs(
|
|
3434
|
+
FeedSelect,
|
|
3219
3435
|
{
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3436
|
+
value: feedMode,
|
|
3437
|
+
onChange: (e) => setFeedMode(e.target.value),
|
|
3438
|
+
children: [
|
|
3439
|
+
/* @__PURE__ */ jsx("option", { value: "signups", children: "Recent Sign-ups" }),
|
|
3440
|
+
/* @__PURE__ */ jsx("option", { value: "active", children: "Recent Active" })
|
|
3441
|
+
]
|
|
3223
3442
|
}
|
|
3224
|
-
)
|
|
3225
|
-
/* @__PURE__ */ jsx(Avatar, { name: userRow.name ?? "", src: null, size: 30 }),
|
|
3226
|
-
/* @__PURE__ */ jsxs(UserInfo, { children: [
|
|
3227
|
-
/* @__PURE__ */ jsx(UserName$1, { children: userRow.name }),
|
|
3228
|
-
/* @__PURE__ */ jsx(UserEmail, { children: userRow.email })
|
|
3229
|
-
] }),
|
|
3230
|
-
/* @__PURE__ */ jsxs(SessionCountChip, { children: [
|
|
3231
|
-
userRow.sessions.length,
|
|
3232
|
-
" session",
|
|
3233
|
-
userRow.sessions.length !== 1 ? "s" : ""
|
|
3234
|
-
] })
|
|
3443
|
+
)
|
|
3235
3444
|
] }),
|
|
3236
|
-
|
|
3237
|
-
/* @__PURE__ */ jsxs(
|
|
3238
|
-
/* @__PURE__ */ jsxs(
|
|
3239
|
-
|
|
3445
|
+
/* @__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: [
|
|
3446
|
+
/* @__PURE__ */ jsxs(FeedTop, { children: [
|
|
3447
|
+
/* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
|
|
3448
|
+
/* @__PURE__ */ jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
|
|
3449
|
+
/* @__PURE__ */ jsx(FeedName, { title: u.name, children: u.name })
|
|
3450
|
+
] }),
|
|
3451
|
+
/* @__PURE__ */ jsx(FeedMeta, { children: relTime(u.createdAt) })
|
|
3452
|
+
] }),
|
|
3453
|
+
/* @__PURE__ */ jsx(FeedEmail, { title: u.email, children: u.email })
|
|
3454
|
+
] }, u.id)) : sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : sessions.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No sessions yet" }) : sessions.map((s) => /* @__PURE__ */ jsxs(FeedItem, { children: [
|
|
3455
|
+
/* @__PURE__ */ jsxs(FeedTop, { children: [
|
|
3456
|
+
/* @__PURE__ */ jsx(
|
|
3457
|
+
FeedName,
|
|
3240
3458
|
{
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
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
|
-
]
|
|
3459
|
+
title: s.userAgent ?? void 0,
|
|
3460
|
+
style: { fontFamily: T.mono, fontSize: 10 },
|
|
3461
|
+
children: s.ipAddress ?? "—"
|
|
3255
3462
|
}
|
|
3256
3463
|
),
|
|
3257
|
-
|
|
3464
|
+
/* @__PURE__ */ jsx(FeedMeta, { children: relTime(s.createdAt) })
|
|
3258
3465
|
] }),
|
|
3259
3466
|
/* @__PURE__ */ jsx(
|
|
3260
|
-
|
|
3467
|
+
FeedEmail,
|
|
3261
3468
|
{
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3469
|
+
title: s.userAgent ?? void 0,
|
|
3470
|
+
style: {
|
|
3471
|
+
fontSize: 9,
|
|
3472
|
+
overflow: "hidden",
|
|
3473
|
+
textOverflow: "ellipsis",
|
|
3474
|
+
whiteSpace: "nowrap"
|
|
3475
|
+
},
|
|
3476
|
+
children: s.userAgent ?? "Unknown agent"
|
|
3267
3477
|
}
|
|
3268
3478
|
)
|
|
3269
|
-
] },
|
|
3270
|
-
] }
|
|
3271
|
-
] })
|
|
3272
|
-
|
|
3273
|
-
/* @__PURE__ */ jsx(
|
|
3274
|
-
|
|
3479
|
+
] }, s.documentId)) })
|
|
3480
|
+
] })
|
|
3481
|
+
] }),
|
|
3482
|
+
/* @__PURE__ */ jsxs(SectionDivider, { children: [
|
|
3483
|
+
/* @__PURE__ */ jsx(DivLabel, { children: "Retention & Activity" }),
|
|
3484
|
+
/* @__PURE__ */ jsx(DivLine, {})
|
|
3485
|
+
] }),
|
|
3486
|
+
/* @__PURE__ */ jsxs(TwoPanel, { children: [
|
|
3487
|
+
/* @__PURE__ */ jsxs(RtnCard, { $delay: 7, children: [
|
|
3488
|
+
/* @__PURE__ */ jsxs(ChartHeader, { children: [
|
|
3489
|
+
/* @__PURE__ */ jsx(ChartTitle, { children: "Cohort Retention" }),
|
|
3490
|
+
/* @__PURE__ */ jsx(Flex, { alignItems: "center", gap: 2, children: [
|
|
3491
|
+
{ hue: 142, label: "≥70%" },
|
|
3492
|
+
{ hue: 38, label: "40–70%" },
|
|
3493
|
+
{ hue: 4, label: "<40%" }
|
|
3494
|
+
].map(({ hue, label }) => /* @__PURE__ */ jsxs(
|
|
3495
|
+
Flex,
|
|
3496
|
+
{
|
|
3497
|
+
alignItems: "center",
|
|
3498
|
+
gap: 1,
|
|
3499
|
+
style: { marginLeft: 8 },
|
|
3500
|
+
children: [
|
|
3501
|
+
/* @__PURE__ */ jsx(
|
|
3502
|
+
"span",
|
|
3503
|
+
{
|
|
3504
|
+
style: {
|
|
3505
|
+
width: 8,
|
|
3506
|
+
height: 8,
|
|
3507
|
+
borderRadius: "50%",
|
|
3508
|
+
background: `hsl(${hue},60%,50%)`,
|
|
3509
|
+
display: "inline-block",
|
|
3510
|
+
flexShrink: 0
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
),
|
|
3514
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: T.textMuted }, children: label })
|
|
3515
|
+
]
|
|
3516
|
+
},
|
|
3517
|
+
hue
|
|
3518
|
+
)) })
|
|
3519
|
+
] }),
|
|
3520
|
+
/* @__PURE__ */ jsx(RtnTip, { children: rtnHovRow ? `Cohort ${rtnHovRow.label} · ${rtnHovRow.cohortSize.toLocaleString()} users · active ${rtnHovRow.activeStart} – ${rtnHovRow.activeEnd}` : "Hover a row to see cohort details" }),
|
|
3521
|
+
retentionQuery.isLoading ? /* @__PURE__ */ jsx(
|
|
3522
|
+
Flex,
|
|
3523
|
+
{
|
|
3524
|
+
justifyContent: "center",
|
|
3525
|
+
alignItems: "center",
|
|
3526
|
+
style: { minHeight: 80 },
|
|
3527
|
+
children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
|
|
3528
|
+
}
|
|
3529
|
+
) : rtnData.length === 0 ? /* @__PURE__ */ jsxs(Empty, { children: [
|
|
3530
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 22 }, children: "📉" }),
|
|
3531
|
+
/* @__PURE__ */ jsx("div", { children: "No retention data for this period" })
|
|
3532
|
+
] }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 5 }, children: rtnData.map((row, i) => {
|
|
3533
|
+
const hue = rateHue(row.retentionRate);
|
|
3534
|
+
return /* @__PURE__ */ jsxs(
|
|
3535
|
+
RtnRow,
|
|
3536
|
+
{
|
|
3537
|
+
$hov: rtnHov === i,
|
|
3538
|
+
onMouseEnter: () => setRtnHov(i),
|
|
3539
|
+
onMouseLeave: () => setRtnHov(null),
|
|
3540
|
+
children: [
|
|
3541
|
+
/* @__PURE__ */ jsx(RtnLabel, { children: row.label }),
|
|
3542
|
+
/* @__PURE__ */ jsx(RtnSize, { children: row.cohortSize.toLocaleString() }),
|
|
3543
|
+
/* @__PURE__ */ jsx(RtnTrack, { children: /* @__PURE__ */ jsx(RtnBar, { $w: row.retentionRate, $hue: hue }) }),
|
|
3544
|
+
/* @__PURE__ */ jsxs(RtnPct, { $hue: hue, children: [
|
|
3545
|
+
row.retentionRate.toFixed(1),
|
|
3546
|
+
"%"
|
|
3547
|
+
] })
|
|
3548
|
+
]
|
|
3549
|
+
},
|
|
3550
|
+
row.n
|
|
3551
|
+
);
|
|
3552
|
+
}) })
|
|
3553
|
+
] }),
|
|
3554
|
+
/* @__PURE__ */ jsx(RingGrid, { children: [
|
|
3275
3555
|
{
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
/* @__PURE__ */ jsx(
|
|
3284
|
-
Button,
|
|
3556
|
+
label: "Daily Active",
|
|
3557
|
+
value: stats.activeUsers.daily.active,
|
|
3558
|
+
pct: stats.activeUsers.daily.percentage,
|
|
3559
|
+
color: T.amber,
|
|
3560
|
+
sparkline: activeSpark,
|
|
3561
|
+
delay: 8
|
|
3562
|
+
},
|
|
3285
3563
|
{
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3564
|
+
label: "Weekly Active",
|
|
3565
|
+
value: stats.activeUsers.weekly.active,
|
|
3566
|
+
pct: stats.activeUsers.weekly.percentage,
|
|
3567
|
+
color: T.green,
|
|
3568
|
+
sparkline: activeSpark,
|
|
3569
|
+
delay: 9
|
|
3570
|
+
},
|
|
3571
|
+
{
|
|
3572
|
+
label: "Monthly Active",
|
|
3573
|
+
value: stats.activeUsers.monthly.active,
|
|
3574
|
+
pct: stats.activeUsers.monthly.percentage,
|
|
3575
|
+
color: T.accent,
|
|
3576
|
+
sparkline: activeSpark,
|
|
3577
|
+
delay: 10
|
|
3290
3578
|
}
|
|
3291
|
-
)
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3579
|
+
].map(({ label, value, pct, color, delay }) => {
|
|
3580
|
+
const isPos = pct == null || pct >= 0;
|
|
3581
|
+
return /* @__PURE__ */ jsxs(RingCard, { $delay: delay, children: [
|
|
3582
|
+
/* @__PURE__ */ jsx(
|
|
3583
|
+
ProgressRing,
|
|
3584
|
+
{
|
|
3585
|
+
value: value ?? 0,
|
|
3586
|
+
max: activeMax,
|
|
3587
|
+
color,
|
|
3588
|
+
size: 56
|
|
3589
|
+
}
|
|
3590
|
+
),
|
|
3591
|
+
/* @__PURE__ */ jsxs(RingInfo, { children: [
|
|
3592
|
+
/* @__PURE__ */ jsx(RingLabel, { children: label }),
|
|
3593
|
+
/* @__PURE__ */ jsx(RingVal, { children: (value ?? 0).toLocaleString() }),
|
|
3594
|
+
pct != null && /* @__PURE__ */ jsxs(
|
|
3595
|
+
TrendBadge,
|
|
3596
|
+
{
|
|
3597
|
+
$pos: isPos,
|
|
3598
|
+
style: { marginTop: 4, marginBottom: 0 },
|
|
3599
|
+
children: [
|
|
3600
|
+
isPos ? "↑" : "↓",
|
|
3601
|
+
" ",
|
|
3602
|
+
Math.abs(pct).toFixed(1),
|
|
3603
|
+
"%"
|
|
3604
|
+
]
|
|
3605
|
+
}
|
|
3606
|
+
)
|
|
3607
|
+
] })
|
|
3608
|
+
] }, label);
|
|
3609
|
+
}) })
|
|
3610
|
+
] })
|
|
3315
3611
|
] });
|
|
3316
3612
|
}
|
|
3317
3613
|
function useUsers(options = {}) {
|
|
@@ -3552,6 +3848,44 @@ const ReadOnlyCodeInput = styled.div`
|
|
|
3552
3848
|
word-break: break-all;
|
|
3553
3849
|
overflow-wrap: anywhere;
|
|
3554
3850
|
`;
|
|
3851
|
+
const SessionCard = styled.div`
|
|
3852
|
+
display: flex;
|
|
3853
|
+
align-items: flex-start;
|
|
3854
|
+
justify-content: space-between;
|
|
3855
|
+
gap: 12px;
|
|
3856
|
+
padding: 10px 0;
|
|
3857
|
+
border-bottom: 1px solid #f5f5f9;
|
|
3858
|
+
&:last-child { border-bottom: none; }
|
|
3859
|
+
`;
|
|
3860
|
+
const SessionMeta = styled.div`
|
|
3861
|
+
display: flex;
|
|
3862
|
+
flex-direction: column;
|
|
3863
|
+
gap: 3px;
|
|
3864
|
+
flex: 1;
|
|
3865
|
+
min-width: 0;
|
|
3866
|
+
`;
|
|
3867
|
+
const IpChip = styled.span`
|
|
3868
|
+
display: inline-block;
|
|
3869
|
+
background: #f0f0ff;
|
|
3870
|
+
color: #4945ff;
|
|
3871
|
+
border-radius: 5px;
|
|
3872
|
+
padding: 2px 6px;
|
|
3873
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
3874
|
+
font-size: 11px;
|
|
3875
|
+
font-weight: 600;
|
|
3876
|
+
`;
|
|
3877
|
+
const TimestampText = styled.span`
|
|
3878
|
+
font-size: 11px;
|
|
3879
|
+
color: #8e8ea9;
|
|
3880
|
+
`;
|
|
3881
|
+
const AgentText = styled.span`
|
|
3882
|
+
font-size: 11px;
|
|
3883
|
+
color: #b8b8c7;
|
|
3884
|
+
overflow: hidden;
|
|
3885
|
+
text-overflow: ellipsis;
|
|
3886
|
+
white-space: nowrap;
|
|
3887
|
+
display: block;
|
|
3888
|
+
`;
|
|
3555
3889
|
const STANDARD_FIELDS = /* @__PURE__ */ new Set([
|
|
3556
3890
|
"id",
|
|
3557
3891
|
"name",
|
|
@@ -3573,6 +3907,7 @@ function UserDetailDrawer({
|
|
|
3573
3907
|
}) {
|
|
3574
3908
|
const qc = useQueryClient();
|
|
3575
3909
|
const { toggleNotification } = useNotification();
|
|
3910
|
+
const { get, put } = useFetchClient();
|
|
3576
3911
|
const schemaQuery = useModelSchema("user");
|
|
3577
3912
|
const userQuery = useQuery({
|
|
3578
3913
|
queryKey: ["dash-user", userId],
|
|
@@ -3594,6 +3929,27 @@ function UserDetailDrawer({
|
|
|
3594
3929
|
return result.data?.organizations ?? [];
|
|
3595
3930
|
}
|
|
3596
3931
|
});
|
|
3932
|
+
const sessionsQuery = useQuery({
|
|
3933
|
+
queryKey: ["dash-user-sessions", userId],
|
|
3934
|
+
queryFn: async () => {
|
|
3935
|
+
const { data } = await get(
|
|
3936
|
+
`/better-auth-dashboard/db?uid=plugin::better-auth.session&filters[userId][$eq]=${userId}&sort[0]=createdAt:desc&pagination[pageSize]=50`
|
|
3937
|
+
);
|
|
3938
|
+
return data.results ?? [];
|
|
3939
|
+
}
|
|
3940
|
+
});
|
|
3941
|
+
const strapiUserQuery = useQuery({
|
|
3942
|
+
queryKey: ["dash-strapi-user", userId],
|
|
3943
|
+
enabled: !!schemaQuery.data,
|
|
3944
|
+
queryFn: async () => {
|
|
3945
|
+
const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
|
|
3946
|
+
const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
|
|
3947
|
+
const { data } = await get(
|
|
3948
|
+
`/better-auth-dashboard/db?uid=plugin::better-auth.user&filters[id][$eq]=${userId}&pagination[pageSize]=1${populateParam}`
|
|
3949
|
+
);
|
|
3950
|
+
return data.results?.[0] ?? null;
|
|
3951
|
+
}
|
|
3952
|
+
});
|
|
3597
3953
|
const [activeTab, setActiveTab] = useState("profile");
|
|
3598
3954
|
const [editName, setEditName] = useState(void 0);
|
|
3599
3955
|
const [editEmail, setEditEmail] = useState(void 0);
|
|
@@ -3604,6 +3960,7 @@ function UserDetailDrawer({
|
|
|
3604
3960
|
const [banReason, setBanReason] = useState("");
|
|
3605
3961
|
const [banExpiresDays, setBanExpiresDays] = useState("");
|
|
3606
3962
|
const [confirmRevokeAll, setConfirmRevokeAll] = useState(false);
|
|
3963
|
+
const [confirmRevokeSessionId, setConfirmRevokeSessionId] = useState(null);
|
|
3607
3964
|
const [confirmUnban, setConfirmUnban] = useState(false);
|
|
3608
3965
|
const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = useState(null);
|
|
3609
3966
|
const [confirmDisable2FA, setConfirmDisable2FA] = useState(false);
|
|
@@ -3622,28 +3979,43 @@ function UserDetailDrawer({
|
|
|
3622
3979
|
setEditExtra((prev) => ({ ...prev, [name]: value }));
|
|
3623
3980
|
};
|
|
3624
3981
|
const extraData = {
|
|
3625
|
-
...
|
|
3982
|
+
...strapiUserQuery.data ?? {},
|
|
3626
3983
|
...editExtra
|
|
3627
3984
|
};
|
|
3628
3985
|
const updateMutation = useMutation({
|
|
3629
3986
|
mutationFn: async () => {
|
|
3630
|
-
const
|
|
3631
|
-
if (editName !== void 0)
|
|
3632
|
-
if (editEmail !== void 0)
|
|
3987
|
+
const baBody = {};
|
|
3988
|
+
if (editName !== void 0) baBody.name = editName;
|
|
3989
|
+
if (editEmail !== void 0) baBody.email = editEmail;
|
|
3633
3990
|
if (editEmailVerified !== void 0)
|
|
3634
|
-
|
|
3635
|
-
if (editImage !== void 0)
|
|
3636
|
-
const
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3991
|
+
baBody.emailVerified = editEmailVerified;
|
|
3992
|
+
if (editImage !== void 0) baBody.image = editImage;
|
|
3993
|
+
const ops = [];
|
|
3994
|
+
if (Object.keys(baBody).length > 0) {
|
|
3995
|
+
ops.push(
|
|
3996
|
+
client.dash.updateUser(baBody, withContext({ userId })).then((result) => {
|
|
3997
|
+
if (result.error)
|
|
3998
|
+
throw new Error(result.error.message ?? "Update failed");
|
|
3999
|
+
})
|
|
4000
|
+
);
|
|
4001
|
+
}
|
|
4002
|
+
if (Object.keys(editExtra).length > 0) {
|
|
4003
|
+
const documentId = strapiUserQuery.data?.documentId;
|
|
4004
|
+
if (!documentId)
|
|
4005
|
+
throw new Error("Could not resolve documentId for user");
|
|
4006
|
+
ops.push(
|
|
4007
|
+
put(
|
|
4008
|
+
`/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
|
|
4009
|
+
editExtra
|
|
4010
|
+
)
|
|
4011
|
+
);
|
|
4012
|
+
}
|
|
4013
|
+
await Promise.all(ops);
|
|
3643
4014
|
},
|
|
3644
4015
|
onSuccess: () => {
|
|
3645
4016
|
qc.invalidateQueries({ queryKey: ["dash-user", userId] });
|
|
3646
4017
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
4018
|
+
qc.invalidateQueries({ queryKey: ["dash-strapi-user", userId] });
|
|
3647
4019
|
setEditName(void 0);
|
|
3648
4020
|
setEditEmail(void 0);
|
|
3649
4021
|
setEditEmailVerified(void 0);
|
|
@@ -3662,6 +4034,27 @@ function UserDetailDrawer({
|
|
|
3662
4034
|
});
|
|
3663
4035
|
}
|
|
3664
4036
|
});
|
|
4037
|
+
const revokeSessionMutation = useMutation({
|
|
4038
|
+
mutationFn: async (sessionId) => {
|
|
4039
|
+
const result = await client.dash.sessions.revoke(
|
|
4040
|
+
{},
|
|
4041
|
+
withContext({ sessionId })
|
|
4042
|
+
);
|
|
4043
|
+
if (result.error)
|
|
4044
|
+
throw new Error(result.error.message ?? "Revoke failed");
|
|
4045
|
+
},
|
|
4046
|
+
onSuccess: () => {
|
|
4047
|
+
setConfirmRevokeSessionId(null);
|
|
4048
|
+
qc.invalidateQueries({ queryKey: ["dash-user-sessions", userId] });
|
|
4049
|
+
toggleNotification({ type: "success", message: "Session revoked" });
|
|
4050
|
+
},
|
|
4051
|
+
onError: (err) => {
|
|
4052
|
+
toggleNotification({
|
|
4053
|
+
type: "danger",
|
|
4054
|
+
message: err.message ?? "Failed to revoke session"
|
|
4055
|
+
});
|
|
4056
|
+
}
|
|
4057
|
+
});
|
|
3665
4058
|
const passwordMutation = useMutation({
|
|
3666
4059
|
mutationFn: async () => {
|
|
3667
4060
|
const result = await client.dash.setPassword(
|
|
@@ -4424,25 +4817,55 @@ function UserDetailDrawer({
|
|
|
4424
4817
|
}
|
|
4425
4818
|
) })
|
|
4426
4819
|
] }) }) }),
|
|
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
|
-
|
|
4820
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
|
|
4821
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
4822
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Session management" }),
|
|
4823
|
+
/* @__PURE__ */ jsxs(AccountRow, { children: [
|
|
4824
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
|
|
4825
|
+
/* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
|
|
4826
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
|
|
4827
|
+
] }),
|
|
4828
|
+
/* @__PURE__ */ jsx(
|
|
4829
|
+
Button,
|
|
4830
|
+
{
|
|
4831
|
+
variant: "danger-light",
|
|
4832
|
+
size: "S",
|
|
4833
|
+
onClick: () => setConfirmRevokeAll(true),
|
|
4834
|
+
style: { flexShrink: 0 },
|
|
4835
|
+
children: "Revoke all"
|
|
4836
|
+
}
|
|
4837
|
+
)
|
|
4838
|
+
] })
|
|
4839
|
+
] }),
|
|
4840
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
4841
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Active sessions" }),
|
|
4842
|
+
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: 0, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxs(SessionCard, { children: [
|
|
4843
|
+
/* @__PURE__ */ jsxs(SessionMeta, { children: [
|
|
4844
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", style: { flexWrap: "wrap" }, children: [
|
|
4845
|
+
session.ipAddress && /* @__PURE__ */ jsx(IpChip, { children: session.ipAddress }),
|
|
4846
|
+
/* @__PURE__ */ jsxs(TimestampText, { children: [
|
|
4847
|
+
"Created",
|
|
4848
|
+
" ",
|
|
4849
|
+
new Date(session.createdAt).toLocaleString(),
|
|
4850
|
+
" · Expires",
|
|
4851
|
+
" ",
|
|
4852
|
+
new Date(session.expiresAt).toLocaleString()
|
|
4853
|
+
] })
|
|
4854
|
+
] }),
|
|
4855
|
+
session.userAgent && /* @__PURE__ */ jsx(AgentText, { children: session.userAgent })
|
|
4856
|
+
] }),
|
|
4857
|
+
/* @__PURE__ */ jsx(
|
|
4858
|
+
IconButton,
|
|
4859
|
+
{
|
|
4860
|
+
label: "Revoke session",
|
|
4861
|
+
onClick: () => setConfirmRevokeSessionId(String(session.id)),
|
|
4862
|
+
style: { flexShrink: 0 },
|
|
4863
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
4864
|
+
}
|
|
4865
|
+
)
|
|
4866
|
+
] }, session.documentId)) })
|
|
4444
4867
|
] })
|
|
4445
|
-
] }) })
|
|
4868
|
+
] }) }),
|
|
4446
4869
|
/* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxs(FormSection, { children: [
|
|
4447
4870
|
/* @__PURE__ */ jsx(SectionLabel, { children: "Memberships" }),
|
|
4448
4871
|
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 +4902,17 @@ function UserDetailDrawer({
|
|
|
4479
4902
|
onCancel: () => setConfirmRevokeAll(false)
|
|
4480
4903
|
}
|
|
4481
4904
|
),
|
|
4905
|
+
confirmRevokeSessionId && /* @__PURE__ */ jsx(
|
|
4906
|
+
ConfirmDialog,
|
|
4907
|
+
{
|
|
4908
|
+
title: "Revoke session",
|
|
4909
|
+
message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
|
|
4910
|
+
confirmLabel: "Revoke",
|
|
4911
|
+
loading: revokeSessionMutation.isLoading,
|
|
4912
|
+
onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
|
|
4913
|
+
onCancel: () => setConfirmRevokeSessionId(null)
|
|
4914
|
+
}
|
|
4915
|
+
),
|
|
4482
4916
|
confirmUnban && /* @__PURE__ */ jsx(
|
|
4483
4917
|
ConfirmDialog,
|
|
4484
4918
|
{
|
|
@@ -5013,8 +5447,7 @@ const Accent = styled.div`
|
|
|
5013
5447
|
const BrandIcon = styled.div`
|
|
5014
5448
|
width: 30px;
|
|
5015
5449
|
height: 30px;
|
|
5016
|
-
|
|
5017
|
-
background: linear-gradient(135deg, #4945ff 0%, #7b79ff 100%);
|
|
5450
|
+
background: black;
|
|
5018
5451
|
display: flex;
|
|
5019
5452
|
align-items: center;
|
|
5020
5453
|
justify-content: center;
|
|
@@ -5090,7 +5523,7 @@ function App() {
|
|
|
5090
5523
|
paddingBottom: 4,
|
|
5091
5524
|
children: [
|
|
5092
5525
|
/* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
|
|
5093
|
-
/* @__PURE__ */ jsx(BrandIcon, { children:
|
|
5526
|
+
/* @__PURE__ */ jsx(BrandIcon, { children: /* @__PURE__ */ jsx(PluginIcon, {}) }),
|
|
5094
5527
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
5095
5528
|
/* @__PURE__ */ jsx(Typography, { variant: "beta", textColor: "neutral800", children: "Better Auth" }),
|
|
5096
5529
|
/* @__PURE__ */ jsx(Box, { paddingTop: "2px", children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Authentication Dashboard" }) })
|
|
@@ -5110,8 +5543,7 @@ function App() {
|
|
|
5110
5543
|
"data-testid": "nav-organizations",
|
|
5111
5544
|
children: "Organizations"
|
|
5112
5545
|
}
|
|
5113
|
-
)
|
|
5114
|
-
/* @__PURE__ */ jsx(Tabs.Trigger, { value: "sessions", "data-testid": "nav-sessions", children: "Sessions" })
|
|
5546
|
+
)
|
|
5115
5547
|
] })
|
|
5116
5548
|
]
|
|
5117
5549
|
}
|
|
@@ -5121,8 +5553,7 @@ function App() {
|
|
|
5121
5553
|
),
|
|
5122
5554
|
/* @__PURE__ */ jsx(Tabs.Content, { value: "overview", "data-testid": "tab-overview", children: /* @__PURE__ */ jsx(OverviewPage, {}) }),
|
|
5123
5555
|
/* @__PURE__ */ jsx(Tabs.Content, { value: "users", "data-testid": "tab-users", children: /* @__PURE__ */ jsx(UsersPage, { config }) }),
|
|
5124
|
-
orgEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsx(OrganizationsPage, { teamsEnabled }) })
|
|
5125
|
-
/* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", "data-testid": "tab-sessions", children: /* @__PURE__ */ jsx(SessionsPage, {}) })
|
|
5556
|
+
orgEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsx(OrganizationsPage, { teamsEnabled }) })
|
|
5126
5557
|
] }) });
|
|
5127
5558
|
}
|
|
5128
5559
|
function Root() {
|