@strapi-community/plugin-better-auth-dashboard 1.0.0-alpha.1 → 1.0.0-alpha.4
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-7EredGQZ.js} +1416 -877
- package/dist/admin/{Root-hwPhIfaT.mjs → Root-B1LuKMEv.mjs} +1419 -880
- package/dist/admin/index-CIxfFlzU.js +66 -0
- package/dist/admin/index-QVlTR8eL.mjs +67 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +105 -7
- package/dist/server/index.mjs +105 -7
- 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-CIxfFlzU.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;
|
|
@@ -896,6 +1042,7 @@ function OrganizationDetail({
|
|
|
896
1042
|
const schemaQuery = useModelSchema("organization");
|
|
897
1043
|
const qc = reactQuery.useQueryClient();
|
|
898
1044
|
const { toggleNotification } = admin.useNotification();
|
|
1045
|
+
const { get, put } = admin.useFetchClient();
|
|
899
1046
|
const orgQuery = reactQuery.useQuery({
|
|
900
1047
|
queryKey: ["dash-org", organizationId],
|
|
901
1048
|
queryFn: async () => {
|
|
@@ -934,6 +1081,24 @@ function OrganizationDetail({
|
|
|
934
1081
|
return result.data ?? [];
|
|
935
1082
|
}
|
|
936
1083
|
});
|
|
1084
|
+
const invitationsQuery = reactQuery.useQuery({
|
|
1085
|
+
queryKey: ["dash-org-invitations", organizationId],
|
|
1086
|
+
queryFn: async () => {
|
|
1087
|
+
const result = await client.dash.organization[organizationId].invitations({}, withContext({ organizationId }));
|
|
1088
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1089
|
+
return result.data ?? [];
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
const strapiOrgQuery = reactQuery.useQuery({
|
|
1093
|
+
queryKey: ["dash-strapi-org", organizationId],
|
|
1094
|
+
enabled: !!orgQuery.data,
|
|
1095
|
+
queryFn: async () => {
|
|
1096
|
+
const { data } = await get(
|
|
1097
|
+
`/better-auth-dashboard/db?uid=plugin::better-auth.organization&filters[id][$eq]=${organizationId}&pagination[pageSize]=1`
|
|
1098
|
+
);
|
|
1099
|
+
return data.results?.[0] ?? null;
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
937
1102
|
const [activeTab, setActiveTab] = react.useState("details");
|
|
938
1103
|
const [editName, setEditName] = react.useState(void 0);
|
|
939
1104
|
const [editSlug, setEditSlug] = react.useState(void 0);
|
|
@@ -946,6 +1111,9 @@ function OrganizationDetail({
|
|
|
946
1111
|
const [confirmDeleteSsoId, setConfirmDeleteSsoId] = react.useState(
|
|
947
1112
|
null
|
|
948
1113
|
);
|
|
1114
|
+
const [inviteEmail, setInviteEmail] = react.useState("");
|
|
1115
|
+
const [inviteRole, setInviteRole] = react.useState("member");
|
|
1116
|
+
const [confirmCancelInvitationId, setConfirmCancelInvitationId] = react.useState(null);
|
|
949
1117
|
const handleExtraChange = (name, value) => {
|
|
950
1118
|
setEditExtra((prev) => ({ ...prev, [name]: value }));
|
|
951
1119
|
};
|
|
@@ -960,13 +1128,13 @@ function OrganizationDetail({
|
|
|
960
1128
|
if (editName !== void 0) body.name = editName;
|
|
961
1129
|
if (editSlug !== void 0) body.slug = editSlug;
|
|
962
1130
|
if (editLogo !== void 0) body.logo = editLogo;
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
1131
|
+
const documentId = strapiOrgQuery.data?.documentId;
|
|
1132
|
+
if (!documentId)
|
|
1133
|
+
throw new Error("Could not resolve documentId for organization");
|
|
1134
|
+
await put(
|
|
1135
|
+
`/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.organization`,
|
|
1136
|
+
body
|
|
966
1137
|
);
|
|
967
|
-
if (result.error)
|
|
968
|
-
throw new Error(result.error.message ?? "Update failed");
|
|
969
|
-
return result.data;
|
|
970
1138
|
},
|
|
971
1139
|
onSuccess: () => {
|
|
972
1140
|
qc.invalidateQueries({ queryKey: ["dash-org", organizationId] });
|
|
@@ -1116,9 +1284,74 @@ function OrganizationDetail({
|
|
|
1116
1284
|
});
|
|
1117
1285
|
}
|
|
1118
1286
|
});
|
|
1287
|
+
const inviteMemberMutation = reactQuery.useMutation({
|
|
1288
|
+
mutationFn: async () => {
|
|
1289
|
+
const result = await client.dash.organization.inviteMember(
|
|
1290
|
+
{ email: inviteEmail, role: inviteRole, invitedBy: "" },
|
|
1291
|
+
withContext({ organizationId })
|
|
1292
|
+
);
|
|
1293
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1294
|
+
return result.data;
|
|
1295
|
+
},
|
|
1296
|
+
onSuccess: () => {
|
|
1297
|
+
qc.invalidateQueries({
|
|
1298
|
+
queryKey: ["dash-org-invitations", organizationId]
|
|
1299
|
+
});
|
|
1300
|
+
setInviteEmail("");
|
|
1301
|
+
setInviteRole("member");
|
|
1302
|
+
toggleNotification({ type: "success", message: "Invitation sent" });
|
|
1303
|
+
},
|
|
1304
|
+
onError: (err) => {
|
|
1305
|
+
toggleNotification({
|
|
1306
|
+
type: "danger",
|
|
1307
|
+
message: err.message ?? "Failed to invite"
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
const cancelInvitationMutation = reactQuery.useMutation({
|
|
1312
|
+
mutationFn: async (invitationId) => {
|
|
1313
|
+
const result = await client.dash.organization.cancelInvitation(
|
|
1314
|
+
{ invitationId },
|
|
1315
|
+
withContext({ organizationId })
|
|
1316
|
+
);
|
|
1317
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1318
|
+
},
|
|
1319
|
+
onSuccess: () => {
|
|
1320
|
+
setConfirmCancelInvitationId(null);
|
|
1321
|
+
qc.invalidateQueries({
|
|
1322
|
+
queryKey: ["dash-org-invitations", organizationId]
|
|
1323
|
+
});
|
|
1324
|
+
toggleNotification({ type: "success", message: "Invitation cancelled" });
|
|
1325
|
+
},
|
|
1326
|
+
onError: (err) => {
|
|
1327
|
+
toggleNotification({
|
|
1328
|
+
type: "danger",
|
|
1329
|
+
message: err.message ?? "Failed to cancel"
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
const resendInvitationMutation = reactQuery.useMutation({
|
|
1334
|
+
mutationFn: async (invitationId) => {
|
|
1335
|
+
const result = await client.dash.organization.resendInvitation(
|
|
1336
|
+
{ invitationId },
|
|
1337
|
+
withContext({ organizationId })
|
|
1338
|
+
);
|
|
1339
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1340
|
+
},
|
|
1341
|
+
onSuccess: () => {
|
|
1342
|
+
toggleNotification({ type: "success", message: "Invitation resent" });
|
|
1343
|
+
},
|
|
1344
|
+
onError: (err) => {
|
|
1345
|
+
toggleNotification({
|
|
1346
|
+
type: "danger",
|
|
1347
|
+
message: err.message ?? "Failed to resend"
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1119
1351
|
const members = membersQuery.data ?? [];
|
|
1120
1352
|
const teams = teamsQuery.data ?? [];
|
|
1121
1353
|
const ssoProviders = ssoQuery.data ?? [];
|
|
1354
|
+
const invitations = invitationsQuery.data ?? [];
|
|
1122
1355
|
const hasOrgEdits = editName !== void 0 || editSlug !== void 0 || editLogo !== void 0 || Object.keys(editExtra).length > 0;
|
|
1123
1356
|
const customFields = Object.entries(schemaQuery.data ?? {}).filter(([name]) => !STANDARD_ORG_FIELDS.has(name)).map(([name, attribute]) => ({ name, attribute }));
|
|
1124
1357
|
const detailsFooter = activeTab === "details" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
@@ -1175,6 +1408,11 @@ function OrganizationDetail({
|
|
|
1175
1408
|
teams.length,
|
|
1176
1409
|
")"
|
|
1177
1410
|
] }),
|
|
1411
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs.Trigger, { value: "invitations", children: [
|
|
1412
|
+
"Invitations (",
|
|
1413
|
+
invitations.length,
|
|
1414
|
+
")"
|
|
1415
|
+
] }),
|
|
1178
1416
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs.Trigger, { value: "sso", children: [
|
|
1179
1417
|
"SSO (",
|
|
1180
1418
|
ssoProviders.length,
|
|
@@ -1430,7 +1668,127 @@ function OrganizationDetail({
|
|
|
1430
1668
|
},
|
|
1431
1669
|
provider.id
|
|
1432
1670
|
)) })
|
|
1433
|
-
] }) }) })
|
|
1671
|
+
] }) }) }),
|
|
1672
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "invitations", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
|
|
1673
|
+
/* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
|
|
1674
|
+
/* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Invite member by email" }),
|
|
1675
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 3, children: [
|
|
1676
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 8, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
|
|
1677
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Email address" }),
|
|
1678
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1679
|
+
designSystem.TextInput,
|
|
1680
|
+
{
|
|
1681
|
+
type: "email",
|
|
1682
|
+
value: inviteEmail,
|
|
1683
|
+
onChange: (e) => setInviteEmail(e.target.value),
|
|
1684
|
+
placeholder: "user@example.com"
|
|
1685
|
+
}
|
|
1686
|
+
)
|
|
1687
|
+
] }) }),
|
|
1688
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
|
|
1689
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Role" }),
|
|
1690
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1691
|
+
designSystem.SingleSelect,
|
|
1692
|
+
{
|
|
1693
|
+
value: inviteRole,
|
|
1694
|
+
onChange: (v) => setInviteRole(String(v)),
|
|
1695
|
+
"aria-label": "Invite role",
|
|
1696
|
+
children: [
|
|
1697
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "member", children: "Member" }),
|
|
1698
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "admin", children: "Admin" }),
|
|
1699
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "owner", children: "Owner" })
|
|
1700
|
+
]
|
|
1701
|
+
}
|
|
1702
|
+
)
|
|
1703
|
+
] }) })
|
|
1704
|
+
] }),
|
|
1705
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1706
|
+
designSystem.Button,
|
|
1707
|
+
{
|
|
1708
|
+
size: "S",
|
|
1709
|
+
startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
|
|
1710
|
+
disabled: !inviteEmail,
|
|
1711
|
+
loading: inviteMemberMutation.isLoading,
|
|
1712
|
+
onClick: () => inviteMemberMutation.mutate(),
|
|
1713
|
+
children: "Send invitation"
|
|
1714
|
+
}
|
|
1715
|
+
) })
|
|
1716
|
+
] }),
|
|
1717
|
+
/* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
|
|
1718
|
+
/* @__PURE__ */ jsxRuntime.jsxs(SectionLabel, { children: [
|
|
1719
|
+
"Invitations (",
|
|
1720
|
+
invitations.length,
|
|
1721
|
+
")"
|
|
1722
|
+
] }),
|
|
1723
|
+
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) => {
|
|
1724
|
+
const statusColor = {
|
|
1725
|
+
pending: "#f59e0b",
|
|
1726
|
+
accepted: "#5cb176",
|
|
1727
|
+
rejected: "#d02b20",
|
|
1728
|
+
canceled: "#8e8ea9"
|
|
1729
|
+
};
|
|
1730
|
+
const color = statusColor[inv.status] ?? "#8e8ea9";
|
|
1731
|
+
const isPending = inv.status === "pending";
|
|
1732
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(AccountRow, { children: [
|
|
1733
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1734
|
+
designSystem.Flex,
|
|
1735
|
+
{
|
|
1736
|
+
direction: "column",
|
|
1737
|
+
gap: 1,
|
|
1738
|
+
alignItems: "flex-start",
|
|
1739
|
+
style: { flex: 1 },
|
|
1740
|
+
children: [
|
|
1741
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: inv.user?.name ?? inv.email }),
|
|
1742
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: [
|
|
1743
|
+
inv.email,
|
|
1744
|
+
" · role: ",
|
|
1745
|
+
inv.role
|
|
1746
|
+
] }),
|
|
1747
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: [
|
|
1748
|
+
"Expires",
|
|
1749
|
+
" ",
|
|
1750
|
+
new Date(inv.expiresAt).toLocaleDateString()
|
|
1751
|
+
] })
|
|
1752
|
+
]
|
|
1753
|
+
}
|
|
1754
|
+
),
|
|
1755
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
1756
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1757
|
+
MonoChip,
|
|
1758
|
+
{
|
|
1759
|
+
style: {
|
|
1760
|
+
color,
|
|
1761
|
+
borderColor: color,
|
|
1762
|
+
background: `${color}18`
|
|
1763
|
+
},
|
|
1764
|
+
children: inv.status
|
|
1765
|
+
}
|
|
1766
|
+
),
|
|
1767
|
+
isPending && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1768
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1769
|
+
designSystem.Button,
|
|
1770
|
+
{
|
|
1771
|
+
size: "S",
|
|
1772
|
+
variant: "secondary",
|
|
1773
|
+
loading: resendInvitationMutation.isLoading,
|
|
1774
|
+
onClick: () => resendInvitationMutation.mutate(inv.id),
|
|
1775
|
+
children: "Resend"
|
|
1776
|
+
}
|
|
1777
|
+
),
|
|
1778
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1779
|
+
designSystem.IconButton,
|
|
1780
|
+
{
|
|
1781
|
+
label: "Cancel invitation",
|
|
1782
|
+
onClick: () => setConfirmCancelInvitationId(inv.id),
|
|
1783
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
|
|
1784
|
+
}
|
|
1785
|
+
)
|
|
1786
|
+
] })
|
|
1787
|
+
] })
|
|
1788
|
+
] }, inv.id);
|
|
1789
|
+
}) })
|
|
1790
|
+
] })
|
|
1791
|
+
] }) })
|
|
1434
1792
|
] }),
|
|
1435
1793
|
confirmRemoveMemberId && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1436
1794
|
ConfirmDialog,
|
|
@@ -1464,6 +1822,17 @@ function OrganizationDetail({
|
|
|
1464
1822
|
onConfirm: () => deleteSsoMutation.mutate(confirmDeleteSsoId),
|
|
1465
1823
|
onCancel: () => setConfirmDeleteSsoId(null)
|
|
1466
1824
|
}
|
|
1825
|
+
),
|
|
1826
|
+
confirmCancelInvitationId && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1827
|
+
ConfirmDialog,
|
|
1828
|
+
{
|
|
1829
|
+
title: "Cancel invitation",
|
|
1830
|
+
message: "Are you sure you want to cancel this invitation? The invite link will no longer work.",
|
|
1831
|
+
confirmLabel: "Cancel invitation",
|
|
1832
|
+
loading: cancelInvitationMutation.isLoading,
|
|
1833
|
+
onConfirm: () => cancelInvitationMutation.mutate(confirmCancelInvitationId),
|
|
1834
|
+
onCancel: () => setConfirmCancelInvitationId(null)
|
|
1835
|
+
}
|
|
1467
1836
|
)
|
|
1468
1837
|
]
|
|
1469
1838
|
}
|
|
@@ -1609,12 +1978,12 @@ function TeamRow({
|
|
|
1609
1978
|
)
|
|
1610
1979
|
] });
|
|
1611
1980
|
}
|
|
1612
|
-
const PAGE_SIZE$
|
|
1613
|
-
const fadeUp$
|
|
1981
|
+
const PAGE_SIZE$1 = 25;
|
|
1982
|
+
const fadeUp$2 = styled.keyframes`
|
|
1614
1983
|
from { opacity: 0; transform: translateY(6px); }
|
|
1615
1984
|
to { opacity: 1; transform: translateY(0); }
|
|
1616
1985
|
`;
|
|
1617
|
-
const Wrap$
|
|
1986
|
+
const Wrap$2 = styled__default.default.div`
|
|
1618
1987
|
padding: 28px 32px;
|
|
1619
1988
|
background: #f6f6f9;
|
|
1620
1989
|
min-height: 100%;
|
|
@@ -1622,39 +1991,39 @@ const Wrap$3 = styled__default.default.div`
|
|
|
1622
1991
|
flex-direction: column;
|
|
1623
1992
|
gap: 20px;
|
|
1624
1993
|
`;
|
|
1625
|
-
const PageHeader$
|
|
1994
|
+
const PageHeader$1 = styled__default.default.div`
|
|
1626
1995
|
display: flex;
|
|
1627
1996
|
justify-content: space-between;
|
|
1628
1997
|
align-items: flex-start;
|
|
1629
1998
|
`;
|
|
1630
|
-
const TitleBlock$
|
|
1999
|
+
const TitleBlock$1 = styled__default.default.div`
|
|
1631
2000
|
display: flex;
|
|
1632
2001
|
flex-direction: column;
|
|
1633
2002
|
gap: 4px;
|
|
1634
2003
|
`;
|
|
1635
|
-
const PageTitle$
|
|
2004
|
+
const PageTitle$1 = styled__default.default.h1`
|
|
1636
2005
|
margin: 0;
|
|
1637
2006
|
font-size: 22px;
|
|
1638
2007
|
font-weight: 800;
|
|
1639
2008
|
color: #32324d;
|
|
1640
2009
|
letter-spacing: -0.03em;
|
|
1641
2010
|
`;
|
|
1642
|
-
const PageSubtitle$
|
|
2011
|
+
const PageSubtitle$1 = styled__default.default.p`
|
|
1643
2012
|
margin: 0;
|
|
1644
2013
|
font-size: 12px;
|
|
1645
2014
|
color: #8e8ea9;
|
|
1646
2015
|
`;
|
|
1647
|
-
const TableCard$
|
|
2016
|
+
const TableCard$1 = styled__default.default.div`
|
|
1648
2017
|
background: #ffffff;
|
|
1649
2018
|
border: 1px solid #eaeaef;
|
|
1650
2019
|
border-radius: 10px;
|
|
1651
2020
|
overflow: hidden;
|
|
1652
2021
|
`;
|
|
1653
|
-
const Table$
|
|
2022
|
+
const Table$1 = styled__default.default.table`
|
|
1654
2023
|
width: 100%;
|
|
1655
2024
|
border-collapse: collapse;
|
|
1656
2025
|
`;
|
|
1657
|
-
const TH$
|
|
2026
|
+
const TH$1 = styled__default.default.th`
|
|
1658
2027
|
text-align: left;
|
|
1659
2028
|
padding: 10px 14px;
|
|
1660
2029
|
font-size: 10px;
|
|
@@ -1668,13 +2037,13 @@ const TH$2 = styled__default.default.th`
|
|
|
1668
2037
|
&:first-child { padding-left: 20px; }
|
|
1669
2038
|
&:last-child { padding-right: 20px; }
|
|
1670
2039
|
`;
|
|
1671
|
-
const THCheck$1 = styled__default.default(TH$
|
|
2040
|
+
const THCheck$1 = styled__default.default(TH$1)`
|
|
1672
2041
|
width: 44px;
|
|
1673
2042
|
`;
|
|
1674
|
-
const THActions$1 = styled__default.default(TH$
|
|
2043
|
+
const THActions$1 = styled__default.default(TH$1)`
|
|
1675
2044
|
width: 80px;
|
|
1676
2045
|
`;
|
|
1677
|
-
const TD$
|
|
2046
|
+
const TD$1 = styled__default.default.td`
|
|
1678
2047
|
padding: 11px 14px;
|
|
1679
2048
|
font-size: 12px;
|
|
1680
2049
|
color: #32324d;
|
|
@@ -1683,14 +2052,14 @@ const TD$2 = styled__default.default.td`
|
|
|
1683
2052
|
&:first-child { padding-left: 20px; }
|
|
1684
2053
|
&:last-child { padding-right: 20px; }
|
|
1685
2054
|
`;
|
|
1686
|
-
const TDCheck$1 = styled__default.default(TD$
|
|
2055
|
+
const TDCheck$1 = styled__default.default(TD$1)`
|
|
1687
2056
|
width: 44px;
|
|
1688
2057
|
`;
|
|
1689
|
-
const TDActions$1 = styled__default.default(TD$
|
|
2058
|
+
const TDActions$1 = styled__default.default(TD$1)`
|
|
1690
2059
|
width: 80px;
|
|
1691
2060
|
`;
|
|
1692
|
-
const TR$
|
|
1693
|
-
animation: ${fadeUp$
|
|
2061
|
+
const TR$1 = styled__default.default.tr`
|
|
2062
|
+
animation: ${fadeUp$2} 280ms ease both;
|
|
1694
2063
|
animation-delay: ${(p) => (p.$i ?? 0) * 25}ms;
|
|
1695
2064
|
background: ${(p) => p.$selected ? "#f0f0ff" : "transparent"};
|
|
1696
2065
|
transition: background 120ms ease;
|
|
@@ -1778,6 +2147,7 @@ function OrgAvatar({ name, logo }) {
|
|
|
1778
2147
|
}
|
|
1779
2148
|
function OrganizationsPage({ teamsEnabled }) {
|
|
1780
2149
|
const qc = reactQuery.useQueryClient();
|
|
2150
|
+
const { toggleNotification } = admin.useNotification();
|
|
1781
2151
|
const [page, setPage] = react.useState(1);
|
|
1782
2152
|
const [search, setSearch] = react.useState("");
|
|
1783
2153
|
const [searchInput, setSearchInput] = react.useState("");
|
|
@@ -1786,13 +2156,13 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1786
2156
|
const [detailOrgId, setDetailOrgId] = react.useState(null);
|
|
1787
2157
|
const [confirmDelete, setConfirmDelete] = react.useState(null);
|
|
1788
2158
|
const [confirmDeleteMany, setConfirmDeleteMany] = react.useState(false);
|
|
1789
|
-
const offset = (page - 1) * PAGE_SIZE$
|
|
2159
|
+
const offset = (page - 1) * PAGE_SIZE$1;
|
|
1790
2160
|
const orgsQuery = reactQuery.useQuery({
|
|
1791
2161
|
queryKey: ["dash-organizations", page, search],
|
|
1792
2162
|
queryFn: async () => {
|
|
1793
2163
|
const result = await client.dash.listOrganizations({
|
|
1794
2164
|
query: {
|
|
1795
|
-
limit: PAGE_SIZE$
|
|
2165
|
+
limit: PAGE_SIZE$1,
|
|
1796
2166
|
offset,
|
|
1797
2167
|
sortBy: "createdAt",
|
|
1798
2168
|
sortOrder: "desc",
|
|
@@ -1817,6 +2187,13 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1817
2187
|
onSuccess: () => {
|
|
1818
2188
|
setConfirmDelete(null);
|
|
1819
2189
|
qc.invalidateQueries({ queryKey: ["dash-organizations"] });
|
|
2190
|
+
toggleNotification({ type: "success", message: "Organization deleted" });
|
|
2191
|
+
},
|
|
2192
|
+
onError: (err) => {
|
|
2193
|
+
toggleNotification({
|
|
2194
|
+
type: "danger",
|
|
2195
|
+
message: err.message ?? "Failed to delete organization"
|
|
2196
|
+
});
|
|
1820
2197
|
}
|
|
1821
2198
|
});
|
|
1822
2199
|
const deleteManyMutation = reactQuery.useMutation({
|
|
@@ -1829,15 +2206,25 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1829
2206
|
throw new Error(result.error.message ?? "Delete failed");
|
|
1830
2207
|
return result.data;
|
|
1831
2208
|
},
|
|
1832
|
-
onSuccess: () => {
|
|
2209
|
+
onSuccess: (_data, organizationIds) => {
|
|
1833
2210
|
setConfirmDeleteMany(false);
|
|
1834
2211
|
setSelected(/* @__PURE__ */ new Set());
|
|
1835
2212
|
qc.invalidateQueries({ queryKey: ["dash-organizations"] });
|
|
2213
|
+
toggleNotification({
|
|
2214
|
+
type: "success",
|
|
2215
|
+
message: `${organizationIds.length} organization${organizationIds.length !== 1 ? "s" : ""} deleted`
|
|
2216
|
+
});
|
|
2217
|
+
},
|
|
2218
|
+
onError: (err) => {
|
|
2219
|
+
toggleNotification({
|
|
2220
|
+
type: "danger",
|
|
2221
|
+
message: err.message ?? "Failed to delete organizations"
|
|
2222
|
+
});
|
|
1836
2223
|
}
|
|
1837
2224
|
});
|
|
1838
2225
|
const orgs = orgsQuery.data && "organizations" in orgsQuery.data ? orgsQuery.data.organizations : [];
|
|
1839
2226
|
const total = orgsQuery.data && "total" in orgsQuery.data ? orgsQuery.data.total : 0;
|
|
1840
|
-
const pageCount = Math.ceil(total / PAGE_SIZE$
|
|
2227
|
+
const pageCount = Math.ceil(total / PAGE_SIZE$1);
|
|
1841
2228
|
const allSelected = orgs.length > 0 && orgs.every((o) => selected.has(o.id));
|
|
1842
2229
|
const someSelected = selected.size > 0;
|
|
1843
2230
|
const toggleSelect = (id) => {
|
|
@@ -1857,11 +2244,11 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1857
2244
|
setSearch(searchInput);
|
|
1858
2245
|
setPage(1);
|
|
1859
2246
|
};
|
|
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$
|
|
2247
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$2, { "data-testid": "organizations-page", children: [
|
|
2248
|
+
/* @__PURE__ */ jsxRuntime.jsxs(PageHeader$1, { children: [
|
|
2249
|
+
/* @__PURE__ */ jsxRuntime.jsxs(TitleBlock$1, { children: [
|
|
2250
|
+
/* @__PURE__ */ jsxRuntime.jsx(PageTitle$1, { children: "Organizations" }),
|
|
2251
|
+
/* @__PURE__ */ jsxRuntime.jsxs(PageSubtitle$1, { children: [
|
|
1865
2252
|
total.toLocaleString(),
|
|
1866
2253
|
" total"
|
|
1867
2254
|
] })
|
|
@@ -1909,7 +2296,7 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1909
2296
|
)
|
|
1910
2297
|
] }),
|
|
1911
2298
|
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$
|
|
2299
|
+
/* @__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
2300
|
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
1914
2301
|
/* @__PURE__ */ jsxRuntime.jsx(THCheck$1, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1915
2302
|
designSystem.Checkbox,
|
|
@@ -1919,14 +2306,14 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1919
2306
|
"aria-label": "Select all"
|
|
1920
2307
|
}
|
|
1921
2308
|
) }),
|
|
1922
|
-
/* @__PURE__ */ jsxRuntime.jsx(TH$
|
|
1923
|
-
/* @__PURE__ */ jsxRuntime.jsx(TH$
|
|
1924
|
-
/* @__PURE__ */ jsxRuntime.jsx(TH$
|
|
1925
|
-
/* @__PURE__ */ jsxRuntime.jsx(TH$
|
|
2309
|
+
/* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Name" }),
|
|
2310
|
+
/* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Slug" }),
|
|
2311
|
+
/* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Members" }),
|
|
2312
|
+
/* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Created" }),
|
|
1926
2313
|
/* @__PURE__ */ jsxRuntime.jsx(THActions$1, {})
|
|
1927
2314
|
] }) }),
|
|
1928
2315
|
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1929
|
-
TD$
|
|
2316
|
+
TD$1,
|
|
1930
2317
|
{
|
|
1931
2318
|
colSpan: 6,
|
|
1932
2319
|
style: {
|
|
@@ -1938,7 +2325,7 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1938
2325
|
children: search ? `No organizations matching "${search}"` : "No organizations found"
|
|
1939
2326
|
}
|
|
1940
2327
|
) }) : orgs.map((org, i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1941
|
-
TR$
|
|
2328
|
+
TR$1,
|
|
1942
2329
|
{
|
|
1943
2330
|
$selected: selected.has(org.id),
|
|
1944
2331
|
$i: i,
|
|
@@ -1952,13 +2339,13 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1952
2339
|
"aria-label": `Select ${org.name}`
|
|
1953
2340
|
}
|
|
1954
2341
|
) }),
|
|
1955
|
-
/* @__PURE__ */ jsxRuntime.jsx(TD$
|
|
2342
|
+
/* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
|
|
1956
2343
|
/* @__PURE__ */ jsxRuntime.jsx(OrgAvatar, { name: org.name, logo: org.logo }),
|
|
1957
2344
|
/* @__PURE__ */ jsxRuntime.jsx(OrgName, { children: org.name })
|
|
1958
2345
|
] }) }),
|
|
1959
|
-
/* @__PURE__ */ jsxRuntime.jsx(TD$
|
|
1960
|
-
/* @__PURE__ */ jsxRuntime.jsx(TD$
|
|
1961
|
-
/* @__PURE__ */ jsxRuntime.jsx(TD$
|
|
2346
|
+
/* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(SlugChip, { children: org.slug }) }),
|
|
2347
|
+
/* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(CountChip, { children: org.memberCount }) }),
|
|
2348
|
+
/* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
|
|
1962
2349
|
/* @__PURE__ */ jsxRuntime.jsx(TDActions$1, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, justifyContent: "flex-end", children: [
|
|
1963
2350
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1964
2351
|
designSystem.IconButton,
|
|
@@ -2105,95 +2492,143 @@ function Avatar({
|
|
|
2105
2492
|
const color = getColor(name || "?");
|
|
2106
2493
|
return /* @__PURE__ */ jsxRuntime.jsx(Circle, { $bg: color.bg, $fg: color.fg, $size: size, children: getInitials(name || "?") });
|
|
2107
2494
|
}
|
|
2108
|
-
const
|
|
2109
|
-
|
|
2495
|
+
const T = {
|
|
2496
|
+
bg: "#f6f6f9",
|
|
2497
|
+
bgCard: "#ffffff",
|
|
2498
|
+
border: "#eaeaef",
|
|
2499
|
+
borderHover: "rgba(73,69,255,0.35)",
|
|
2500
|
+
accent: "#4945ff",
|
|
2501
|
+
green: "#5cb176",
|
|
2502
|
+
greenDim: "rgba(92,177,118,0.12)",
|
|
2503
|
+
amber: "#d9822f",
|
|
2504
|
+
red: "#d02b20",
|
|
2505
|
+
redDim: "rgba(208,43,32,0.1)",
|
|
2506
|
+
purple: "#8460b8",
|
|
2507
|
+
textPrimary: "#32324d",
|
|
2508
|
+
textSecondary: "#666687",
|
|
2509
|
+
textMuted: "#b8b8c7",
|
|
2510
|
+
mono: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`
|
|
2511
|
+
};
|
|
2512
|
+
const fadeUp$1 = styled.keyframes`
|
|
2513
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
2110
2514
|
to { opacity: 1; transform: translateY(0); }
|
|
2111
2515
|
`;
|
|
2112
|
-
const Wrap$
|
|
2113
|
-
padding: 28px 32px;
|
|
2114
|
-
background:
|
|
2516
|
+
const Wrap$1 = styled__default.default.div`
|
|
2517
|
+
padding: 28px 32px 48px;
|
|
2518
|
+
background: ${T.bg};
|
|
2115
2519
|
min-height: 100%;
|
|
2116
2520
|
display: flex;
|
|
2117
2521
|
flex-direction: column;
|
|
2118
|
-
gap:
|
|
2522
|
+
gap: 22px;
|
|
2119
2523
|
`;
|
|
2120
|
-
const
|
|
2121
|
-
display:
|
|
2122
|
-
|
|
2123
|
-
gap:
|
|
2124
|
-
|
|
2125
|
-
@media (max-width: 768px) { grid-template-columns: repeat(2, 1fr); }
|
|
2524
|
+
const SectionDivider = styled__default.default.div`
|
|
2525
|
+
display: flex;
|
|
2526
|
+
align-items: center;
|
|
2527
|
+
gap: 10px;
|
|
2528
|
+
margin-bottom: -6px;
|
|
2126
2529
|
`;
|
|
2127
|
-
const
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2530
|
+
const DivLabel = styled__default.default.span`
|
|
2531
|
+
font-size: 9px;
|
|
2532
|
+
font-weight: 700;
|
|
2533
|
+
letter-spacing: 0.14em;
|
|
2534
|
+
text-transform: uppercase;
|
|
2535
|
+
color: ${T.textMuted};
|
|
2536
|
+
white-space: nowrap;
|
|
2132
2537
|
`;
|
|
2133
|
-
const
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2538
|
+
const DivLine = styled__default.default.div`
|
|
2539
|
+
flex: 1;
|
|
2540
|
+
height: 1px;
|
|
2541
|
+
background: ${T.border};
|
|
2137
2542
|
`;
|
|
2138
2543
|
const Card = styled__default.default.div`
|
|
2139
|
-
background:
|
|
2140
|
-
border: 1px solid
|
|
2141
|
-
border-radius:
|
|
2544
|
+
background: ${T.bgCard};
|
|
2545
|
+
border: 1px solid ${T.border};
|
|
2546
|
+
border-radius: 12px;
|
|
2142
2547
|
overflow: hidden;
|
|
2143
2548
|
position: relative;
|
|
2144
|
-
animation: ${fadeUp$
|
|
2549
|
+
animation: ${fadeUp$1} 380ms ease both;
|
|
2145
2550
|
animation-delay: ${(p) => (p.$delay ?? 0) * 55}ms;
|
|
2146
|
-
transition: border-color
|
|
2147
|
-
|
|
2148
|
-
${(p) => p.$accent && `
|
|
2149
|
-
&::before {
|
|
2150
|
-
content: '';
|
|
2151
|
-
position: absolute;
|
|
2152
|
-
top: 0; left: 0; right: 0;
|
|
2153
|
-
height: 3px;
|
|
2154
|
-
background: ${p.$accent};
|
|
2155
|
-
z-index: 1;
|
|
2156
|
-
}
|
|
2157
|
-
`}
|
|
2158
|
-
|
|
2551
|
+
transition: border-color 200ms, box-shadow 200ms;
|
|
2159
2552
|
&:hover {
|
|
2160
|
-
border-color:
|
|
2161
|
-
box-shadow: 0
|
|
2553
|
+
border-color: ${T.borderHover};
|
|
2554
|
+
box-shadow: 0 0 0 1px ${T.borderHover}, 0 8px 32px rgba(124,109,250,0.07);
|
|
2162
2555
|
}
|
|
2163
2556
|
`;
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2557
|
+
const PillGroup = styled__default.default.div`
|
|
2558
|
+
display: flex;
|
|
2559
|
+
background: rgba(0,0,0,0.03);
|
|
2560
|
+
border: 1px solid ${T.border};
|
|
2561
|
+
border-radius: 9px;
|
|
2562
|
+
padding: 3px;
|
|
2563
|
+
gap: 2px;
|
|
2166
2564
|
`;
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2565
|
+
const Pill = styled__default.default.button`
|
|
2566
|
+
appearance: none;
|
|
2567
|
+
border: none;
|
|
2568
|
+
cursor: pointer;
|
|
2569
|
+
padding: 5px 16px;
|
|
2570
|
+
border-radius: 6px;
|
|
2571
|
+
font-size: 11px;
|
|
2572
|
+
font-weight: 600;
|
|
2573
|
+
letter-spacing: 0.03em;
|
|
2574
|
+
transition: background 180ms, color 180ms;
|
|
2575
|
+
background: ${(p) => p.$active ? T.accent : "transparent"};
|
|
2576
|
+
color: ${(p) => p.$active ? "#fff" : T.textSecondary};
|
|
2577
|
+
&:hover {
|
|
2578
|
+
background: ${(p) => p.$active ? T.accent : "rgba(0,0,0,0.05)"};
|
|
2579
|
+
color: ${(p) => p.$active ? "#fff" : T.textPrimary};
|
|
2580
|
+
}
|
|
2581
|
+
`;
|
|
2582
|
+
const StatGrid = styled__default.default.div`
|
|
2583
|
+
display: grid;
|
|
2584
|
+
grid-template-columns: repeat(5, 1fr);
|
|
2585
|
+
gap: 10px;
|
|
2586
|
+
@media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
|
|
2587
|
+
`;
|
|
2588
|
+
const StatCard = styled__default.default(Card)`
|
|
2589
|
+
padding: 18px 20px 0;
|
|
2590
|
+
&::after {
|
|
2591
|
+
content: '';
|
|
2592
|
+
position: absolute;
|
|
2593
|
+
top: 0; left: 0; right: 0;
|
|
2594
|
+
height: 2px;
|
|
2595
|
+
background: ${(p) => p.$accent};
|
|
2596
|
+
opacity: 0.75;
|
|
2597
|
+
}
|
|
2598
|
+
`;
|
|
2599
|
+
const StatLabel = styled__default.default.div`
|
|
2600
|
+
font-size: 9px;
|
|
2601
|
+
font-weight: 700;
|
|
2602
|
+
letter-spacing: 0.1em;
|
|
2171
2603
|
text-transform: uppercase;
|
|
2172
|
-
color:
|
|
2173
|
-
margin-bottom:
|
|
2604
|
+
color: ${T.textMuted};
|
|
2605
|
+
margin-bottom: 10px;
|
|
2174
2606
|
`;
|
|
2175
2607
|
const StatValue = styled__default.default.div`
|
|
2176
2608
|
font-size: 30px;
|
|
2177
2609
|
font-weight: 800;
|
|
2178
|
-
color:
|
|
2610
|
+
color: ${T.textPrimary};
|
|
2179
2611
|
line-height: 1;
|
|
2180
|
-
letter-spacing: -0.
|
|
2612
|
+
letter-spacing: -0.045em;
|
|
2181
2613
|
font-variant-numeric: tabular-nums;
|
|
2182
2614
|
margin-bottom: 10px;
|
|
2183
2615
|
`;
|
|
2184
2616
|
const TrendBadge = styled__default.default.span`
|
|
2185
2617
|
display: inline-flex;
|
|
2186
2618
|
align-items: center;
|
|
2187
|
-
gap:
|
|
2188
|
-
padding: 2px
|
|
2619
|
+
gap: 3px;
|
|
2620
|
+
padding: 2px 8px;
|
|
2189
2621
|
border-radius: 20px;
|
|
2190
|
-
font-size:
|
|
2622
|
+
font-size: 9px;
|
|
2191
2623
|
font-weight: 700;
|
|
2192
|
-
|
|
2193
|
-
|
|
2624
|
+
letter-spacing: 0.03em;
|
|
2625
|
+
background: ${(p) => p.$pos ? T.greenDim : T.redDim};
|
|
2626
|
+
color: ${(p) => p.$pos ? T.green : T.red};
|
|
2627
|
+
margin-bottom: 12px;
|
|
2194
2628
|
`;
|
|
2195
2629
|
const SparkWrap = styled__default.default.div`
|
|
2196
|
-
height:
|
|
2630
|
+
height: 44px;
|
|
2631
|
+
margin: 0 -20px;
|
|
2197
2632
|
`;
|
|
2198
2633
|
const ChartCard = styled__default.default(Card)`
|
|
2199
2634
|
padding: 20px 20px 12px;
|
|
@@ -2205,31 +2640,42 @@ const ChartHeader = styled__default.default.div`
|
|
|
2205
2640
|
display: flex;
|
|
2206
2641
|
align-items: center;
|
|
2207
2642
|
justify-content: space-between;
|
|
2643
|
+
gap: 12px;
|
|
2208
2644
|
flex-shrink: 0;
|
|
2209
2645
|
`;
|
|
2210
2646
|
const ChartTitle = styled__default.default.div`
|
|
2211
|
-
font-size:
|
|
2647
|
+
font-size: 12px;
|
|
2212
2648
|
font-weight: 700;
|
|
2213
|
-
color:
|
|
2649
|
+
color: ${T.textPrimary};
|
|
2650
|
+
letter-spacing: 0.01em;
|
|
2214
2651
|
`;
|
|
2215
|
-
const
|
|
2652
|
+
const SeriesRow = styled__default.default.div`
|
|
2216
2653
|
display: flex;
|
|
2217
2654
|
align-items: center;
|
|
2218
|
-
gap:
|
|
2655
|
+
gap: 6px;
|
|
2219
2656
|
`;
|
|
2220
|
-
const
|
|
2657
|
+
const SeriesBtn = styled__default.default.button`
|
|
2658
|
+
appearance: none;
|
|
2659
|
+
border: 1px solid ${(p) => p.$on ? `${p.$c}55` : T.border};
|
|
2660
|
+
background: ${(p) => p.$on ? `${p.$c}18` : "transparent"};
|
|
2661
|
+
color: ${(p) => p.$on ? p.$c : T.textMuted};
|
|
2662
|
+
font-size: 10px;
|
|
2663
|
+
font-weight: 600;
|
|
2664
|
+
padding: 3px 10px 3px 8px;
|
|
2665
|
+
border-radius: 6px;
|
|
2666
|
+
cursor: pointer;
|
|
2221
2667
|
display: flex;
|
|
2222
2668
|
align-items: center;
|
|
2223
2669
|
gap: 5px;
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2670
|
+
transition: all 160ms;
|
|
2671
|
+
&:hover {
|
|
2672
|
+
border-color: ${(p) => `${p.$c}88`};
|
|
2673
|
+
color: ${(p) => p.$c};
|
|
2674
|
+
background: ${(p) => `${p.$c}22`};
|
|
2675
|
+
}
|
|
2229
2676
|
`;
|
|
2230
|
-
const
|
|
2231
|
-
width:
|
|
2232
|
-
height: 8px;
|
|
2677
|
+
const SDot = styled__default.default.span`
|
|
2678
|
+
width: 6px; height: 6px;
|
|
2233
2679
|
border-radius: 50%;
|
|
2234
2680
|
background: ${(p) => p.$c};
|
|
2235
2681
|
display: inline-block;
|
|
@@ -2237,140 +2683,188 @@ const LegendDot = styled__default.default.span`
|
|
|
2237
2683
|
`;
|
|
2238
2684
|
const HoverInfo = styled__default.default.div`
|
|
2239
2685
|
font-size: 11px;
|
|
2240
|
-
color:
|
|
2686
|
+
color: ${T.textSecondary};
|
|
2241
2687
|
font-variant-numeric: tabular-nums;
|
|
2242
|
-
|
|
2688
|
+
font-family: ${T.mono};
|
|
2689
|
+
min-height: 15px;
|
|
2690
|
+
`;
|
|
2691
|
+
const TwoPanel = styled__default.default.div`
|
|
2692
|
+
display: grid;
|
|
2693
|
+
grid-template-columns: ${(p) => p.$ratio ?? "1fr 288px"};
|
|
2694
|
+
gap: 10px;
|
|
2695
|
+
align-items: stretch;
|
|
2696
|
+
@media (max-width: 900px) { grid-template-columns: 1fr; }
|
|
2243
2697
|
`;
|
|
2244
2698
|
const FeedCard = styled__default.default(Card)`
|
|
2245
2699
|
display: flex;
|
|
2246
2700
|
flex-direction: column;
|
|
2247
|
-
|
|
2701
|
+
overflow: hidden;
|
|
2248
2702
|
`;
|
|
2249
|
-
const
|
|
2250
|
-
padding: 16px
|
|
2251
|
-
border-bottom: 1px solid
|
|
2252
|
-
|
|
2253
|
-
font-size: 10px;
|
|
2703
|
+
const FeedHead = styled__default.default.div`
|
|
2704
|
+
padding: 13px 16px 9px;
|
|
2705
|
+
border-bottom: 1px solid ${T.border};
|
|
2706
|
+
font-size: 11px;
|
|
2254
2707
|
font-weight: 700;
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2708
|
+
color: ${T.textPrimary};
|
|
2709
|
+
flex-shrink: 0;
|
|
2710
|
+
display: flex;
|
|
2711
|
+
justify-content: space-between;
|
|
2712
|
+
align-items: center;
|
|
2258
2713
|
`;
|
|
2259
|
-
const
|
|
2714
|
+
const FeedSelect = styled__default.default.select`
|
|
2715
|
+
appearance: none;
|
|
2716
|
+
border: 1px solid ${T.border};
|
|
2717
|
+
border-radius: 6px;
|
|
2718
|
+
background: ${T.bgCard};
|
|
2719
|
+
color: ${T.textSecondary};
|
|
2720
|
+
font-size: 10px;
|
|
2721
|
+
font-weight: 600;
|
|
2722
|
+
padding: 3px 22px 3px 8px;
|
|
2723
|
+
cursor: pointer;
|
|
2724
|
+
outline: none;
|
|
2725
|
+
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");
|
|
2726
|
+
background-repeat: no-repeat;
|
|
2727
|
+
background-position: right 7px center;
|
|
2728
|
+
transition: border-color 160ms;
|
|
2729
|
+
&:hover, &:focus { border-color: ${T.accent}; color: ${T.textPrimary}; }
|
|
2730
|
+
`;
|
|
2731
|
+
const FeedScroll = styled__default.default.div`
|
|
2260
2732
|
flex: 1;
|
|
2261
2733
|
overflow-y: auto;
|
|
2262
|
-
|
|
2263
|
-
&::-webkit-scrollbar { width:
|
|
2264
|
-
&::-webkit-scrollbar-
|
|
2265
|
-
&::-webkit-scrollbar-thumb { background: #d9d8ff; border-radius: 2px; }
|
|
2734
|
+
min-height: 0;
|
|
2735
|
+
&::-webkit-scrollbar { width: 4px; }
|
|
2736
|
+
&::-webkit-scrollbar-thumb { background: ${T.textMuted}; border-radius: 2px; }
|
|
2266
2737
|
`;
|
|
2267
2738
|
const FeedItem = styled__default.default.div`
|
|
2739
|
+
padding: 9px 14px;
|
|
2740
|
+
border-bottom: 1px solid #f0f0f5;
|
|
2268
2741
|
display: flex;
|
|
2269
|
-
|
|
2270
|
-
gap:
|
|
2271
|
-
|
|
2272
|
-
&:hover { background: #
|
|
2742
|
+
flex-direction: column;
|
|
2743
|
+
gap: 3px;
|
|
2744
|
+
transition: background 130ms;
|
|
2745
|
+
&:hover { background: #fafaff; }
|
|
2746
|
+
&:last-child { border-bottom: none; }
|
|
2273
2747
|
`;
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2276
|
-
|
|
2748
|
+
const FeedTop = styled__default.default.div`
|
|
2749
|
+
display: flex;
|
|
2750
|
+
align-items: center;
|
|
2751
|
+
justify-content: space-between;
|
|
2752
|
+
gap: 6px;
|
|
2277
2753
|
`;
|
|
2278
|
-
const FeedName = styled__default.default.
|
|
2279
|
-
font-size:
|
|
2754
|
+
const FeedName = styled__default.default.span`
|
|
2755
|
+
font-size: 11px;
|
|
2280
2756
|
font-weight: 600;
|
|
2281
|
-
color:
|
|
2282
|
-
white-space: nowrap;
|
|
2757
|
+
color: ${T.textPrimary};
|
|
2283
2758
|
overflow: hidden;
|
|
2284
2759
|
text-overflow: ellipsis;
|
|
2760
|
+
white-space: nowrap;
|
|
2285
2761
|
`;
|
|
2286
|
-
const
|
|
2762
|
+
const FeedEmail = styled__default.default.span`
|
|
2287
2763
|
font-size: 10px;
|
|
2288
|
-
color:
|
|
2289
|
-
white-space: nowrap;
|
|
2764
|
+
color: ${T.textSecondary};
|
|
2290
2765
|
overflow: hidden;
|
|
2291
2766
|
text-overflow: ellipsis;
|
|
2292
|
-
|
|
2767
|
+
white-space: nowrap;
|
|
2768
|
+
display: block;
|
|
2293
2769
|
`;
|
|
2294
|
-
const
|
|
2295
|
-
font-size:
|
|
2296
|
-
color:
|
|
2770
|
+
const FeedMeta = styled__default.default.span`
|
|
2771
|
+
font-size: 9px;
|
|
2772
|
+
color: ${T.textMuted};
|
|
2297
2773
|
white-space: nowrap;
|
|
2298
2774
|
font-variant-numeric: tabular-nums;
|
|
2299
|
-
margin-top: 1px;
|
|
2300
2775
|
`;
|
|
2301
|
-
const
|
|
2302
|
-
|
|
2303
|
-
|
|
2776
|
+
const RtnCard = styled__default.default(Card)`
|
|
2777
|
+
padding: 20px;
|
|
2778
|
+
display: flex;
|
|
2779
|
+
flex-direction: column;
|
|
2780
|
+
gap: 12px;
|
|
2304
2781
|
`;
|
|
2305
|
-
const
|
|
2306
|
-
|
|
2307
|
-
|
|
2782
|
+
const RtnRow = styled__default.default.div`
|
|
2783
|
+
display: flex;
|
|
2784
|
+
align-items: center;
|
|
2785
|
+
gap: 10px;
|
|
2786
|
+
padding: 4px 6px;
|
|
2787
|
+
border-radius: 7px;
|
|
2788
|
+
transition: background 130ms;
|
|
2789
|
+
background: ${(p) => p.$hov ? "#f0f0f6" : "transparent"};
|
|
2790
|
+
cursor: default;
|
|
2308
2791
|
`;
|
|
2309
|
-
const
|
|
2310
|
-
text-align: left;
|
|
2311
|
-
padding: 10px 14px;
|
|
2792
|
+
const RtnLabel = styled__default.default.span`
|
|
2312
2793
|
font-size: 10px;
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
text-
|
|
2316
|
-
letter-spacing: 0.06em;
|
|
2317
|
-
border-bottom: 1px solid #eaeaef;
|
|
2318
|
-
background: #fafafa;
|
|
2794
|
+
color: ${T.textSecondary};
|
|
2795
|
+
min-width: 80px;
|
|
2796
|
+
text-align: right;
|
|
2319
2797
|
white-space: nowrap;
|
|
2320
|
-
&:first-child { padding-left: 20px; }
|
|
2321
|
-
&:last-child { padding-right: 20px; }
|
|
2322
2798
|
`;
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2799
|
+
const RtnSize = styled__default.default.span`
|
|
2800
|
+
font-size: 9px;
|
|
2801
|
+
color: ${T.textMuted};
|
|
2802
|
+
min-width: 52px;
|
|
2803
|
+
text-align: right;
|
|
2804
|
+
white-space: nowrap;
|
|
2805
|
+
font-variant-numeric: tabular-nums;
|
|
2326
2806
|
`;
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
border-
|
|
2332
|
-
|
|
2333
|
-
&:first-child { padding-left: 20px; }
|
|
2334
|
-
&:last-child { padding-right: 20px; }
|
|
2807
|
+
const RtnTrack = styled__default.default.div`
|
|
2808
|
+
flex: 1;
|
|
2809
|
+
height: 10px;
|
|
2810
|
+
background: #eaeaef;
|
|
2811
|
+
border-radius: 4px;
|
|
2812
|
+
overflow: hidden;
|
|
2335
2813
|
`;
|
|
2336
|
-
const
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2814
|
+
const RtnBar = styled__default.default.div`
|
|
2815
|
+
width: ${(p) => Math.max(p.$w, 0.5)}%;
|
|
2816
|
+
height: 100%;
|
|
2817
|
+
background: hsl(${(p) => p.$hue}, 60%, 50%);
|
|
2818
|
+
border-radius: 4px;
|
|
2819
|
+
transition: width 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
2820
|
+
`;
|
|
2821
|
+
const RtnPct = styled__default.default.span`
|
|
2342
2822
|
font-size: 10px;
|
|
2343
2823
|
font-weight: 700;
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2824
|
+
color: hsl(${(p) => p.$hue}, 60%, 60%);
|
|
2825
|
+
min-width: 38px;
|
|
2826
|
+
text-align: right;
|
|
2827
|
+
font-variant-numeric: tabular-nums;
|
|
2828
|
+
font-family: ${T.mono};
|
|
2829
|
+
`;
|
|
2830
|
+
const RtnTip = styled__default.default.div`
|
|
2831
|
+
font-size: 10px;
|
|
2832
|
+
color: ${T.textSecondary};
|
|
2833
|
+
min-height: 14px;
|
|
2834
|
+
font-variant-numeric: tabular-nums;
|
|
2835
|
+
padding: 0 6px;
|
|
2355
2836
|
`;
|
|
2356
|
-
const
|
|
2837
|
+
const RingGrid = styled__default.default.div`
|
|
2357
2838
|
display: flex;
|
|
2358
|
-
|
|
2839
|
+
flex-direction: column;
|
|
2359
2840
|
gap: 10px;
|
|
2360
|
-
margin-bottom: -8px;
|
|
2361
2841
|
`;
|
|
2362
|
-
const
|
|
2363
|
-
|
|
2842
|
+
const RingCard = styled__default.default(Card)`
|
|
2843
|
+
padding: 18px 20px;
|
|
2844
|
+
display: flex;
|
|
2845
|
+
align-items: center;
|
|
2846
|
+
gap: 16px;
|
|
2847
|
+
flex: 1;
|
|
2848
|
+
`;
|
|
2849
|
+
const RingInfo = styled__default.default.div`
|
|
2850
|
+
display: flex;
|
|
2851
|
+
flex-direction: column;
|
|
2852
|
+
gap: 4px;
|
|
2853
|
+
`;
|
|
2854
|
+
const RingVal = styled__default.default.div`
|
|
2855
|
+
font-size: 24px;
|
|
2856
|
+
font-weight: 800;
|
|
2857
|
+
color: ${T.textPrimary};
|
|
2858
|
+
letter-spacing: -0.04em;
|
|
2859
|
+
font-variant-numeric: tabular-nums;
|
|
2860
|
+
line-height: 1;
|
|
2861
|
+
`;
|
|
2862
|
+
const RingLabel = styled__default.default.div`
|
|
2863
|
+
font-size: 9px;
|
|
2364
2864
|
font-weight: 700;
|
|
2365
2865
|
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;
|
|
2866
|
+
letter-spacing: 0.1em;
|
|
2867
|
+
color: ${T.textMuted};
|
|
2374
2868
|
`;
|
|
2375
2869
|
const Empty = styled__default.default.div`
|
|
2376
2870
|
display: flex;
|
|
@@ -2378,32 +2872,52 @@ const Empty = styled__default.default.div`
|
|
|
2378
2872
|
align-items: center;
|
|
2379
2873
|
justify-content: center;
|
|
2380
2874
|
padding: 32px;
|
|
2381
|
-
color:
|
|
2875
|
+
color: ${T.textMuted};
|
|
2382
2876
|
font-size: 12px;
|
|
2383
|
-
gap:
|
|
2877
|
+
gap: 8px;
|
|
2384
2878
|
`;
|
|
2879
|
+
function useCountUp(target, duration = 850) {
|
|
2880
|
+
const [count, setCount] = react.useState(0);
|
|
2881
|
+
const prevRef = react.useRef(0);
|
|
2882
|
+
react.useEffect(() => {
|
|
2883
|
+
let raf;
|
|
2884
|
+
let start = 0;
|
|
2885
|
+
const from = prevRef.current;
|
|
2886
|
+
const diff = target - from;
|
|
2887
|
+
const step = (ts) => {
|
|
2888
|
+
if (!start) start = ts;
|
|
2889
|
+
const progress = Math.min((ts - start) / duration, 1);
|
|
2890
|
+
const eased = 1 - (1 - progress) ** 3;
|
|
2891
|
+
setCount(Math.round(from + eased * diff));
|
|
2892
|
+
if (progress < 1) raf = requestAnimationFrame(step);
|
|
2893
|
+
else prevRef.current = target;
|
|
2894
|
+
};
|
|
2895
|
+
raf = requestAnimationFrame(step);
|
|
2896
|
+
return () => cancelAnimationFrame(raf);
|
|
2897
|
+
}, [target, duration]);
|
|
2898
|
+
return count;
|
|
2899
|
+
}
|
|
2385
2900
|
function Sparkline({
|
|
2386
2901
|
data,
|
|
2387
2902
|
color,
|
|
2388
2903
|
id
|
|
2389
2904
|
}) {
|
|
2390
2905
|
const W = 300;
|
|
2391
|
-
const H =
|
|
2906
|
+
const H = 44;
|
|
2392
2907
|
if (data.length < 2) return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "100%", height: H });
|
|
2393
2908
|
const min = Math.min(...data);
|
|
2394
2909
|
const max = Math.max(...data, min + 1);
|
|
2395
|
-
const range = max - min;
|
|
2396
2910
|
const pts = data.map((v, i) => ({
|
|
2397
2911
|
x: i / (data.length - 1) * W,
|
|
2398
|
-
y: H -
|
|
2912
|
+
y: H - 3 - (v - min) / (max - min) * (H - 8)
|
|
2399
2913
|
}));
|
|
2400
|
-
const line = pts.reduce((
|
|
2914
|
+
const line = pts.reduce((a, p, i) => {
|
|
2401
2915
|
if (i === 0) return `M ${p.x} ${p.y}`;
|
|
2402
2916
|
const pr = pts[i - 1];
|
|
2403
2917
|
const cx = (pr.x + p.x) / 2;
|
|
2404
|
-
return `${
|
|
2918
|
+
return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
|
|
2405
2919
|
}, "");
|
|
2406
|
-
const area = `${line} L ${pts
|
|
2920
|
+
const area = `${line} L ${pts.at(-1).x} ${H} L 0 ${H} Z`;
|
|
2407
2921
|
const gid = `spk-${id}`;
|
|
2408
2922
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2409
2923
|
"svg",
|
|
@@ -2412,10 +2926,10 @@ function Sparkline({
|
|
|
2412
2926
|
height: H,
|
|
2413
2927
|
viewBox: `0 0 ${W} ${H}`,
|
|
2414
2928
|
preserveAspectRatio: "none",
|
|
2415
|
-
"aria-hidden":
|
|
2929
|
+
"aria-hidden": true,
|
|
2416
2930
|
children: [
|
|
2417
2931
|
/* @__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.
|
|
2932
|
+
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.32" }),
|
|
2419
2933
|
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
|
|
2420
2934
|
] }) }),
|
|
2421
2935
|
/* @__PURE__ */ jsxRuntime.jsx("path", { d: area, fill: `url(#${gid})` }),
|
|
@@ -2424,57 +2938,58 @@ function Sparkline({
|
|
|
2424
2938
|
{
|
|
2425
2939
|
d: line,
|
|
2426
2940
|
stroke: color,
|
|
2427
|
-
strokeWidth: "
|
|
2941
|
+
strokeWidth: "1.6",
|
|
2428
2942
|
fill: "none",
|
|
2429
|
-
strokeLinecap: "round"
|
|
2430
|
-
strokeLinejoin: "round"
|
|
2943
|
+
strokeLinecap: "round"
|
|
2431
2944
|
}
|
|
2432
2945
|
)
|
|
2433
2946
|
]
|
|
2434
2947
|
}
|
|
2435
2948
|
);
|
|
2436
2949
|
}
|
|
2437
|
-
const
|
|
2438
|
-
{ key: "totalUsers", color:
|
|
2439
|
-
{ key: "newUsers", color:
|
|
2440
|
-
{ key: "activeUsers", color:
|
|
2950
|
+
const ALL_SERIES = [
|
|
2951
|
+
{ key: "totalUsers", color: T.accent, label: "Total" },
|
|
2952
|
+
{ key: "newUsers", color: T.green, label: "New" },
|
|
2953
|
+
{ key: "activeUsers", color: T.amber, label: "Active" }
|
|
2441
2954
|
];
|
|
2442
|
-
function
|
|
2955
|
+
function GrowthChart({
|
|
2443
2956
|
data,
|
|
2444
2957
|
hovered,
|
|
2445
|
-
onHover
|
|
2958
|
+
onHover,
|
|
2959
|
+
activeSeries
|
|
2446
2960
|
}) {
|
|
2447
2961
|
const W = 600;
|
|
2448
|
-
const H =
|
|
2449
|
-
const PL =
|
|
2962
|
+
const H = 200;
|
|
2963
|
+
const PL = 40;
|
|
2450
2964
|
const PR = 12;
|
|
2451
2965
|
const PT = 12;
|
|
2452
2966
|
const PB = 28;
|
|
2453
2967
|
const CW = W - PL - PR;
|
|
2454
2968
|
const CH = H - PT - PB;
|
|
2455
|
-
if (data.length
|
|
2969
|
+
if (!data.length) {
|
|
2456
2970
|
return /* @__PURE__ */ jsxRuntime.jsxs(Empty, { children: [
|
|
2457
2971
|
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 24 }, children: "📊" }),
|
|
2458
2972
|
/* @__PURE__ */ jsxRuntime.jsx("div", { children: "No growth data for this period" })
|
|
2459
2973
|
] });
|
|
2460
2974
|
}
|
|
2461
|
-
const
|
|
2975
|
+
const active = ALL_SERIES.filter((s) => activeSeries.has(s.key));
|
|
2976
|
+
const allVals = data.flatMap((d) => active.map((s) => d[s.key]));
|
|
2462
2977
|
const maxV = Math.max(...allVals, 10);
|
|
2463
2978
|
const yMax = Math.ceil(maxV * 1.15);
|
|
2464
2979
|
const xp = (i) => PL + i / Math.max(data.length - 1, 1) * CW;
|
|
2465
2980
|
const yp = (v) => PT + (1 - v / yMax) * CH;
|
|
2466
2981
|
const smooth = (vals) => {
|
|
2467
2982
|
const pts = vals.map((v, i) => ({ x: xp(i), y: yp(v) }));
|
|
2468
|
-
const path = pts.reduce((
|
|
2983
|
+
const path = pts.reduce((a, p, i) => {
|
|
2469
2984
|
if (i === 0) return `M ${p.x} ${p.y}`;
|
|
2470
2985
|
const pr = pts[i - 1];
|
|
2471
2986
|
const cx = (pr.x + p.x) / 2;
|
|
2472
|
-
return `${
|
|
2987
|
+
return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
|
|
2473
2988
|
}, "");
|
|
2474
|
-
const area = `${path} L ${pts
|
|
2989
|
+
const area = `${path} L ${pts.at(-1).x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
|
|
2475
2990
|
return { pts, path, area };
|
|
2476
2991
|
};
|
|
2477
|
-
const lines =
|
|
2992
|
+
const lines = active.map((s) => ({
|
|
2478
2993
|
...s,
|
|
2479
2994
|
...smooth(data.map((d) => d[s.key]))
|
|
2480
2995
|
}));
|
|
@@ -2497,17 +3012,17 @@ function AreaChart({
|
|
|
2497
3012
|
/* @__PURE__ */ jsxRuntime.jsx("defs", { children: lines.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2498
3013
|
"linearGradient",
|
|
2499
3014
|
{
|
|
2500
|
-
id: `ag-${s.
|
|
3015
|
+
id: `ag-${s.key}`,
|
|
2501
3016
|
x1: "0",
|
|
2502
3017
|
y1: "0",
|
|
2503
3018
|
x2: "0",
|
|
2504
3019
|
y2: "1",
|
|
2505
3020
|
children: [
|
|
2506
|
-
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.
|
|
3021
|
+
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.18" }),
|
|
2507
3022
|
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: s.color, stopOpacity: "0" })
|
|
2508
3023
|
]
|
|
2509
3024
|
},
|
|
2510
|
-
s.
|
|
3025
|
+
s.key
|
|
2511
3026
|
)) }),
|
|
2512
3027
|
yTicks.map((t) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
|
|
2513
3028
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2527,7 +3042,7 @@ function AreaChart({
|
|
|
2527
3042
|
x: PL - 6,
|
|
2528
3043
|
y: t.y + 4,
|
|
2529
3044
|
textAnchor: "end",
|
|
2530
|
-
fill:
|
|
3045
|
+
fill: T.textMuted,
|
|
2531
3046
|
fontSize: "9",
|
|
2532
3047
|
children: t.v >= 1e3 ? `${(t.v / 1e3).toFixed(1)}k` : t.v
|
|
2533
3048
|
}
|
|
@@ -2544,25 +3059,18 @@ function AreaChart({
|
|
|
2544
3059
|
strokeWidth: "1"
|
|
2545
3060
|
}
|
|
2546
3061
|
),
|
|
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
|
-
)),
|
|
3062
|
+
lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx("path", { d: s.area, fill: `url(#ag-${s.key})` }, `area-${s.key}`)),
|
|
2555
3063
|
lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2556
3064
|
"path",
|
|
2557
3065
|
{
|
|
2558
3066
|
d: s.path,
|
|
2559
3067
|
stroke: s.color,
|
|
2560
|
-
strokeWidth: "
|
|
3068
|
+
strokeWidth: "2",
|
|
2561
3069
|
fill: "none",
|
|
2562
3070
|
strokeLinecap: "round",
|
|
2563
3071
|
strokeLinejoin: "round"
|
|
2564
3072
|
},
|
|
2565
|
-
`
|
|
3073
|
+
`line-${s.key}`
|
|
2566
3074
|
)),
|
|
2567
3075
|
data.map((d, i) => {
|
|
2568
3076
|
if (i % step !== 0 && i !== data.length - 1) return null;
|
|
@@ -2572,7 +3080,7 @@ function AreaChart({
|
|
|
2572
3080
|
x: xp(i),
|
|
2573
3081
|
y: H - 6,
|
|
2574
3082
|
textAnchor: "middle",
|
|
2575
|
-
fill:
|
|
3083
|
+
fill: T.textMuted,
|
|
2576
3084
|
fontSize: "9",
|
|
2577
3085
|
children: d.label
|
|
2578
3086
|
},
|
|
@@ -2582,7 +3090,7 @@ function AreaChart({
|
|
|
2582
3090
|
data.map((d, i) => {
|
|
2583
3091
|
const isHov = hovered === i;
|
|
2584
3092
|
return (
|
|
2585
|
-
// biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
|
|
3093
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
|
|
2586
3094
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2587
3095
|
"g",
|
|
2588
3096
|
{
|
|
@@ -2607,10 +3115,9 @@ function AreaChart({
|
|
|
2607
3115
|
y1: PT,
|
|
2608
3116
|
x2: xp(i),
|
|
2609
3117
|
y2: PT + CH,
|
|
2610
|
-
stroke: "
|
|
3118
|
+
stroke: "rgba(50,50,77,0.2)",
|
|
2611
3119
|
strokeWidth: "1",
|
|
2612
|
-
strokeDasharray: "3 3"
|
|
2613
|
-
opacity: "0.35"
|
|
3120
|
+
strokeDasharray: "3 3"
|
|
2614
3121
|
}
|
|
2615
3122
|
),
|
|
2616
3123
|
lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2620,11 +3127,11 @@ function AreaChart({
|
|
|
2620
3127
|
cy: yp(d[s.key]),
|
|
2621
3128
|
r: isHov ? 4 : 2.5,
|
|
2622
3129
|
fill: s.color,
|
|
2623
|
-
stroke:
|
|
3130
|
+
stroke: T.bg,
|
|
2624
3131
|
strokeWidth: isHov ? 2 : 1,
|
|
2625
|
-
opacity: isHov ? 1 : 0.
|
|
3132
|
+
opacity: isHov ? 1 : 0.5
|
|
2626
3133
|
},
|
|
2627
|
-
s.
|
|
3134
|
+
s.key
|
|
2628
3135
|
))
|
|
2629
3136
|
]
|
|
2630
3137
|
},
|
|
@@ -2636,23 +3143,70 @@ function AreaChart({
|
|
|
2636
3143
|
}
|
|
2637
3144
|
);
|
|
2638
3145
|
}
|
|
3146
|
+
function ProgressRing({
|
|
3147
|
+
value,
|
|
3148
|
+
max,
|
|
3149
|
+
color,
|
|
3150
|
+
size = 56
|
|
3151
|
+
}) {
|
|
3152
|
+
const r = (size - 9) / 2;
|
|
3153
|
+
const circ = 2 * Math.PI * r;
|
|
3154
|
+
const pct = Math.min(value / Math.max(max, 1), 1);
|
|
3155
|
+
const offset = circ * (1 - pct);
|
|
3156
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3157
|
+
"svg",
|
|
3158
|
+
{
|
|
3159
|
+
width: size,
|
|
3160
|
+
height: size,
|
|
3161
|
+
style: { flexShrink: 0 },
|
|
3162
|
+
"aria-hidden": true,
|
|
3163
|
+
children: [
|
|
3164
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3165
|
+
"circle",
|
|
3166
|
+
{
|
|
3167
|
+
cx: size / 2,
|
|
3168
|
+
cy: size / 2,
|
|
3169
|
+
r,
|
|
3170
|
+
fill: "none",
|
|
3171
|
+
stroke: "#e5e7eb",
|
|
3172
|
+
strokeWidth: "7"
|
|
3173
|
+
}
|
|
3174
|
+
),
|
|
3175
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3176
|
+
"circle",
|
|
3177
|
+
{
|
|
3178
|
+
cx: size / 2,
|
|
3179
|
+
cy: size / 2,
|
|
3180
|
+
r,
|
|
3181
|
+
fill: "none",
|
|
3182
|
+
stroke: color,
|
|
3183
|
+
strokeWidth: "7",
|
|
3184
|
+
strokeDasharray: `${circ} ${circ}`,
|
|
3185
|
+
strokeDashoffset: offset,
|
|
3186
|
+
strokeLinecap: "round",
|
|
3187
|
+
transform: `rotate(-90 ${size / 2} ${size / 2})`,
|
|
3188
|
+
style: {
|
|
3189
|
+
transition: "stroke-dashoffset 0.9s cubic-bezier(0.4,0,0.2,1)"
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
)
|
|
3193
|
+
]
|
|
3194
|
+
}
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
2639
3197
|
function relTime(date) {
|
|
2640
|
-
const
|
|
2641
|
-
const
|
|
2642
|
-
if (
|
|
2643
|
-
if (
|
|
2644
|
-
const
|
|
2645
|
-
if (
|
|
2646
|
-
return `${Math.floor(
|
|
3198
|
+
const diff = Date.now() - new Date(date).getTime();
|
|
3199
|
+
const mins = Math.floor(diff / 6e4);
|
|
3200
|
+
if (mins < 1) return "just now";
|
|
3201
|
+
if (mins < 60) return `${mins}m`;
|
|
3202
|
+
const hrs = Math.floor(mins / 60);
|
|
3203
|
+
if (hrs < 24) return `${hrs}h`;
|
|
3204
|
+
return `${Math.floor(hrs / 24)}d`;
|
|
2647
3205
|
}
|
|
2648
|
-
function
|
|
2649
|
-
return
|
|
2650
|
-
month: "short",
|
|
2651
|
-
day: "numeric",
|
|
2652
|
-
year: "numeric"
|
|
2653
|
-
});
|
|
3206
|
+
function rateHue(r) {
|
|
3207
|
+
return r >= 70 ? 142 : r >= 40 ? 38 : 4;
|
|
2654
3208
|
}
|
|
2655
|
-
function
|
|
3209
|
+
function StatItem({
|
|
2656
3210
|
id,
|
|
2657
3211
|
label,
|
|
2658
3212
|
value,
|
|
@@ -2661,26 +3215,39 @@ function StatCardItem({
|
|
|
2661
3215
|
color,
|
|
2662
3216
|
delay = 0
|
|
2663
3217
|
}) {
|
|
3218
|
+
const animated = useCountUp(value);
|
|
2664
3219
|
const isPos = pct === void 0 || pct >= 0;
|
|
2665
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2666
|
-
/* @__PURE__ */ jsxRuntime.
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
"%"
|
|
2674
|
-
] })
|
|
3220
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(StatCard, { $delay: delay, $accent: color, children: [
|
|
3221
|
+
/* @__PURE__ */ jsxRuntime.jsx(StatLabel, { children: label }),
|
|
3222
|
+
/* @__PURE__ */ jsxRuntime.jsx(StatValue, { children: animated.toLocaleString() }),
|
|
3223
|
+
pct !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(TrendBadge, { $pos: isPos, children: [
|
|
3224
|
+
isPos ? "↑" : "↓",
|
|
3225
|
+
" ",
|
|
3226
|
+
Math.abs(pct).toFixed(1),
|
|
3227
|
+
"%"
|
|
2675
3228
|
] }),
|
|
2676
3229
|
sparkline && sparkline.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(SparkWrap, { children: /* @__PURE__ */ jsxRuntime.jsx(Sparkline, { data: sparkline, color, id }) })
|
|
2677
3230
|
] });
|
|
2678
3231
|
}
|
|
2679
3232
|
function OverviewPage() {
|
|
3233
|
+
const { get } = admin.useFetchClient();
|
|
2680
3234
|
const [period, setPeriod] = react.useState(
|
|
2681
3235
|
"weekly"
|
|
2682
3236
|
);
|
|
3237
|
+
const [feedMode, setFeedMode] = react.useState("signups");
|
|
2683
3238
|
const [hovIdx, setHovIdx] = react.useState(null);
|
|
3239
|
+
const [rtnHov, setRtnHov] = react.useState(null);
|
|
3240
|
+
const [activeSeries, setActiveSeries] = react.useState(
|
|
3241
|
+
() => /* @__PURE__ */ new Set(["totalUsers", "newUsers", "activeUsers"])
|
|
3242
|
+
);
|
|
3243
|
+
const toggleSeries = (key) => {
|
|
3244
|
+
setActiveSeries((prev) => {
|
|
3245
|
+
const next = new Set(prev);
|
|
3246
|
+
if (next.has(key) && next.size > 1) next.delete(key);
|
|
3247
|
+
else next.add(key);
|
|
3248
|
+
return next;
|
|
3249
|
+
});
|
|
3250
|
+
};
|
|
2684
3251
|
const statsQuery = reactQuery.useQuery({
|
|
2685
3252
|
queryKey: ["dash-user-stats"],
|
|
2686
3253
|
queryFn: async () => {
|
|
@@ -2697,12 +3264,10 @@ function OverviewPage() {
|
|
|
2697
3264
|
return r.data;
|
|
2698
3265
|
}
|
|
2699
3266
|
});
|
|
2700
|
-
const
|
|
2701
|
-
queryKey: ["dash-
|
|
3267
|
+
const retentionQuery = reactQuery.useQuery({
|
|
3268
|
+
queryKey: ["dash-user-retention", period],
|
|
2702
3269
|
queryFn: async () => {
|
|
2703
|
-
const r = await client.dash.
|
|
2704
|
-
query: { page: 1, limit: 10 }
|
|
2705
|
-
});
|
|
3270
|
+
const r = await client.dash.userRetentionData({ query: { period } });
|
|
2706
3271
|
if (r.error) throw new Error(r.error.message ?? "Failed");
|
|
2707
3272
|
return r.data;
|
|
2708
3273
|
}
|
|
@@ -2711,12 +3276,22 @@ function OverviewPage() {
|
|
|
2711
3276
|
queryKey: ["dash-recent-users"],
|
|
2712
3277
|
queryFn: async () => {
|
|
2713
3278
|
const r = await client.dash.listUsers({
|
|
2714
|
-
query: {
|
|
3279
|
+
query: { limit: 12, offset: 0, sortBy: "createdAt", sortOrder: "desc" }
|
|
2715
3280
|
});
|
|
2716
3281
|
if (r.error) throw new Error(r.error.message ?? "Failed");
|
|
2717
3282
|
return r.data;
|
|
2718
3283
|
}
|
|
2719
3284
|
});
|
|
3285
|
+
const sessionsQuery = reactQuery.useQuery({
|
|
3286
|
+
queryKey: ["dash-recent-sessions"],
|
|
3287
|
+
queryFn: async () => {
|
|
3288
|
+
const { data } = await get(
|
|
3289
|
+
"/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=createdAt:desc"
|
|
3290
|
+
);
|
|
3291
|
+
return data.results ?? [];
|
|
3292
|
+
},
|
|
3293
|
+
refetchInterval: 3e4
|
|
3294
|
+
});
|
|
2720
3295
|
const orgsQuery = reactQuery.useQuery({
|
|
2721
3296
|
queryKey: ["dash-orgs-count"],
|
|
2722
3297
|
queryFn: async () => {
|
|
@@ -2727,595 +3302,385 @@ function OverviewPage() {
|
|
|
2727
3302
|
return r.data;
|
|
2728
3303
|
}
|
|
2729
3304
|
});
|
|
3305
|
+
const sessionsRaw = sessionsQuery.data ?? [];
|
|
3306
|
+
const activeUserIds = [];
|
|
3307
|
+
const lastActiveByUserId = /* @__PURE__ */ new Map();
|
|
3308
|
+
for (const s of sessionsRaw) {
|
|
3309
|
+
if (!lastActiveByUserId.has(s.userId)) {
|
|
3310
|
+
lastActiveByUserId.set(s.userId, s.createdAt);
|
|
3311
|
+
activeUserIds.push(s.userId);
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
const activeUsersQuery = reactQuery.useQuery({
|
|
3315
|
+
queryKey: ["dash-active-users", activeUserIds],
|
|
3316
|
+
queryFn: async () => {
|
|
3317
|
+
if (activeUserIds.length === 0) return [];
|
|
3318
|
+
const params = new URLSearchParams({ uid: "plugin::better-auth.user" });
|
|
3319
|
+
for (let i = 0; i < activeUserIds.length; i++) {
|
|
3320
|
+
params.set(`filters[id][$in][${i}]`, activeUserIds[i]);
|
|
3321
|
+
}
|
|
3322
|
+
params.set("pagination[pageSize]", "12");
|
|
3323
|
+
const { data } = await get(
|
|
3324
|
+
`/better-auth-dashboard/db?${params}`
|
|
3325
|
+
);
|
|
3326
|
+
return data.results ?? [];
|
|
3327
|
+
},
|
|
3328
|
+
enabled: feedMode === "active" && activeUserIds.length > 0
|
|
3329
|
+
});
|
|
3330
|
+
const chartRef = react.useRef(null);
|
|
3331
|
+
const [chartHeight, setChartHeight] = react.useState(0);
|
|
3332
|
+
react.useEffect(() => {
|
|
3333
|
+
const el = chartRef.current;
|
|
3334
|
+
if (!el) return;
|
|
3335
|
+
const ro = new ResizeObserver((entries) => {
|
|
3336
|
+
for (const entry of entries) setChartHeight(entry.contentRect.height);
|
|
3337
|
+
});
|
|
3338
|
+
ro.observe(el);
|
|
3339
|
+
return () => ro.disconnect();
|
|
3340
|
+
}, []);
|
|
2730
3341
|
if (statsQuery.isLoading) {
|
|
2731
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3342
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3343
|
+
designSystem.Flex,
|
|
3344
|
+
{
|
|
3345
|
+
justifyContent: "center",
|
|
3346
|
+
alignItems: "center",
|
|
3347
|
+
padding: 12,
|
|
3348
|
+
style: { background: T.bg, minHeight: "100%" },
|
|
3349
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
|
|
3350
|
+
}
|
|
3351
|
+
);
|
|
2732
3352
|
}
|
|
2733
3353
|
const stats = statsQuery.data;
|
|
2734
3354
|
if (!stats) return null;
|
|
2735
3355
|
const graphData = graphQuery.data?.data ?? [];
|
|
2736
|
-
const
|
|
2737
|
-
const
|
|
3356
|
+
const rtnData = retentionQuery.data?.data ?? [];
|
|
3357
|
+
const users = usersQuery.data?.users ?? [];
|
|
2738
3358
|
const orgCount = orgsQuery.data?.total ?? 0;
|
|
3359
|
+
const activeUserMap = /* @__PURE__ */ new Map();
|
|
3360
|
+
for (const u of activeUsersQuery.data ?? []) {
|
|
3361
|
+
activeUserMap.set(String(u.id), u);
|
|
3362
|
+
}
|
|
3363
|
+
const sortedActiveUsers = activeUserIds.map((uid) => activeUserMap.get(uid)).filter((u) => u !== void 0);
|
|
2739
3364
|
const totalSpark = graphData.map((d) => d.totalUsers);
|
|
2740
3365
|
const newSpark = graphData.map((d) => d.newUsers);
|
|
2741
3366
|
const activeSpark = graphData.map((d) => d.activeUsers);
|
|
2742
3367
|
const hovRow = hovIdx !== null ? graphData[hovIdx] : null;
|
|
2743
|
-
|
|
3368
|
+
const rtnHovRow = rtnHov !== null ? rtnData[rtnHov] : null;
|
|
3369
|
+
const activeMax = Math.max(
|
|
3370
|
+
stats.activeUsers.daily.active ?? 0,
|
|
3371
|
+
stats.activeUsers.weekly.active ?? 0,
|
|
3372
|
+
stats.activeUsers.monthly.active ?? 0,
|
|
3373
|
+
1
|
|
3374
|
+
);
|
|
3375
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$1, { "data-testid": "overview-page", children: [
|
|
2744
3376
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "flex-end", children: [
|
|
2745
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2746
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2747
|
-
|
|
3377
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3378
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3379
|
+
"div",
|
|
3380
|
+
{
|
|
3381
|
+
style: {
|
|
3382
|
+
fontSize: 22,
|
|
3383
|
+
fontWeight: 800,
|
|
3384
|
+
color: T.textPrimary,
|
|
3385
|
+
letterSpacing: "-0.03em"
|
|
3386
|
+
},
|
|
3387
|
+
children: "Overview"
|
|
3388
|
+
}
|
|
3389
|
+
),
|
|
3390
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: T.textSecondary, marginTop: 4 }, children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
|
|
2748
3391
|
weekday: "long",
|
|
2749
3392
|
year: "numeric",
|
|
2750
3393
|
month: "long",
|
|
2751
3394
|
day: "numeric"
|
|
2752
|
-
}) })
|
|
3395
|
+
}) })
|
|
2753
3396
|
] }),
|
|
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
|
-
) })
|
|
3397
|
+
/* @__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
3398
|
] }),
|
|
2769
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2770
|
-
/* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "
|
|
3399
|
+
/* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
|
|
3400
|
+
/* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Metrics" }),
|
|
2771
3401
|
/* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
|
|
2772
3402
|
] }),
|
|
2773
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3403
|
+
/* @__PURE__ */ jsxRuntime.jsxs(StatGrid, { children: [
|
|
2774
3404
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2775
|
-
|
|
3405
|
+
StatItem,
|
|
2776
3406
|
{
|
|
2777
3407
|
id: "total",
|
|
2778
3408
|
label: "Total Users",
|
|
2779
3409
|
value: stats.total ?? 0,
|
|
2780
3410
|
sparkline: totalSpark,
|
|
2781
|
-
color:
|
|
3411
|
+
color: T.accent,
|
|
2782
3412
|
delay: 0
|
|
2783
3413
|
}
|
|
2784
3414
|
),
|
|
2785
3415
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2786
|
-
|
|
3416
|
+
StatItem,
|
|
2787
3417
|
{
|
|
2788
3418
|
id: "d-sig",
|
|
2789
3419
|
label: "Daily Sign-ups",
|
|
2790
3420
|
value: stats.daily.signUps ?? 0,
|
|
2791
3421
|
pct: stats.daily.percentage ?? void 0,
|
|
2792
3422
|
sparkline: newSpark,
|
|
2793
|
-
color:
|
|
3423
|
+
color: T.green,
|
|
2794
3424
|
delay: 1
|
|
2795
3425
|
}
|
|
2796
3426
|
),
|
|
2797
3427
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2798
|
-
|
|
3428
|
+
StatItem,
|
|
2799
3429
|
{
|
|
2800
3430
|
id: "w-sig",
|
|
2801
3431
|
label: "Weekly Sign-ups",
|
|
2802
3432
|
value: stats.weekly.signUps ?? 0,
|
|
2803
3433
|
pct: stats.weekly.percentage ?? void 0,
|
|
2804
3434
|
sparkline: newSpark,
|
|
2805
|
-
color:
|
|
3435
|
+
color: T.green,
|
|
2806
3436
|
delay: 2
|
|
2807
3437
|
}
|
|
2808
3438
|
),
|
|
2809
3439
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2810
|
-
|
|
3440
|
+
StatItem,
|
|
2811
3441
|
{
|
|
2812
3442
|
id: "m-sig",
|
|
2813
3443
|
label: "Monthly Sign-ups",
|
|
2814
3444
|
value: stats.monthly.signUps ?? 0,
|
|
2815
3445
|
pct: stats.monthly.percentage ?? void 0,
|
|
2816
3446
|
sparkline: newSpark,
|
|
2817
|
-
color:
|
|
3447
|
+
color: T.green,
|
|
2818
3448
|
delay: 3
|
|
2819
3449
|
}
|
|
2820
3450
|
),
|
|
2821
3451
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2822
|
-
|
|
3452
|
+
StatItem,
|
|
2823
3453
|
{
|
|
2824
3454
|
id: "orgs",
|
|
2825
3455
|
label: "Organizations",
|
|
2826
3456
|
value: orgCount,
|
|
2827
|
-
color:
|
|
3457
|
+
color: T.purple,
|
|
2828
3458
|
delay: 4
|
|
2829
3459
|
}
|
|
2830
3460
|
)
|
|
2831
3461
|
] }),
|
|
2832
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2833
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3462
|
+
/* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
|
|
3463
|
+
/* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Growth" }),
|
|
3464
|
+
/* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
|
|
3465
|
+
] }),
|
|
3466
|
+
/* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
|
|
3467
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { ref: chartRef, $delay: 5, children: [
|
|
2834
3468
|
/* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
|
|
2835
3469
|
/* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "User Growth" }),
|
|
2836
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
3470
|
+
/* @__PURE__ */ jsxRuntime.jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3471
|
+
SeriesBtn,
|
|
3472
|
+
{
|
|
3473
|
+
$on: activeSeries.has(s.key),
|
|
3474
|
+
$c: s.color,
|
|
3475
|
+
onClick: () => toggleSeries(s.key),
|
|
3476
|
+
children: [
|
|
3477
|
+
/* @__PURE__ */ jsxRuntime.jsx(SDot, { $c: s.color }),
|
|
3478
|
+
s.label
|
|
3479
|
+
]
|
|
3480
|
+
},
|
|
3481
|
+
s.key
|
|
3482
|
+
)) })
|
|
3483
|
+
] }),
|
|
3484
|
+
/* @__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
3485
|
graphQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2843
3486
|
designSystem.Flex,
|
|
2844
3487
|
{
|
|
2845
3488
|
justifyContent: "center",
|
|
2846
3489
|
alignItems: "center",
|
|
2847
|
-
style: { flex: 1, minHeight:
|
|
3490
|
+
style: { flex: 1, minHeight: 170 },
|
|
2848
3491
|
children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
|
|
2849
3492
|
}
|
|
2850
|
-
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
3493
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
3494
|
+
GrowthChart,
|
|
3495
|
+
{
|
|
3496
|
+
data: graphData,
|
|
3497
|
+
hovered: hovIdx,
|
|
3498
|
+
onHover: setHovIdx,
|
|
3499
|
+
activeSeries
|
|
3500
|
+
}
|
|
3501
|
+
)
|
|
2851
3502
|
] }),
|
|
2852
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2853
|
-
|
|
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,
|
|
3503
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3504
|
+
FeedCard,
|
|
2902
3505
|
{
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
3506
|
+
$delay: 6,
|
|
3507
|
+
style: chartHeight > 0 ? { height: chartHeight } : void 0,
|
|
3508
|
+
children: [
|
|
3509
|
+
/* @__PURE__ */ jsxRuntime.jsxs(FeedHead, { children: [
|
|
3510
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recently Active" }),
|
|
3511
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3512
|
+
FeedSelect,
|
|
3513
|
+
{
|
|
3514
|
+
value: feedMode,
|
|
3515
|
+
onChange: (e) => setFeedMode(e.target.value),
|
|
3516
|
+
children: [
|
|
3517
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "signups", children: "Recent Sign-ups" }),
|
|
3518
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "active", children: "Recently Active" })
|
|
3519
|
+
]
|
|
3520
|
+
}
|
|
3521
|
+
)
|
|
3522
|
+
] }),
|
|
3523
|
+
/* @__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: [
|
|
3524
|
+
/* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
|
|
3525
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
|
|
3526
|
+
/* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
|
|
3527
|
+
/* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
|
|
3528
|
+
] }),
|
|
3529
|
+
/* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(u.createdAt) })
|
|
3530
|
+
] }),
|
|
3531
|
+
/* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
|
|
3532
|
+
] }, u.id)) : activeUsersQuery.isLoading || sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : sortedActiveUsers.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No recent activity" }) : sortedActiveUsers.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
|
|
3533
|
+
/* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
|
|
3534
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
|
|
3535
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3536
|
+
Avatar,
|
|
3537
|
+
{
|
|
3538
|
+
name: u.name,
|
|
3539
|
+
src: u.image ?? void 0,
|
|
3540
|
+
size: 20
|
|
3541
|
+
}
|
|
3542
|
+
),
|
|
3543
|
+
/* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
|
|
3544
|
+
] }),
|
|
3545
|
+
/* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(
|
|
3546
|
+
lastActiveByUserId.get(String(u.id)) ?? u.createdAt
|
|
3547
|
+
) })
|
|
3548
|
+
] }),
|
|
3549
|
+
/* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
|
|
3550
|
+
] }, u.documentId)) })
|
|
3551
|
+
]
|
|
2910
3552
|
}
|
|
2911
3553
|
)
|
|
2912
3554
|
] }),
|
|
2913
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2914
|
-
/* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "
|
|
3555
|
+
/* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
|
|
3556
|
+
/* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Retention & Activity" }),
|
|
2915
3557
|
/* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
|
|
2916
3558
|
] }),
|
|
2917
|
-
/* @__PURE__ */ jsxRuntime.
|
|
2918
|
-
/* @__PURE__ */ jsxRuntime.
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
TD$1,
|
|
2948
|
-
{
|
|
2949
|
-
style: {
|
|
2950
|
-
color: "#8e8ea9",
|
|
2951
|
-
fontSize: 11,
|
|
2952
|
-
whiteSpace: "nowrap"
|
|
3559
|
+
/* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
|
|
3560
|
+
/* @__PURE__ */ jsxRuntime.jsxs(RtnCard, { $delay: 7, children: [
|
|
3561
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
|
|
3562
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "Cohort Retention" }),
|
|
3563
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { alignItems: "center", gap: 2, children: [
|
|
3564
|
+
{ hue: 142, label: "≥70%" },
|
|
3565
|
+
{ hue: 38, label: "40–70%" },
|
|
3566
|
+
{ hue: 4, label: "<40%" }
|
|
3567
|
+
].map(({ hue, label }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3568
|
+
designSystem.Flex,
|
|
3569
|
+
{
|
|
3570
|
+
alignItems: "center",
|
|
3571
|
+
gap: 1,
|
|
3572
|
+
style: { marginLeft: 8 },
|
|
3573
|
+
children: [
|
|
3574
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3575
|
+
"span",
|
|
3576
|
+
{
|
|
3577
|
+
style: {
|
|
3578
|
+
width: 8,
|
|
3579
|
+
height: 8,
|
|
3580
|
+
borderRadius: "50%",
|
|
3581
|
+
background: `hsl(${hue},60%,50%)`,
|
|
3582
|
+
display: "inline-block",
|
|
3583
|
+
flexShrink: 0
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
),
|
|
3587
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 9, color: T.textMuted }, children: label })
|
|
3588
|
+
]
|
|
2953
3589
|
},
|
|
2954
|
-
|
|
2955
|
-
}
|
|
2956
|
-
),
|
|
2957
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2958
|
-
|
|
3590
|
+
hue
|
|
3591
|
+
)) })
|
|
3592
|
+
] }),
|
|
3593
|
+
/* @__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" }),
|
|
3594
|
+
retentionQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3595
|
+
designSystem.Flex,
|
|
2959
3596
|
{
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3597
|
+
justifyContent: "center",
|
|
3598
|
+
alignItems: "center",
|
|
3599
|
+
style: { minHeight: 80 },
|
|
3600
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
|
|
2963
3601
|
}
|
|
2964
|
-
)
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
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" })
|
|
3602
|
+
) : rtnData.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(Empty, { children: [
|
|
3603
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 22 }, children: "📉" }),
|
|
3604
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: "No retention data for this period" })
|
|
3605
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: 5 }, children: rtnData.map((row, i) => {
|
|
3606
|
+
const hue = rateHue(row.retentionRate);
|
|
3607
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3608
|
+
RtnRow,
|
|
3609
|
+
{
|
|
3610
|
+
$hov: rtnHov === i,
|
|
3611
|
+
onMouseEnter: () => setRtnHov(i),
|
|
3612
|
+
onMouseLeave: () => setRtnHov(null),
|
|
3613
|
+
children: [
|
|
3614
|
+
/* @__PURE__ */ jsxRuntime.jsx(RtnLabel, { children: row.label }),
|
|
3615
|
+
/* @__PURE__ */ jsxRuntime.jsx(RtnSize, { children: row.cohortSize.toLocaleString() }),
|
|
3616
|
+
/* @__PURE__ */ jsxRuntime.jsx(RtnTrack, { children: /* @__PURE__ */ jsxRuntime.jsx(RtnBar, { $w: row.retentionRate, $hue: hue }) }),
|
|
3617
|
+
/* @__PURE__ */ jsxRuntime.jsxs(RtnPct, { $hue: hue, children: [
|
|
3618
|
+
row.retentionRate.toFixed(1),
|
|
3619
|
+
"%"
|
|
3620
|
+
] })
|
|
3621
|
+
]
|
|
3622
|
+
},
|
|
3623
|
+
row.n
|
|
3624
|
+
);
|
|
3625
|
+
}) })
|
|
3188
3626
|
] }),
|
|
3189
|
-
|
|
3190
|
-
designSystem.Button,
|
|
3627
|
+
/* @__PURE__ */ jsxRuntime.jsx(RingGrid, { children: [
|
|
3191
3628
|
{
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3629
|
+
label: "Daily Active",
|
|
3630
|
+
value: stats.activeUsers.daily.active,
|
|
3631
|
+
pct: stats.activeUsers.daily.percentage,
|
|
3632
|
+
color: T.amber,
|
|
3633
|
+
sparkline: activeSpark,
|
|
3634
|
+
delay: 8
|
|
3635
|
+
},
|
|
3636
|
+
{
|
|
3637
|
+
label: "Weekly Active",
|
|
3638
|
+
value: stats.activeUsers.weekly.active,
|
|
3639
|
+
pct: stats.activeUsers.weekly.percentage,
|
|
3640
|
+
color: T.green,
|
|
3641
|
+
sparkline: activeSpark,
|
|
3642
|
+
delay: 9
|
|
3643
|
+
},
|
|
3644
|
+
{
|
|
3645
|
+
label: "Monthly Active",
|
|
3646
|
+
value: stats.activeUsers.monthly.active,
|
|
3647
|
+
pct: stats.activeUsers.monthly.percentage,
|
|
3648
|
+
color: T.accent,
|
|
3649
|
+
sparkline: activeSpark,
|
|
3650
|
+
delay: 10
|
|
3202
3651
|
}
|
|
3203
|
-
)
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
/* @__PURE__ */ jsxRuntime.jsx(TableCard$1, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading sessions…" }) }) : usersWithSessions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { children: "No active sessions" }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3207
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ColumnHeader, { children: [
|
|
3208
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3209
|
-
designSystem.Checkbox,
|
|
3210
|
-
{
|
|
3211
|
-
checked: allSelected,
|
|
3212
|
-
onCheckedChange: handleSelectAll,
|
|
3213
|
-
"aria-label": "Select all users"
|
|
3214
|
-
}
|
|
3215
|
-
),
|
|
3216
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "User / Sessions" }),
|
|
3217
|
-
/* @__PURE__ */ jsxRuntime.jsx(ColumnHeaderRight, { children: "Sessions" })
|
|
3218
|
-
] }),
|
|
3219
|
-
usersWithSessions.map((userRow, i) => /* @__PURE__ */ jsxRuntime.jsxs(UserGroup, { $i: i, "data-testid": "session-user-row", children: [
|
|
3220
|
-
/* @__PURE__ */ jsxRuntime.jsxs(UserRowHeader, { children: [
|
|
3652
|
+
].map(({ label, value, pct, color, delay }) => {
|
|
3653
|
+
const isPos = pct == null || pct >= 0;
|
|
3654
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(RingCard, { $delay: delay, children: [
|
|
3221
3655
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3222
|
-
|
|
3656
|
+
ProgressRing,
|
|
3223
3657
|
{
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3658
|
+
value: value ?? 0,
|
|
3659
|
+
max: activeMax,
|
|
3660
|
+
color,
|
|
3661
|
+
size: 56
|
|
3227
3662
|
}
|
|
3228
3663
|
),
|
|
3229
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3230
|
-
|
|
3231
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3232
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3233
|
-
|
|
3234
|
-
/* @__PURE__ */ jsxRuntime.jsxs(SessionCountChip, { children: [
|
|
3235
|
-
userRow.sessions.length,
|
|
3236
|
-
" session",
|
|
3237
|
-
userRow.sessions.length !== 1 ? "s" : ""
|
|
3238
|
-
] })
|
|
3239
|
-
] }),
|
|
3240
|
-
userRow.sessions.map((session) => /* @__PURE__ */ jsxRuntime.jsxs(SessionCard, { "data-testid": "session-row", children: [
|
|
3241
|
-
/* @__PURE__ */ jsxRuntime.jsxs(SessionMeta, { children: [
|
|
3242
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3243
|
-
designSystem.Flex,
|
|
3664
|
+
/* @__PURE__ */ jsxRuntime.jsxs(RingInfo, { children: [
|
|
3665
|
+
/* @__PURE__ */ jsxRuntime.jsx(RingLabel, { children: label }),
|
|
3666
|
+
/* @__PURE__ */ jsxRuntime.jsx(RingVal, { children: (value ?? 0).toLocaleString() }),
|
|
3667
|
+
pct != null && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3668
|
+
TrendBadge,
|
|
3244
3669
|
{
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
style: { flexWrap: "wrap" },
|
|
3670
|
+
$pos: isPos,
|
|
3671
|
+
style: { marginTop: 4, marginBottom: 0 },
|
|
3248
3672
|
children: [
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
" ",
|
|
3254
|
-
"· Expires",
|
|
3255
|
-
" ",
|
|
3256
|
-
new Date(session.expiresAt).toLocaleString()
|
|
3257
|
-
] })
|
|
3673
|
+
isPos ? "↑" : "↓",
|
|
3674
|
+
" ",
|
|
3675
|
+
Math.abs(pct).toFixed(1),
|
|
3676
|
+
"%"
|
|
3258
3677
|
]
|
|
3259
3678
|
}
|
|
3260
|
-
)
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
{
|
|
3266
|
-
label: "Revoke session",
|
|
3267
|
-
onClick: () => setConfirmRevokeSessionId(session.id),
|
|
3268
|
-
style: { flexShrink: 0 },
|
|
3269
|
-
"data-testid": "revoke-session-btn",
|
|
3270
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
|
|
3271
|
-
}
|
|
3272
|
-
)
|
|
3273
|
-
] }, session.id))
|
|
3274
|
-
] }, userRow.id))
|
|
3275
|
-
] }) }),
|
|
3276
|
-
usersWithSessions.length === PAGE_SIZE$1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
|
|
3277
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3278
|
-
designSystem.Button,
|
|
3279
|
-
{
|
|
3280
|
-
variant: "tertiary",
|
|
3281
|
-
size: "S",
|
|
3282
|
-
disabled: page === 1,
|
|
3283
|
-
onClick: () => setPage((p) => p - 1),
|
|
3284
|
-
children: "Previous"
|
|
3285
|
-
}
|
|
3286
|
-
),
|
|
3287
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3288
|
-
designSystem.Button,
|
|
3289
|
-
{
|
|
3290
|
-
variant: "tertiary",
|
|
3291
|
-
size: "S",
|
|
3292
|
-
onClick: () => setPage((p) => p + 1),
|
|
3293
|
-
children: "Next"
|
|
3294
|
-
}
|
|
3295
|
-
)
|
|
3296
|
-
] }) }),
|
|
3297
|
-
confirmRevokeSessionId && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3298
|
-
ConfirmDialog,
|
|
3299
|
-
{
|
|
3300
|
-
title: "Revoke session",
|
|
3301
|
-
message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
|
|
3302
|
-
confirmLabel: "Revoke",
|
|
3303
|
-
loading: revokeSessionMutation.isLoading,
|
|
3304
|
-
onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
|
|
3305
|
-
onCancel: () => setConfirmRevokeSessionId(null)
|
|
3306
|
-
}
|
|
3307
|
-
),
|
|
3308
|
-
confirmRevokeMany && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3309
|
-
ConfirmDialog,
|
|
3310
|
-
{
|
|
3311
|
-
title: `Revoke sessions for ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
|
|
3312
|
-
message: `Are you sure you want to revoke all sessions for ${selected.size} user${selected.size !== 1 ? "s" : ""}? They will be signed out on all their devices.`,
|
|
3313
|
-
confirmLabel: "Revoke all",
|
|
3314
|
-
loading: revokeManyMutation.isLoading,
|
|
3315
|
-
onConfirm: () => revokeManyMutation.mutate([...selected]),
|
|
3316
|
-
onCancel: () => setConfirmRevokeMany(false)
|
|
3317
|
-
}
|
|
3318
|
-
)
|
|
3679
|
+
)
|
|
3680
|
+
] })
|
|
3681
|
+
] }, label);
|
|
3682
|
+
}) })
|
|
3683
|
+
] })
|
|
3319
3684
|
] });
|
|
3320
3685
|
}
|
|
3321
3686
|
function useUsers(options = {}) {
|
|
@@ -3556,6 +3921,45 @@ const ReadOnlyCodeInput = styled__default.default.div`
|
|
|
3556
3921
|
word-break: break-all;
|
|
3557
3922
|
overflow-wrap: anywhere;
|
|
3558
3923
|
`;
|
|
3924
|
+
const SessionCard = styled__default.default.div`
|
|
3925
|
+
display: flex;
|
|
3926
|
+
align-items: flex-start;
|
|
3927
|
+
justify-content: space-between;
|
|
3928
|
+
gap: 12px;
|
|
3929
|
+
padding: 10px 14px;
|
|
3930
|
+
background: white;
|
|
3931
|
+
border: 1px solid #eaeaef;
|
|
3932
|
+
border-radius: 8px;
|
|
3933
|
+
`;
|
|
3934
|
+
const SessionMeta = styled__default.default.div`
|
|
3935
|
+
display: flex;
|
|
3936
|
+
flex-direction: column;
|
|
3937
|
+
gap: 3px;
|
|
3938
|
+
flex: 1;
|
|
3939
|
+
min-width: 0;
|
|
3940
|
+
`;
|
|
3941
|
+
const IpChip = styled__default.default.span`
|
|
3942
|
+
display: inline-block;
|
|
3943
|
+
background: #f0f0ff;
|
|
3944
|
+
color: #4945ff;
|
|
3945
|
+
border-radius: 5px;
|
|
3946
|
+
padding: 2px 6px;
|
|
3947
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
3948
|
+
font-size: 11px;
|
|
3949
|
+
font-weight: 600;
|
|
3950
|
+
`;
|
|
3951
|
+
const TimestampText = styled__default.default.span`
|
|
3952
|
+
font-size: 11px;
|
|
3953
|
+
color: #8e8ea9;
|
|
3954
|
+
`;
|
|
3955
|
+
const AgentText = styled__default.default.span`
|
|
3956
|
+
font-size: 11px;
|
|
3957
|
+
color: #b8b8c7;
|
|
3958
|
+
overflow: hidden;
|
|
3959
|
+
text-overflow: ellipsis;
|
|
3960
|
+
white-space: nowrap;
|
|
3961
|
+
display: block;
|
|
3962
|
+
`;
|
|
3559
3963
|
const STANDARD_FIELDS = /* @__PURE__ */ new Set([
|
|
3560
3964
|
"id",
|
|
3561
3965
|
"name",
|
|
@@ -3577,6 +3981,7 @@ function UserDetailDrawer({
|
|
|
3577
3981
|
}) {
|
|
3578
3982
|
const qc = reactQuery.useQueryClient();
|
|
3579
3983
|
const { toggleNotification } = admin.useNotification();
|
|
3984
|
+
const { get, put } = admin.useFetchClient();
|
|
3580
3985
|
const schemaQuery = useModelSchema("user");
|
|
3581
3986
|
const userQuery = reactQuery.useQuery({
|
|
3582
3987
|
queryKey: ["dash-user", userId],
|
|
@@ -3598,6 +4003,27 @@ function UserDetailDrawer({
|
|
|
3598
4003
|
return result.data?.organizations ?? [];
|
|
3599
4004
|
}
|
|
3600
4005
|
});
|
|
4006
|
+
const sessionsQuery = reactQuery.useQuery({
|
|
4007
|
+
queryKey: ["dash-user-sessions", userId],
|
|
4008
|
+
queryFn: async () => {
|
|
4009
|
+
const { data } = await get(
|
|
4010
|
+
`/better-auth-dashboard/db?uid=plugin::better-auth.session&filters[userId][$eq]=${userId}&sort[0]=createdAt:desc&pagination[pageSize]=50`
|
|
4011
|
+
);
|
|
4012
|
+
return data.results ?? [];
|
|
4013
|
+
}
|
|
4014
|
+
});
|
|
4015
|
+
const strapiUserQuery = reactQuery.useQuery({
|
|
4016
|
+
queryKey: ["dash-strapi-user", userId],
|
|
4017
|
+
enabled: !!schemaQuery.data,
|
|
4018
|
+
queryFn: async () => {
|
|
4019
|
+
const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
|
|
4020
|
+
const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
|
|
4021
|
+
const { data } = await get(
|
|
4022
|
+
`/better-auth-dashboard/db?uid=plugin::better-auth.user&filters[id][$eq]=${userId}&pagination[pageSize]=1${populateParam}`
|
|
4023
|
+
);
|
|
4024
|
+
return data.results?.[0] ?? null;
|
|
4025
|
+
}
|
|
4026
|
+
});
|
|
3601
4027
|
const [activeTab, setActiveTab] = react.useState("profile");
|
|
3602
4028
|
const [editName, setEditName] = react.useState(void 0);
|
|
3603
4029
|
const [editEmail, setEditEmail] = react.useState(void 0);
|
|
@@ -3608,6 +4034,8 @@ function UserDetailDrawer({
|
|
|
3608
4034
|
const [banReason, setBanReason] = react.useState("");
|
|
3609
4035
|
const [banExpiresDays, setBanExpiresDays] = react.useState("");
|
|
3610
4036
|
const [confirmRevokeAll, setConfirmRevokeAll] = react.useState(false);
|
|
4037
|
+
const [confirmRevokeSessionId, setConfirmRevokeSessionId] = react.useState(null);
|
|
4038
|
+
const [confirmBan, setConfirmBan] = react.useState(false);
|
|
3611
4039
|
const [confirmUnban, setConfirmUnban] = react.useState(false);
|
|
3612
4040
|
const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = react.useState(null);
|
|
3613
4041
|
const [confirmDisable2FA, setConfirmDisable2FA] = react.useState(false);
|
|
@@ -3626,7 +4054,7 @@ function UserDetailDrawer({
|
|
|
3626
4054
|
setEditExtra((prev) => ({ ...prev, [name]: value }));
|
|
3627
4055
|
};
|
|
3628
4056
|
const extraData = {
|
|
3629
|
-
...
|
|
4057
|
+
...strapiUserQuery.data ?? {},
|
|
3630
4058
|
...editExtra
|
|
3631
4059
|
};
|
|
3632
4060
|
const updateMutation = reactQuery.useMutation({
|
|
@@ -3637,17 +4065,17 @@ function UserDetailDrawer({
|
|
|
3637
4065
|
if (editEmailVerified !== void 0)
|
|
3638
4066
|
body.emailVerified = editEmailVerified;
|
|
3639
4067
|
if (editImage !== void 0) body.image = editImage;
|
|
3640
|
-
const
|
|
3641
|
-
|
|
3642
|
-
|
|
4068
|
+
const documentId = strapiUserQuery.data?.documentId;
|
|
4069
|
+
if (!documentId) throw new Error("Could not resolve documentId for user");
|
|
4070
|
+
await put(
|
|
4071
|
+
`/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
|
|
4072
|
+
body
|
|
3643
4073
|
);
|
|
3644
|
-
if (result.error)
|
|
3645
|
-
throw new Error(result.error.message ?? "Update failed");
|
|
3646
|
-
return result.data;
|
|
3647
4074
|
},
|
|
3648
4075
|
onSuccess: () => {
|
|
3649
4076
|
qc.invalidateQueries({ queryKey: ["dash-user", userId] });
|
|
3650
4077
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
4078
|
+
qc.invalidateQueries({ queryKey: ["dash-strapi-user", userId] });
|
|
3651
4079
|
setEditName(void 0);
|
|
3652
4080
|
setEditEmail(void 0);
|
|
3653
4081
|
setEditEmailVerified(void 0);
|
|
@@ -3666,6 +4094,27 @@ function UserDetailDrawer({
|
|
|
3666
4094
|
});
|
|
3667
4095
|
}
|
|
3668
4096
|
});
|
|
4097
|
+
const revokeSessionMutation = reactQuery.useMutation({
|
|
4098
|
+
mutationFn: async (sessionId) => {
|
|
4099
|
+
const result = await client.dash.sessions.revoke(
|
|
4100
|
+
{},
|
|
4101
|
+
withContext({ sessionId })
|
|
4102
|
+
);
|
|
4103
|
+
if (result.error)
|
|
4104
|
+
throw new Error(result.error.message ?? "Revoke failed");
|
|
4105
|
+
},
|
|
4106
|
+
onSuccess: () => {
|
|
4107
|
+
setConfirmRevokeSessionId(null);
|
|
4108
|
+
qc.invalidateQueries({ queryKey: ["dash-user-sessions", userId] });
|
|
4109
|
+
toggleNotification({ type: "success", message: "Session revoked" });
|
|
4110
|
+
},
|
|
4111
|
+
onError: (err) => {
|
|
4112
|
+
toggleNotification({
|
|
4113
|
+
type: "danger",
|
|
4114
|
+
message: err.message ?? "Failed to revoke session"
|
|
4115
|
+
});
|
|
4116
|
+
}
|
|
4117
|
+
});
|
|
3669
4118
|
const passwordMutation = reactQuery.useMutation({
|
|
3670
4119
|
mutationFn: async () => {
|
|
3671
4120
|
const result = await client.dash.setPassword(
|
|
@@ -3703,6 +4152,7 @@ function UserDetailDrawer({
|
|
|
3703
4152
|
return result.data;
|
|
3704
4153
|
},
|
|
3705
4154
|
onSuccess: () => {
|
|
4155
|
+
setConfirmBan(false);
|
|
3706
4156
|
qc.invalidateQueries({ queryKey: ["dash-user", userId] });
|
|
3707
4157
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
3708
4158
|
setBanReason("");
|
|
@@ -4318,7 +4768,8 @@ function UserDetailDrawer({
|
|
|
4318
4768
|
] })
|
|
4319
4769
|
] })
|
|
4320
4770
|
] }) }),
|
|
4321
|
-
banEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap:
|
|
4771
|
+
banEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
|
|
4772
|
+
/* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Ban status" }),
|
|
4322
4773
|
/* @__PURE__ */ jsxRuntime.jsxs(WarnCard, { children: [
|
|
4323
4774
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4324
4775
|
designSystem.Typography,
|
|
@@ -4333,7 +4784,7 @@ function UserDetailDrawer({
|
|
|
4333
4784
|
"Reason: ",
|
|
4334
4785
|
user.banReason
|
|
4335
4786
|
] }),
|
|
4336
|
-
user.banExpires
|
|
4787
|
+
user.banExpires ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
|
|
4337
4788
|
"Expires:",
|
|
4338
4789
|
" ",
|
|
4339
4790
|
new Date(user.banExpires).toLocaleDateString(
|
|
@@ -4345,8 +4796,7 @@ function UserDetailDrawer({
|
|
|
4345
4796
|
day: "numeric"
|
|
4346
4797
|
}
|
|
4347
4798
|
)
|
|
4348
|
-
] }),
|
|
4349
|
-
!user.banExpires && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
|
|
4799
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
|
|
4350
4800
|
] }),
|
|
4351
4801
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4352
4802
|
designSystem.Button,
|
|
@@ -4356,27 +4806,28 @@ function UserDetailDrawer({
|
|
|
4356
4806
|
children: "Lift ban"
|
|
4357
4807
|
}
|
|
4358
4808
|
) })
|
|
4359
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4809
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
|
|
4810
|
+
/* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Apply ban" }),
|
|
4811
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4812
|
+
designSystem.Field.Root,
|
|
4813
|
+
{
|
|
4814
|
+
hint: "Optional — will be shown to the user",
|
|
4815
|
+
style: { width: "100%" },
|
|
4816
|
+
children: [
|
|
4817
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Reason" }),
|
|
4818
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4819
|
+
designSystem.TextInput,
|
|
4820
|
+
{
|
|
4821
|
+
value: banReason,
|
|
4822
|
+
onChange: (e) => setBanReason(e.target.value),
|
|
4823
|
+
placeholder: "e.g. Violated terms of service"
|
|
4824
|
+
}
|
|
4825
|
+
),
|
|
4826
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
|
|
4827
|
+
]
|
|
4828
|
+
}
|
|
4829
|
+
),
|
|
4360
4830
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
|
|
4361
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 12, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4362
|
-
designSystem.Field.Root,
|
|
4363
|
-
{
|
|
4364
|
-
hint: "Optional — will be shown to the user",
|
|
4365
|
-
style: { width: "100%" },
|
|
4366
|
-
children: [
|
|
4367
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Reason" }),
|
|
4368
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4369
|
-
designSystem.TextInput,
|
|
4370
|
-
{
|
|
4371
|
-
value: banReason,
|
|
4372
|
-
onChange: (e) => setBanReason(e.target.value),
|
|
4373
|
-
placeholder: "e.g. Violated terms of service"
|
|
4374
|
-
}
|
|
4375
|
-
),
|
|
4376
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
|
|
4377
|
-
]
|
|
4378
|
-
}
|
|
4379
|
-
) }),
|
|
4380
4831
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4381
4832
|
designSystem.Field.Root,
|
|
4382
4833
|
{
|
|
@@ -4397,24 +4848,13 @@ function UserDetailDrawer({
|
|
|
4397
4848
|
]
|
|
4398
4849
|
}
|
|
4399
4850
|
) }),
|
|
4400
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children:
|
|
4401
|
-
designSystem.Flex,
|
|
4402
|
-
{
|
|
4403
|
-
direction: "column",
|
|
4404
|
-
justifyContent: "flex-end",
|
|
4405
|
-
style: { height: "100%", paddingBottom: 4 },
|
|
4406
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs(PreviewPill, { children: [
|
|
4407
|
-
"⏱ Expires ",
|
|
4408
|
-
banExpiryPreview
|
|
4409
|
-
] })
|
|
4410
|
-
}
|
|
4411
|
-
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
4851
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4412
4852
|
designSystem.Flex,
|
|
4413
4853
|
{
|
|
4414
4854
|
direction: "column",
|
|
4415
4855
|
justifyContent: "flex-end",
|
|
4416
4856
|
style: { height: "100%", paddingBottom: 4 },
|
|
4417
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(PreviewPill, { children: "♾ Permanent ban" })
|
|
4857
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(PreviewPill, { children: banExpiryPreview ? `⏱ Expires ${banExpiryPreview}` : "♾ Permanent ban" })
|
|
4418
4858
|
}
|
|
4419
4859
|
) })
|
|
4420
4860
|
] }),
|
|
@@ -4422,31 +4862,68 @@ function UserDetailDrawer({
|
|
|
4422
4862
|
designSystem.Button,
|
|
4423
4863
|
{
|
|
4424
4864
|
variant: "danger",
|
|
4425
|
-
|
|
4426
|
-
onClick: () => banMutation.mutate(),
|
|
4865
|
+
onClick: () => setConfirmBan(true),
|
|
4427
4866
|
children: "Ban user"
|
|
4428
4867
|
}
|
|
4429
4868
|
) })
|
|
4430
4869
|
] }) }) }),
|
|
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
|
-
|
|
4870
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
|
|
4871
|
+
/* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
|
|
4872
|
+
/* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Session management" }),
|
|
4873
|
+
/* @__PURE__ */ jsxRuntime.jsxs(AccountRow, { children: [
|
|
4874
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
|
|
4875
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
|
|
4876
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
|
|
4877
|
+
] }),
|
|
4878
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4879
|
+
designSystem.Button,
|
|
4880
|
+
{
|
|
4881
|
+
variant: "danger-light",
|
|
4882
|
+
size: "S",
|
|
4883
|
+
onClick: () => setConfirmRevokeAll(true),
|
|
4884
|
+
style: { flexShrink: 0 },
|
|
4885
|
+
children: "Revoke all"
|
|
4886
|
+
}
|
|
4887
|
+
)
|
|
4888
|
+
] })
|
|
4889
|
+
] }),
|
|
4890
|
+
/* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
|
|
4891
|
+
/* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Active sessions" }),
|
|
4892
|
+
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: 2, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxRuntime.jsxs(SessionCard, { children: [
|
|
4893
|
+
/* @__PURE__ */ jsxRuntime.jsxs(SessionMeta, { children: [
|
|
4894
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4895
|
+
designSystem.Flex,
|
|
4896
|
+
{
|
|
4897
|
+
gap: 2,
|
|
4898
|
+
alignItems: "center",
|
|
4899
|
+
style: { flexWrap: "wrap" },
|
|
4900
|
+
children: [
|
|
4901
|
+
session.ipAddress && /* @__PURE__ */ jsxRuntime.jsx(IpChip, { children: session.ipAddress }),
|
|
4902
|
+
/* @__PURE__ */ jsxRuntime.jsxs(TimestampText, { children: [
|
|
4903
|
+
"Created",
|
|
4904
|
+
" ",
|
|
4905
|
+
new Date(session.createdAt).toLocaleString(),
|
|
4906
|
+
" · Expires",
|
|
4907
|
+
" ",
|
|
4908
|
+
new Date(session.expiresAt).toLocaleString()
|
|
4909
|
+
] })
|
|
4910
|
+
]
|
|
4911
|
+
}
|
|
4912
|
+
),
|
|
4913
|
+
session.userAgent && /* @__PURE__ */ jsxRuntime.jsx(AgentText, { children: session.userAgent })
|
|
4914
|
+
] }),
|
|
4915
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4916
|
+
designSystem.IconButton,
|
|
4917
|
+
{
|
|
4918
|
+
label: "Revoke session",
|
|
4919
|
+
onClick: () => setConfirmRevokeSessionId(String(session.id)),
|
|
4920
|
+
style: { flexShrink: 0 },
|
|
4921
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
|
|
4922
|
+
}
|
|
4923
|
+
)
|
|
4924
|
+
] }, session.documentId)) })
|
|
4448
4925
|
] })
|
|
4449
|
-
] }) })
|
|
4926
|
+
] }) }),
|
|
4450
4927
|
/* @__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
4928
|
/* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Memberships" }),
|
|
4452
4929
|
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 +4960,29 @@ function UserDetailDrawer({
|
|
|
4483
4960
|
onCancel: () => setConfirmRevokeAll(false)
|
|
4484
4961
|
}
|
|
4485
4962
|
),
|
|
4963
|
+
confirmRevokeSessionId && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4964
|
+
ConfirmDialog,
|
|
4965
|
+
{
|
|
4966
|
+
title: "Revoke session",
|
|
4967
|
+
message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
|
|
4968
|
+
confirmLabel: "Revoke",
|
|
4969
|
+
loading: revokeSessionMutation.isLoading,
|
|
4970
|
+
onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
|
|
4971
|
+
onCancel: () => setConfirmRevokeSessionId(null)
|
|
4972
|
+
}
|
|
4973
|
+
),
|
|
4974
|
+
confirmBan && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4975
|
+
ConfirmDialog,
|
|
4976
|
+
{
|
|
4977
|
+
title: "Ban user",
|
|
4978
|
+
message: banReason ? `Ban this user for the following reason: "${banReason}"? They will be prevented from signing in.` : "Are you sure you want to ban this user? They will be prevented from signing in.",
|
|
4979
|
+
confirmLabel: "Ban user",
|
|
4980
|
+
variant: "danger",
|
|
4981
|
+
loading: banMutation.isLoading,
|
|
4982
|
+
onConfirm: () => banMutation.mutate(),
|
|
4983
|
+
onCancel: () => setConfirmBan(false)
|
|
4984
|
+
}
|
|
4985
|
+
),
|
|
4486
4986
|
confirmUnban && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4487
4987
|
ConfirmDialog,
|
|
4488
4988
|
{
|
|
@@ -4713,6 +5213,7 @@ const Toolbar = styled__default.default.div`
|
|
|
4713
5213
|
`;
|
|
4714
5214
|
function UsersPage({ config }) {
|
|
4715
5215
|
const qc = reactQuery.useQueryClient();
|
|
5216
|
+
const { toggleNotification } = admin.useNotification();
|
|
4716
5217
|
const banEnabled = hasPlugin(config, "admin");
|
|
4717
5218
|
const emailVerificationEnabled = config.emailVerification.sendVerificationEmailEnabled;
|
|
4718
5219
|
const twoFactorEnabled = hasPlugin(config, "two-factor");
|
|
@@ -4744,6 +5245,13 @@ function UsersPage({ config }) {
|
|
|
4744
5245
|
setConfirmDelete(null);
|
|
4745
5246
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
4746
5247
|
qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
|
|
5248
|
+
toggleNotification({ type: "success", message: "User deleted" });
|
|
5249
|
+
},
|
|
5250
|
+
onError: (err) => {
|
|
5251
|
+
toggleNotification({
|
|
5252
|
+
type: "danger",
|
|
5253
|
+
message: err.message ?? "Failed to delete user"
|
|
5254
|
+
});
|
|
4747
5255
|
}
|
|
4748
5256
|
});
|
|
4749
5257
|
const deleteManyMutation = reactQuery.useMutation({
|
|
@@ -4756,11 +5264,21 @@ function UsersPage({ config }) {
|
|
|
4756
5264
|
throw new Error(result.error.message ?? "Delete failed");
|
|
4757
5265
|
return result.data;
|
|
4758
5266
|
},
|
|
4759
|
-
onSuccess: () => {
|
|
5267
|
+
onSuccess: (_data, userIds) => {
|
|
4760
5268
|
setConfirmDeleteMany(false);
|
|
4761
5269
|
setSelected(/* @__PURE__ */ new Set());
|
|
4762
5270
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
4763
5271
|
qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
|
|
5272
|
+
toggleNotification({
|
|
5273
|
+
type: "success",
|
|
5274
|
+
message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} deleted`
|
|
5275
|
+
});
|
|
5276
|
+
},
|
|
5277
|
+
onError: (err) => {
|
|
5278
|
+
toggleNotification({
|
|
5279
|
+
type: "danger",
|
|
5280
|
+
message: err.message ?? "Failed to delete users"
|
|
5281
|
+
});
|
|
4764
5282
|
}
|
|
4765
5283
|
});
|
|
4766
5284
|
const banManyMutation = reactQuery.useMutation({
|
|
@@ -4772,10 +5290,20 @@ function UsersPage({ config }) {
|
|
|
4772
5290
|
if (result.error) throw new Error(result.error.message ?? "Ban failed");
|
|
4773
5291
|
return result.data;
|
|
4774
5292
|
},
|
|
4775
|
-
onSuccess: () => {
|
|
5293
|
+
onSuccess: (_data, userIds) => {
|
|
4776
5294
|
setConfirmBanMany(false);
|
|
4777
5295
|
setSelected(/* @__PURE__ */ new Set());
|
|
4778
5296
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
5297
|
+
toggleNotification({
|
|
5298
|
+
type: "success",
|
|
5299
|
+
message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} banned`
|
|
5300
|
+
});
|
|
5301
|
+
},
|
|
5302
|
+
onError: (err) => {
|
|
5303
|
+
toggleNotification({
|
|
5304
|
+
type: "danger",
|
|
5305
|
+
message: err.message ?? "Failed to ban users"
|
|
5306
|
+
});
|
|
4779
5307
|
}
|
|
4780
5308
|
});
|
|
4781
5309
|
const toggleSelect = (id) => {
|
|
@@ -4954,14 +5482,28 @@ function UsersPage({ config }) {
|
|
|
4954
5482
|
user.id
|
|
4955
5483
|
)) })
|
|
4956
5484
|
] }) }),
|
|
4957
|
-
pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
5485
|
+
pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
|
|
5486
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5487
|
+
designSystem.Button,
|
|
5488
|
+
{
|
|
5489
|
+
variant: "tertiary",
|
|
5490
|
+
size: "S",
|
|
5491
|
+
disabled: page === 1,
|
|
5492
|
+
onClick: () => setPage((p) => p - 1),
|
|
5493
|
+
children: "Previous"
|
|
5494
|
+
}
|
|
5495
|
+
),
|
|
5496
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5497
|
+
designSystem.Button,
|
|
5498
|
+
{
|
|
5499
|
+
variant: "tertiary",
|
|
5500
|
+
size: "S",
|
|
5501
|
+
disabled: page >= pageCount,
|
|
5502
|
+
onClick: () => setPage((p) => p + 1),
|
|
5503
|
+
children: "Next"
|
|
5504
|
+
}
|
|
5505
|
+
)
|
|
5506
|
+
] }) }),
|
|
4965
5507
|
showCreate && /* @__PURE__ */ jsxRuntime.jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
|
|
4966
5508
|
detailUserId && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4967
5509
|
UserDetailDrawer,
|
|
@@ -5017,8 +5559,7 @@ const Accent = styled__default.default.div`
|
|
|
5017
5559
|
const BrandIcon = styled__default.default.div`
|
|
5018
5560
|
width: 30px;
|
|
5019
5561
|
height: 30px;
|
|
5020
|
-
|
|
5021
|
-
background: linear-gradient(135deg, #4945ff 0%, #7b79ff 100%);
|
|
5562
|
+
background: black;
|
|
5022
5563
|
display: flex;
|
|
5023
5564
|
align-items: center;
|
|
5024
5565
|
justify-content: center;
|
|
@@ -5094,7 +5635,7 @@ function App() {
|
|
|
5094
5635
|
paddingBottom: 4,
|
|
5095
5636
|
children: [
|
|
5096
5637
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
5097
|
-
/* @__PURE__ */ jsxRuntime.jsx(BrandIcon, { children:
|
|
5638
|
+
/* @__PURE__ */ jsxRuntime.jsx(BrandIcon, { children: /* @__PURE__ */ jsxRuntime.jsx(index.PluginIcon, {}) }),
|
|
5098
5639
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
|
|
5099
5640
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", textColor: "neutral800", children: "Better Auth" }),
|
|
5100
5641
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: "2px", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Authentication Dashboard" }) })
|
|
@@ -5114,8 +5655,7 @@ function App() {
|
|
|
5114
5655
|
"data-testid": "nav-organizations",
|
|
5115
5656
|
children: "Organizations"
|
|
5116
5657
|
}
|
|
5117
|
-
)
|
|
5118
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Trigger, { value: "sessions", "data-testid": "nav-sessions", children: "Sessions" })
|
|
5658
|
+
)
|
|
5119
5659
|
] })
|
|
5120
5660
|
]
|
|
5121
5661
|
}
|
|
@@ -5125,8 +5665,7 @@ function App() {
|
|
|
5125
5665
|
),
|
|
5126
5666
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "overview", "data-testid": "tab-overview", children: /* @__PURE__ */ jsxRuntime.jsx(OverviewPage, {}) }),
|
|
5127
5667
|
/* @__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, {}) })
|
|
5668
|
+
orgEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsxRuntime.jsx(OrganizationsPage, { teamsEnabled }) })
|
|
5130
5669
|
] }) });
|
|
5131
5670
|
}
|
|
5132
5671
|
function Root() {
|