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