@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
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState, useMemo } from "react";
|
|
2
|
+
import { useEffect, useState, useRef, useMemo } from "react";
|
|
3
3
|
import { useQuery, useQueryClient, useMutation, QueryClient, QueryClientProvider } from "react-query";
|
|
4
|
-
import { Modal, Typography, Button, Portal, Flex, IconButton, Box, Field, TextInput, Combobox, ComboboxOption, Checkbox, SingleSelect, SingleSelectOption, NumberInput, JSONInput, Textarea, Alert, Tabs, Grid, SearchForm, Searchbar, Loader, Badge
|
|
4
|
+
import { Modal, Typography, Button, Portal, Flex, IconButton, Box, Field, TextInput, Combobox, ComboboxOption, Checkbox, SingleSelect, SingleSelectOption, NumberInput, JSONInput, Textarea, Alert, Tabs, Grid, SearchForm, Searchbar, Loader, Badge } from "@strapi/design-system";
|
|
5
5
|
import styled, { keyframes } from "styled-components";
|
|
6
6
|
import { dashClient } from "@better-auth/infra/client";
|
|
7
7
|
import { createAuthClient, InferPlugin } from "better-auth/client";
|
|
8
|
-
import { Cross, Images,
|
|
8
|
+
import { Cross, Images, Trash, Plus, Pencil } from "@strapi/icons";
|
|
9
9
|
import { useNotification, useFetchClient } from "@strapi/strapi/admin";
|
|
10
|
-
import { g as getMediaLibraryComponent } from "./index-
|
|
10
|
+
import { g as getMediaLibraryComponent, P as PluginIcon } from "./index-QVlTR8eL.mjs";
|
|
11
11
|
const dashPathMethods = () => ({
|
|
12
12
|
id: "dash-path-methods",
|
|
13
13
|
pathMethods: {
|
|
@@ -454,7 +454,7 @@ function UserCombobox({
|
|
|
454
454
|
]) : void 0;
|
|
455
455
|
const result = await client.dash.listUsers({
|
|
456
456
|
query: {
|
|
457
|
-
limit: 20,
|
|
457
|
+
limit: search ? 100 : 20,
|
|
458
458
|
offset: 0,
|
|
459
459
|
sortBy: "createdAt",
|
|
460
460
|
sortOrder: "desc",
|
|
@@ -629,6 +629,159 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
|
|
|
629
629
|
function slugify(str) {
|
|
630
630
|
return str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
631
631
|
}
|
|
632
|
+
const EntryRow = styled.div`
|
|
633
|
+
display: flex;
|
|
634
|
+
align-items: center;
|
|
635
|
+
justify-content: space-between;
|
|
636
|
+
gap: 8px;
|
|
637
|
+
padding: 8px 12px;
|
|
638
|
+
width: 100%;
|
|
639
|
+
box-sizing: border-box;
|
|
640
|
+
background: #ffffff;
|
|
641
|
+
border: 1px solid ${({ theme }) => theme.colors?.neutral200 ?? "#dcdce4"};
|
|
642
|
+
border-radius: 4px;
|
|
643
|
+
min-width: 0;
|
|
644
|
+
`;
|
|
645
|
+
function getDisplayLabel(doc) {
|
|
646
|
+
for (const key of ["name", "title", "email", "username", "label", "slug"]) {
|
|
647
|
+
const v = doc[key];
|
|
648
|
+
if (typeof v === "string" && v.trim()) return v;
|
|
649
|
+
}
|
|
650
|
+
return String(doc.documentId ?? doc.id ?? "");
|
|
651
|
+
}
|
|
652
|
+
function normalizeValue(val, cache) {
|
|
653
|
+
if (!val) return [];
|
|
654
|
+
if (typeof val === "object" && !Array.isArray(val) && val !== null && "set" in val) {
|
|
655
|
+
const items2 = val.set ?? [];
|
|
656
|
+
return items2.filter((item) => item?.documentId).map((item) => ({
|
|
657
|
+
documentId: item.documentId,
|
|
658
|
+
label: cache.get(item.documentId) ?? item.documentId
|
|
659
|
+
}));
|
|
660
|
+
}
|
|
661
|
+
const items = Array.isArray(val) ? val : [val];
|
|
662
|
+
return items.filter(
|
|
663
|
+
(item) => typeof item === "object" && item !== null && "documentId" in item
|
|
664
|
+
).map((item) => ({
|
|
665
|
+
documentId: String(item.documentId),
|
|
666
|
+
label: getDisplayLabel(item)
|
|
667
|
+
}));
|
|
668
|
+
}
|
|
669
|
+
function RelationField({
|
|
670
|
+
name,
|
|
671
|
+
label,
|
|
672
|
+
attribute,
|
|
673
|
+
value,
|
|
674
|
+
onChange,
|
|
675
|
+
readOnly = false
|
|
676
|
+
}) {
|
|
677
|
+
const { get } = useFetchClient();
|
|
678
|
+
const cache = useRef(/* @__PURE__ */ new Map());
|
|
679
|
+
const [search, setSearch] = useState("");
|
|
680
|
+
const [addKey, setAddKey] = useState(0);
|
|
681
|
+
const target = attribute.target ?? "";
|
|
682
|
+
const relationType = attribute.relation ?? "manyToOne";
|
|
683
|
+
const isMulti = relationType === "oneToMany" || relationType === "manyToMany";
|
|
684
|
+
const currentDocs = normalizeValue(value, cache.current);
|
|
685
|
+
const selectedIds = new Set(currentDocs.map((d) => d.documentId));
|
|
686
|
+
const { data: searchResults = [], isLoading } = useQuery({
|
|
687
|
+
queryKey: ["relation-search", target, search],
|
|
688
|
+
queryFn: async () => {
|
|
689
|
+
const params = new URLSearchParams({
|
|
690
|
+
uid: target,
|
|
691
|
+
"pagination[pageSize]": "50"
|
|
692
|
+
});
|
|
693
|
+
if (search) params.set("filters[name][$containsi]", search);
|
|
694
|
+
const { data } = await get(
|
|
695
|
+
`/better-auth-dashboard/db?${params}`
|
|
696
|
+
);
|
|
697
|
+
const docs = data.results ?? [];
|
|
698
|
+
for (const doc of docs) {
|
|
699
|
+
if (doc.documentId) {
|
|
700
|
+
cache.current.set(String(doc.documentId), getDisplayLabel(doc));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return docs.map((doc) => ({
|
|
704
|
+
documentId: String(doc.documentId ?? ""),
|
|
705
|
+
label: getDisplayLabel(doc)
|
|
706
|
+
})).filter((d) => d.documentId);
|
|
707
|
+
},
|
|
708
|
+
keepPreviousData: true,
|
|
709
|
+
enabled: !readOnly
|
|
710
|
+
});
|
|
711
|
+
const availableOptions = searchResults.filter(
|
|
712
|
+
(r) => !selectedIds.has(r.documentId)
|
|
713
|
+
);
|
|
714
|
+
const commit = (ids) => {
|
|
715
|
+
onChange(name, { set: ids.map((id) => ({ documentId: id })) });
|
|
716
|
+
};
|
|
717
|
+
const handleSelect = (val) => {
|
|
718
|
+
if (!val) return;
|
|
719
|
+
const opt = searchResults.find((r) => r.documentId === val);
|
|
720
|
+
if (opt) cache.current.set(val, opt.label);
|
|
721
|
+
if (isMulti) {
|
|
722
|
+
if (selectedIds.has(val)) return;
|
|
723
|
+
commit([...currentDocs.map((d) => d.documentId), val]);
|
|
724
|
+
} else {
|
|
725
|
+
commit([val]);
|
|
726
|
+
}
|
|
727
|
+
setSearch("");
|
|
728
|
+
setAddKey((k) => k + 1);
|
|
729
|
+
};
|
|
730
|
+
const handleRemove = (docId) => {
|
|
731
|
+
commit(
|
|
732
|
+
currentDocs.filter((d) => d.documentId !== docId).map((d) => d.documentId)
|
|
733
|
+
);
|
|
734
|
+
};
|
|
735
|
+
return /* @__PURE__ */ jsxs(Field.Root, { style: { width: "100%" }, children: [
|
|
736
|
+
/* @__PURE__ */ jsx(Field.Label, { children: label }),
|
|
737
|
+
!readOnly && /* @__PURE__ */ jsx(
|
|
738
|
+
Combobox,
|
|
739
|
+
{
|
|
740
|
+
value: "",
|
|
741
|
+
onChange: (val) => handleSelect(val),
|
|
742
|
+
onInputChange: (e) => setSearch(e.target.value),
|
|
743
|
+
loading: isLoading,
|
|
744
|
+
placeholder: "Search…",
|
|
745
|
+
children: availableOptions.map((opt) => /* @__PURE__ */ jsx(ComboboxOption, { value: opt.documentId, children: opt.label }, opt.documentId))
|
|
746
|
+
},
|
|
747
|
+
addKey
|
|
748
|
+
),
|
|
749
|
+
currentDocs.length > 0 && /* @__PURE__ */ jsx(
|
|
750
|
+
Flex,
|
|
751
|
+
{
|
|
752
|
+
direction: "column",
|
|
753
|
+
gap: 1,
|
|
754
|
+
style: { marginTop: 8, width: "100%" },
|
|
755
|
+
children: currentDocs.map((doc) => /* @__PURE__ */ jsxs(EntryRow, { children: [
|
|
756
|
+
/* @__PURE__ */ jsx(
|
|
757
|
+
Typography,
|
|
758
|
+
{
|
|
759
|
+
variant: "omega",
|
|
760
|
+
textColor: "neutral800",
|
|
761
|
+
style: {
|
|
762
|
+
overflow: "hidden",
|
|
763
|
+
textOverflow: "ellipsis",
|
|
764
|
+
whiteSpace: "nowrap",
|
|
765
|
+
flex: 1,
|
|
766
|
+
minWidth: 0
|
|
767
|
+
},
|
|
768
|
+
children: doc.label
|
|
769
|
+
}
|
|
770
|
+
),
|
|
771
|
+
!readOnly && /* @__PURE__ */ jsx(
|
|
772
|
+
IconButton,
|
|
773
|
+
{
|
|
774
|
+
label: "Remove",
|
|
775
|
+
size: "S",
|
|
776
|
+
onClick: () => handleRemove(doc.documentId),
|
|
777
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
778
|
+
}
|
|
779
|
+
)
|
|
780
|
+
] }, doc.documentId))
|
|
781
|
+
}
|
|
782
|
+
)
|
|
783
|
+
] });
|
|
784
|
+
}
|
|
632
785
|
function makeLabel(name) {
|
|
633
786
|
return name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
634
787
|
}
|
|
@@ -752,24 +905,15 @@ function DynamicField({
|
|
|
752
905
|
] });
|
|
753
906
|
}
|
|
754
907
|
if (type === "relation") {
|
|
755
|
-
return /* @__PURE__ */
|
|
756
|
-
|
|
908
|
+
return /* @__PURE__ */ jsx(
|
|
909
|
+
RelationField,
|
|
757
910
|
{
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
{
|
|
765
|
-
value: value != null ? String(value) : "",
|
|
766
|
-
onChange: (e) => onChange(name, e.target.value),
|
|
767
|
-
disabled: readOnly,
|
|
768
|
-
placeholder: "ID…"
|
|
769
|
-
}
|
|
770
|
-
),
|
|
771
|
-
/* @__PURE__ */ jsx(Field.Hint, {})
|
|
772
|
-
]
|
|
911
|
+
name,
|
|
912
|
+
label,
|
|
913
|
+
attribute,
|
|
914
|
+
value,
|
|
915
|
+
onChange,
|
|
916
|
+
readOnly
|
|
773
917
|
}
|
|
774
918
|
);
|
|
775
919
|
}
|
|
@@ -840,6 +984,8 @@ function useModelSchema(model) {
|
|
|
840
984
|
if (SYSTEM_FIELDS.has(name)) continue;
|
|
841
985
|
if (attr.type === "relation" && typeof attr.target === "string" && (attr.target.startsWith("plugin::users-permissions") || attr.target.startsWith("admin::")))
|
|
842
986
|
continue;
|
|
987
|
+
const baOptions = attr.pluginOptions?.["better-auth"];
|
|
988
|
+
if (baOptions?.managed === true) continue;
|
|
843
989
|
attributes[name] = attr;
|
|
844
990
|
}
|
|
845
991
|
return attributes;
|
|
@@ -892,6 +1038,7 @@ function OrganizationDetail({
|
|
|
892
1038
|
const schemaQuery = useModelSchema("organization");
|
|
893
1039
|
const qc = useQueryClient();
|
|
894
1040
|
const { toggleNotification } = useNotification();
|
|
1041
|
+
const { get, put } = useFetchClient();
|
|
895
1042
|
const orgQuery = useQuery({
|
|
896
1043
|
queryKey: ["dash-org", organizationId],
|
|
897
1044
|
queryFn: async () => {
|
|
@@ -930,6 +1077,24 @@ function OrganizationDetail({
|
|
|
930
1077
|
return result.data ?? [];
|
|
931
1078
|
}
|
|
932
1079
|
});
|
|
1080
|
+
const invitationsQuery = useQuery({
|
|
1081
|
+
queryKey: ["dash-org-invitations", organizationId],
|
|
1082
|
+
queryFn: async () => {
|
|
1083
|
+
const result = await client.dash.organization[organizationId].invitations({}, withContext({ organizationId }));
|
|
1084
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1085
|
+
return result.data ?? [];
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
const strapiOrgQuery = useQuery({
|
|
1089
|
+
queryKey: ["dash-strapi-org", organizationId],
|
|
1090
|
+
enabled: !!orgQuery.data,
|
|
1091
|
+
queryFn: async () => {
|
|
1092
|
+
const { data } = await get(
|
|
1093
|
+
`/better-auth-dashboard/db?uid=plugin::better-auth.organization&filters[id][$eq]=${organizationId}&pagination[pageSize]=1`
|
|
1094
|
+
);
|
|
1095
|
+
return data.results?.[0] ?? null;
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
933
1098
|
const [activeTab, setActiveTab] = useState("details");
|
|
934
1099
|
const [editName, setEditName] = useState(void 0);
|
|
935
1100
|
const [editSlug, setEditSlug] = useState(void 0);
|
|
@@ -942,6 +1107,9 @@ function OrganizationDetail({
|
|
|
942
1107
|
const [confirmDeleteSsoId, setConfirmDeleteSsoId] = useState(
|
|
943
1108
|
null
|
|
944
1109
|
);
|
|
1110
|
+
const [inviteEmail, setInviteEmail] = useState("");
|
|
1111
|
+
const [inviteRole, setInviteRole] = useState("member");
|
|
1112
|
+
const [confirmCancelInvitationId, setConfirmCancelInvitationId] = useState(null);
|
|
945
1113
|
const handleExtraChange = (name, value) => {
|
|
946
1114
|
setEditExtra((prev) => ({ ...prev, [name]: value }));
|
|
947
1115
|
};
|
|
@@ -956,13 +1124,13 @@ function OrganizationDetail({
|
|
|
956
1124
|
if (editName !== void 0) body.name = editName;
|
|
957
1125
|
if (editSlug !== void 0) body.slug = editSlug;
|
|
958
1126
|
if (editLogo !== void 0) body.logo = editLogo;
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
1127
|
+
const documentId = strapiOrgQuery.data?.documentId;
|
|
1128
|
+
if (!documentId)
|
|
1129
|
+
throw new Error("Could not resolve documentId for organization");
|
|
1130
|
+
await put(
|
|
1131
|
+
`/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.organization`,
|
|
1132
|
+
body
|
|
962
1133
|
);
|
|
963
|
-
if (result.error)
|
|
964
|
-
throw new Error(result.error.message ?? "Update failed");
|
|
965
|
-
return result.data;
|
|
966
1134
|
},
|
|
967
1135
|
onSuccess: () => {
|
|
968
1136
|
qc.invalidateQueries({ queryKey: ["dash-org", organizationId] });
|
|
@@ -1112,9 +1280,74 @@ function OrganizationDetail({
|
|
|
1112
1280
|
});
|
|
1113
1281
|
}
|
|
1114
1282
|
});
|
|
1283
|
+
const inviteMemberMutation = useMutation({
|
|
1284
|
+
mutationFn: async () => {
|
|
1285
|
+
const result = await client.dash.organization.inviteMember(
|
|
1286
|
+
{ email: inviteEmail, role: inviteRole, invitedBy: "" },
|
|
1287
|
+
withContext({ organizationId })
|
|
1288
|
+
);
|
|
1289
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1290
|
+
return result.data;
|
|
1291
|
+
},
|
|
1292
|
+
onSuccess: () => {
|
|
1293
|
+
qc.invalidateQueries({
|
|
1294
|
+
queryKey: ["dash-org-invitations", organizationId]
|
|
1295
|
+
});
|
|
1296
|
+
setInviteEmail("");
|
|
1297
|
+
setInviteRole("member");
|
|
1298
|
+
toggleNotification({ type: "success", message: "Invitation sent" });
|
|
1299
|
+
},
|
|
1300
|
+
onError: (err) => {
|
|
1301
|
+
toggleNotification({
|
|
1302
|
+
type: "danger",
|
|
1303
|
+
message: err.message ?? "Failed to invite"
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
const cancelInvitationMutation = useMutation({
|
|
1308
|
+
mutationFn: async (invitationId) => {
|
|
1309
|
+
const result = await client.dash.organization.cancelInvitation(
|
|
1310
|
+
{ invitationId },
|
|
1311
|
+
withContext({ organizationId })
|
|
1312
|
+
);
|
|
1313
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1314
|
+
},
|
|
1315
|
+
onSuccess: () => {
|
|
1316
|
+
setConfirmCancelInvitationId(null);
|
|
1317
|
+
qc.invalidateQueries({
|
|
1318
|
+
queryKey: ["dash-org-invitations", organizationId]
|
|
1319
|
+
});
|
|
1320
|
+
toggleNotification({ type: "success", message: "Invitation cancelled" });
|
|
1321
|
+
},
|
|
1322
|
+
onError: (err) => {
|
|
1323
|
+
toggleNotification({
|
|
1324
|
+
type: "danger",
|
|
1325
|
+
message: err.message ?? "Failed to cancel"
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
const resendInvitationMutation = useMutation({
|
|
1330
|
+
mutationFn: async (invitationId) => {
|
|
1331
|
+
const result = await client.dash.organization.resendInvitation(
|
|
1332
|
+
{ invitationId },
|
|
1333
|
+
withContext({ organizationId })
|
|
1334
|
+
);
|
|
1335
|
+
if (result.error) throw new Error(result.error.message ?? "Failed");
|
|
1336
|
+
},
|
|
1337
|
+
onSuccess: () => {
|
|
1338
|
+
toggleNotification({ type: "success", message: "Invitation resent" });
|
|
1339
|
+
},
|
|
1340
|
+
onError: (err) => {
|
|
1341
|
+
toggleNotification({
|
|
1342
|
+
type: "danger",
|
|
1343
|
+
message: err.message ?? "Failed to resend"
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1115
1347
|
const members = membersQuery.data ?? [];
|
|
1116
1348
|
const teams = teamsQuery.data ?? [];
|
|
1117
1349
|
const ssoProviders = ssoQuery.data ?? [];
|
|
1350
|
+
const invitations = invitationsQuery.data ?? [];
|
|
1118
1351
|
const hasOrgEdits = editName !== void 0 || editSlug !== void 0 || editLogo !== void 0 || Object.keys(editExtra).length > 0;
|
|
1119
1352
|
const customFields = Object.entries(schemaQuery.data ?? {}).filter(([name]) => !STANDARD_ORG_FIELDS.has(name)).map(([name, attribute]) => ({ name, attribute }));
|
|
1120
1353
|
const detailsFooter = activeTab === "details" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -1171,6 +1404,11 @@ function OrganizationDetail({
|
|
|
1171
1404
|
teams.length,
|
|
1172
1405
|
")"
|
|
1173
1406
|
] }),
|
|
1407
|
+
/* @__PURE__ */ jsxs(Tabs.Trigger, { value: "invitations", children: [
|
|
1408
|
+
"Invitations (",
|
|
1409
|
+
invitations.length,
|
|
1410
|
+
")"
|
|
1411
|
+
] }),
|
|
1174
1412
|
/* @__PURE__ */ jsxs(Tabs.Trigger, { value: "sso", children: [
|
|
1175
1413
|
"SSO (",
|
|
1176
1414
|
ssoProviders.length,
|
|
@@ -1426,7 +1664,127 @@ function OrganizationDetail({
|
|
|
1426
1664
|
},
|
|
1427
1665
|
provider.id
|
|
1428
1666
|
)) })
|
|
1429
|
-
] }) }) })
|
|
1667
|
+
] }) }) }),
|
|
1668
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "invitations", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
|
|
1669
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
1670
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Invite member by email" }),
|
|
1671
|
+
/* @__PURE__ */ jsxs(Grid.Root, { gap: 3, children: [
|
|
1672
|
+
/* @__PURE__ */ jsx(Grid.Item, { col: 8, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
|
|
1673
|
+
/* @__PURE__ */ jsx(Field.Label, { children: "Email address" }),
|
|
1674
|
+
/* @__PURE__ */ jsx(
|
|
1675
|
+
TextInput,
|
|
1676
|
+
{
|
|
1677
|
+
type: "email",
|
|
1678
|
+
value: inviteEmail,
|
|
1679
|
+
onChange: (e) => setInviteEmail(e.target.value),
|
|
1680
|
+
placeholder: "user@example.com"
|
|
1681
|
+
}
|
|
1682
|
+
)
|
|
1683
|
+
] }) }),
|
|
1684
|
+
/* @__PURE__ */ jsx(Grid.Item, { col: 4, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
|
|
1685
|
+
/* @__PURE__ */ jsx(Field.Label, { children: "Role" }),
|
|
1686
|
+
/* @__PURE__ */ jsxs(
|
|
1687
|
+
SingleSelect,
|
|
1688
|
+
{
|
|
1689
|
+
value: inviteRole,
|
|
1690
|
+
onChange: (v) => setInviteRole(String(v)),
|
|
1691
|
+
"aria-label": "Invite role",
|
|
1692
|
+
children: [
|
|
1693
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "member", children: "Member" }),
|
|
1694
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "admin", children: "Admin" }),
|
|
1695
|
+
/* @__PURE__ */ jsx(SingleSelectOption, { value: "owner", children: "Owner" })
|
|
1696
|
+
]
|
|
1697
|
+
}
|
|
1698
|
+
)
|
|
1699
|
+
] }) })
|
|
1700
|
+
] }),
|
|
1701
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
|
|
1702
|
+
Button,
|
|
1703
|
+
{
|
|
1704
|
+
size: "S",
|
|
1705
|
+
startIcon: /* @__PURE__ */ jsx(Plus, {}),
|
|
1706
|
+
disabled: !inviteEmail,
|
|
1707
|
+
loading: inviteMemberMutation.isLoading,
|
|
1708
|
+
onClick: () => inviteMemberMutation.mutate(),
|
|
1709
|
+
children: "Send invitation"
|
|
1710
|
+
}
|
|
1711
|
+
) })
|
|
1712
|
+
] }),
|
|
1713
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
1714
|
+
/* @__PURE__ */ jsxs(SectionLabel, { children: [
|
|
1715
|
+
"Invitations (",
|
|
1716
|
+
invitations.length,
|
|
1717
|
+
")"
|
|
1718
|
+
] }),
|
|
1719
|
+
invitationsQuery.isLoading ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral500", children: "Loading…" }) : invitations.length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "No invitations yet." }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: invitations.map((inv) => {
|
|
1720
|
+
const statusColor = {
|
|
1721
|
+
pending: "#f59e0b",
|
|
1722
|
+
accepted: "#5cb176",
|
|
1723
|
+
rejected: "#d02b20",
|
|
1724
|
+
canceled: "#8e8ea9"
|
|
1725
|
+
};
|
|
1726
|
+
const color = statusColor[inv.status] ?? "#8e8ea9";
|
|
1727
|
+
const isPending = inv.status === "pending";
|
|
1728
|
+
return /* @__PURE__ */ jsxs(AccountRow, { children: [
|
|
1729
|
+
/* @__PURE__ */ jsxs(
|
|
1730
|
+
Flex,
|
|
1731
|
+
{
|
|
1732
|
+
direction: "column",
|
|
1733
|
+
gap: 1,
|
|
1734
|
+
alignItems: "flex-start",
|
|
1735
|
+
style: { flex: 1 },
|
|
1736
|
+
children: [
|
|
1737
|
+
/* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: inv.user?.name ?? inv.email }),
|
|
1738
|
+
/* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
|
|
1739
|
+
inv.email,
|
|
1740
|
+
" · role: ",
|
|
1741
|
+
inv.role
|
|
1742
|
+
] }),
|
|
1743
|
+
/* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
|
|
1744
|
+
"Expires",
|
|
1745
|
+
" ",
|
|
1746
|
+
new Date(inv.expiresAt).toLocaleDateString()
|
|
1747
|
+
] })
|
|
1748
|
+
]
|
|
1749
|
+
}
|
|
1750
|
+
),
|
|
1751
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
|
|
1752
|
+
/* @__PURE__ */ jsx(
|
|
1753
|
+
MonoChip,
|
|
1754
|
+
{
|
|
1755
|
+
style: {
|
|
1756
|
+
color,
|
|
1757
|
+
borderColor: color,
|
|
1758
|
+
background: `${color}18`
|
|
1759
|
+
},
|
|
1760
|
+
children: inv.status
|
|
1761
|
+
}
|
|
1762
|
+
),
|
|
1763
|
+
isPending && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1764
|
+
/* @__PURE__ */ jsx(
|
|
1765
|
+
Button,
|
|
1766
|
+
{
|
|
1767
|
+
size: "S",
|
|
1768
|
+
variant: "secondary",
|
|
1769
|
+
loading: resendInvitationMutation.isLoading,
|
|
1770
|
+
onClick: () => resendInvitationMutation.mutate(inv.id),
|
|
1771
|
+
children: "Resend"
|
|
1772
|
+
}
|
|
1773
|
+
),
|
|
1774
|
+
/* @__PURE__ */ jsx(
|
|
1775
|
+
IconButton,
|
|
1776
|
+
{
|
|
1777
|
+
label: "Cancel invitation",
|
|
1778
|
+
onClick: () => setConfirmCancelInvitationId(inv.id),
|
|
1779
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
1780
|
+
}
|
|
1781
|
+
)
|
|
1782
|
+
] })
|
|
1783
|
+
] })
|
|
1784
|
+
] }, inv.id);
|
|
1785
|
+
}) })
|
|
1786
|
+
] })
|
|
1787
|
+
] }) })
|
|
1430
1788
|
] }),
|
|
1431
1789
|
confirmRemoveMemberId && /* @__PURE__ */ jsx(
|
|
1432
1790
|
ConfirmDialog,
|
|
@@ -1460,6 +1818,17 @@ function OrganizationDetail({
|
|
|
1460
1818
|
onConfirm: () => deleteSsoMutation.mutate(confirmDeleteSsoId),
|
|
1461
1819
|
onCancel: () => setConfirmDeleteSsoId(null)
|
|
1462
1820
|
}
|
|
1821
|
+
),
|
|
1822
|
+
confirmCancelInvitationId && /* @__PURE__ */ jsx(
|
|
1823
|
+
ConfirmDialog,
|
|
1824
|
+
{
|
|
1825
|
+
title: "Cancel invitation",
|
|
1826
|
+
message: "Are you sure you want to cancel this invitation? The invite link will no longer work.",
|
|
1827
|
+
confirmLabel: "Cancel invitation",
|
|
1828
|
+
loading: cancelInvitationMutation.isLoading,
|
|
1829
|
+
onConfirm: () => cancelInvitationMutation.mutate(confirmCancelInvitationId),
|
|
1830
|
+
onCancel: () => setConfirmCancelInvitationId(null)
|
|
1831
|
+
}
|
|
1463
1832
|
)
|
|
1464
1833
|
]
|
|
1465
1834
|
}
|
|
@@ -1605,12 +1974,12 @@ function TeamRow({
|
|
|
1605
1974
|
)
|
|
1606
1975
|
] });
|
|
1607
1976
|
}
|
|
1608
|
-
const PAGE_SIZE$
|
|
1609
|
-
const fadeUp$
|
|
1977
|
+
const PAGE_SIZE$1 = 25;
|
|
1978
|
+
const fadeUp$2 = keyframes`
|
|
1610
1979
|
from { opacity: 0; transform: translateY(6px); }
|
|
1611
1980
|
to { opacity: 1; transform: translateY(0); }
|
|
1612
1981
|
`;
|
|
1613
|
-
const Wrap$
|
|
1982
|
+
const Wrap$2 = styled.div`
|
|
1614
1983
|
padding: 28px 32px;
|
|
1615
1984
|
background: #f6f6f9;
|
|
1616
1985
|
min-height: 100%;
|
|
@@ -1618,39 +1987,39 @@ const Wrap$3 = styled.div`
|
|
|
1618
1987
|
flex-direction: column;
|
|
1619
1988
|
gap: 20px;
|
|
1620
1989
|
`;
|
|
1621
|
-
const PageHeader$
|
|
1990
|
+
const PageHeader$1 = styled.div`
|
|
1622
1991
|
display: flex;
|
|
1623
1992
|
justify-content: space-between;
|
|
1624
1993
|
align-items: flex-start;
|
|
1625
1994
|
`;
|
|
1626
|
-
const TitleBlock$
|
|
1995
|
+
const TitleBlock$1 = styled.div`
|
|
1627
1996
|
display: flex;
|
|
1628
1997
|
flex-direction: column;
|
|
1629
1998
|
gap: 4px;
|
|
1630
1999
|
`;
|
|
1631
|
-
const PageTitle$
|
|
2000
|
+
const PageTitle$1 = styled.h1`
|
|
1632
2001
|
margin: 0;
|
|
1633
2002
|
font-size: 22px;
|
|
1634
2003
|
font-weight: 800;
|
|
1635
2004
|
color: #32324d;
|
|
1636
2005
|
letter-spacing: -0.03em;
|
|
1637
2006
|
`;
|
|
1638
|
-
const PageSubtitle$
|
|
2007
|
+
const PageSubtitle$1 = styled.p`
|
|
1639
2008
|
margin: 0;
|
|
1640
2009
|
font-size: 12px;
|
|
1641
2010
|
color: #8e8ea9;
|
|
1642
2011
|
`;
|
|
1643
|
-
const TableCard$
|
|
2012
|
+
const TableCard$1 = styled.div`
|
|
1644
2013
|
background: #ffffff;
|
|
1645
2014
|
border: 1px solid #eaeaef;
|
|
1646
2015
|
border-radius: 10px;
|
|
1647
2016
|
overflow: hidden;
|
|
1648
2017
|
`;
|
|
1649
|
-
const Table$
|
|
2018
|
+
const Table$1 = styled.table`
|
|
1650
2019
|
width: 100%;
|
|
1651
2020
|
border-collapse: collapse;
|
|
1652
2021
|
`;
|
|
1653
|
-
const TH$
|
|
2022
|
+
const TH$1 = styled.th`
|
|
1654
2023
|
text-align: left;
|
|
1655
2024
|
padding: 10px 14px;
|
|
1656
2025
|
font-size: 10px;
|
|
@@ -1664,13 +2033,13 @@ const TH$2 = styled.th`
|
|
|
1664
2033
|
&:first-child { padding-left: 20px; }
|
|
1665
2034
|
&:last-child { padding-right: 20px; }
|
|
1666
2035
|
`;
|
|
1667
|
-
const THCheck$1 = styled(TH$
|
|
2036
|
+
const THCheck$1 = styled(TH$1)`
|
|
1668
2037
|
width: 44px;
|
|
1669
2038
|
`;
|
|
1670
|
-
const THActions$1 = styled(TH$
|
|
2039
|
+
const THActions$1 = styled(TH$1)`
|
|
1671
2040
|
width: 80px;
|
|
1672
2041
|
`;
|
|
1673
|
-
const TD$
|
|
2042
|
+
const TD$1 = styled.td`
|
|
1674
2043
|
padding: 11px 14px;
|
|
1675
2044
|
font-size: 12px;
|
|
1676
2045
|
color: #32324d;
|
|
@@ -1679,14 +2048,14 @@ const TD$2 = styled.td`
|
|
|
1679
2048
|
&:first-child { padding-left: 20px; }
|
|
1680
2049
|
&:last-child { padding-right: 20px; }
|
|
1681
2050
|
`;
|
|
1682
|
-
const TDCheck$1 = styled(TD$
|
|
2051
|
+
const TDCheck$1 = styled(TD$1)`
|
|
1683
2052
|
width: 44px;
|
|
1684
2053
|
`;
|
|
1685
|
-
const TDActions$1 = styled(TD$
|
|
2054
|
+
const TDActions$1 = styled(TD$1)`
|
|
1686
2055
|
width: 80px;
|
|
1687
2056
|
`;
|
|
1688
|
-
const TR$
|
|
1689
|
-
animation: ${fadeUp$
|
|
2057
|
+
const TR$1 = styled.tr`
|
|
2058
|
+
animation: ${fadeUp$2} 280ms ease both;
|
|
1690
2059
|
animation-delay: ${(p) => (p.$i ?? 0) * 25}ms;
|
|
1691
2060
|
background: ${(p) => p.$selected ? "#f0f0ff" : "transparent"};
|
|
1692
2061
|
transition: background 120ms ease;
|
|
@@ -1774,6 +2143,7 @@ function OrgAvatar({ name, logo }) {
|
|
|
1774
2143
|
}
|
|
1775
2144
|
function OrganizationsPage({ teamsEnabled }) {
|
|
1776
2145
|
const qc = useQueryClient();
|
|
2146
|
+
const { toggleNotification } = useNotification();
|
|
1777
2147
|
const [page, setPage] = useState(1);
|
|
1778
2148
|
const [search, setSearch] = useState("");
|
|
1779
2149
|
const [searchInput, setSearchInput] = useState("");
|
|
@@ -1782,13 +2152,13 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1782
2152
|
const [detailOrgId, setDetailOrgId] = useState(null);
|
|
1783
2153
|
const [confirmDelete, setConfirmDelete] = useState(null);
|
|
1784
2154
|
const [confirmDeleteMany, setConfirmDeleteMany] = useState(false);
|
|
1785
|
-
const offset = (page - 1) * PAGE_SIZE$
|
|
2155
|
+
const offset = (page - 1) * PAGE_SIZE$1;
|
|
1786
2156
|
const orgsQuery = useQuery({
|
|
1787
2157
|
queryKey: ["dash-organizations", page, search],
|
|
1788
2158
|
queryFn: async () => {
|
|
1789
2159
|
const result = await client.dash.listOrganizations({
|
|
1790
2160
|
query: {
|
|
1791
|
-
limit: PAGE_SIZE$
|
|
2161
|
+
limit: PAGE_SIZE$1,
|
|
1792
2162
|
offset,
|
|
1793
2163
|
sortBy: "createdAt",
|
|
1794
2164
|
sortOrder: "desc",
|
|
@@ -1813,6 +2183,13 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1813
2183
|
onSuccess: () => {
|
|
1814
2184
|
setConfirmDelete(null);
|
|
1815
2185
|
qc.invalidateQueries({ queryKey: ["dash-organizations"] });
|
|
2186
|
+
toggleNotification({ type: "success", message: "Organization deleted" });
|
|
2187
|
+
},
|
|
2188
|
+
onError: (err) => {
|
|
2189
|
+
toggleNotification({
|
|
2190
|
+
type: "danger",
|
|
2191
|
+
message: err.message ?? "Failed to delete organization"
|
|
2192
|
+
});
|
|
1816
2193
|
}
|
|
1817
2194
|
});
|
|
1818
2195
|
const deleteManyMutation = useMutation({
|
|
@@ -1825,15 +2202,25 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1825
2202
|
throw new Error(result.error.message ?? "Delete failed");
|
|
1826
2203
|
return result.data;
|
|
1827
2204
|
},
|
|
1828
|
-
onSuccess: () => {
|
|
2205
|
+
onSuccess: (_data, organizationIds) => {
|
|
1829
2206
|
setConfirmDeleteMany(false);
|
|
1830
2207
|
setSelected(/* @__PURE__ */ new Set());
|
|
1831
2208
|
qc.invalidateQueries({ queryKey: ["dash-organizations"] });
|
|
2209
|
+
toggleNotification({
|
|
2210
|
+
type: "success",
|
|
2211
|
+
message: `${organizationIds.length} organization${organizationIds.length !== 1 ? "s" : ""} deleted`
|
|
2212
|
+
});
|
|
2213
|
+
},
|
|
2214
|
+
onError: (err) => {
|
|
2215
|
+
toggleNotification({
|
|
2216
|
+
type: "danger",
|
|
2217
|
+
message: err.message ?? "Failed to delete organizations"
|
|
2218
|
+
});
|
|
1832
2219
|
}
|
|
1833
2220
|
});
|
|
1834
2221
|
const orgs = orgsQuery.data && "organizations" in orgsQuery.data ? orgsQuery.data.organizations : [];
|
|
1835
2222
|
const total = orgsQuery.data && "total" in orgsQuery.data ? orgsQuery.data.total : 0;
|
|
1836
|
-
const pageCount = Math.ceil(total / PAGE_SIZE$
|
|
2223
|
+
const pageCount = Math.ceil(total / PAGE_SIZE$1);
|
|
1837
2224
|
const allSelected = orgs.length > 0 && orgs.every((o) => selected.has(o.id));
|
|
1838
2225
|
const someSelected = selected.size > 0;
|
|
1839
2226
|
const toggleSelect = (id) => {
|
|
@@ -1853,11 +2240,11 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1853
2240
|
setSearch(searchInput);
|
|
1854
2241
|
setPage(1);
|
|
1855
2242
|
};
|
|
1856
|
-
return /* @__PURE__ */ jsxs(Wrap$
|
|
1857
|
-
/* @__PURE__ */ jsxs(PageHeader$
|
|
1858
|
-
/* @__PURE__ */ jsxs(TitleBlock$
|
|
1859
|
-
/* @__PURE__ */ jsx(PageTitle$
|
|
1860
|
-
/* @__PURE__ */ jsxs(PageSubtitle$
|
|
2243
|
+
return /* @__PURE__ */ jsxs(Wrap$2, { "data-testid": "organizations-page", children: [
|
|
2244
|
+
/* @__PURE__ */ jsxs(PageHeader$1, { children: [
|
|
2245
|
+
/* @__PURE__ */ jsxs(TitleBlock$1, { children: [
|
|
2246
|
+
/* @__PURE__ */ jsx(PageTitle$1, { children: "Organizations" }),
|
|
2247
|
+
/* @__PURE__ */ jsxs(PageSubtitle$1, { children: [
|
|
1861
2248
|
total.toLocaleString(),
|
|
1862
2249
|
" total"
|
|
1863
2250
|
] })
|
|
@@ -1905,7 +2292,7 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1905
2292
|
)
|
|
1906
2293
|
] }),
|
|
1907
2294
|
orgsQuery.isError && /* @__PURE__ */ jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: orgsQuery.error instanceof Error ? orgsQuery.error.message : "An error occurred" }),
|
|
1908
|
-
/* @__PURE__ */ jsx(TableCard$
|
|
2295
|
+
/* @__PURE__ */ jsx(TableCard$1, { children: orgsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading organizations…" }) }) : /* @__PURE__ */ jsxs(Table$1, { children: [
|
|
1909
2296
|
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
1910
2297
|
/* @__PURE__ */ jsx(THCheck$1, { children: /* @__PURE__ */ jsx(
|
|
1911
2298
|
Checkbox,
|
|
@@ -1915,14 +2302,14 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1915
2302
|
"aria-label": "Select all"
|
|
1916
2303
|
}
|
|
1917
2304
|
) }),
|
|
1918
|
-
/* @__PURE__ */ jsx(TH$
|
|
1919
|
-
/* @__PURE__ */ jsx(TH$
|
|
1920
|
-
/* @__PURE__ */ jsx(TH$
|
|
1921
|
-
/* @__PURE__ */ jsx(TH$
|
|
2305
|
+
/* @__PURE__ */ jsx(TH$1, { children: "Name" }),
|
|
2306
|
+
/* @__PURE__ */ jsx(TH$1, { children: "Slug" }),
|
|
2307
|
+
/* @__PURE__ */ jsx(TH$1, { children: "Members" }),
|
|
2308
|
+
/* @__PURE__ */ jsx(TH$1, { children: "Created" }),
|
|
1922
2309
|
/* @__PURE__ */ jsx(THActions$1, {})
|
|
1923
2310
|
] }) }),
|
|
1924
2311
|
/* @__PURE__ */ jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
|
|
1925
|
-
TD$
|
|
2312
|
+
TD$1,
|
|
1926
2313
|
{
|
|
1927
2314
|
colSpan: 6,
|
|
1928
2315
|
style: {
|
|
@@ -1934,7 +2321,7 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1934
2321
|
children: search ? `No organizations matching "${search}"` : "No organizations found"
|
|
1935
2322
|
}
|
|
1936
2323
|
) }) : orgs.map((org, i) => /* @__PURE__ */ jsxs(
|
|
1937
|
-
TR$
|
|
2324
|
+
TR$1,
|
|
1938
2325
|
{
|
|
1939
2326
|
$selected: selected.has(org.id),
|
|
1940
2327
|
$i: i,
|
|
@@ -1948,13 +2335,13 @@ function OrganizationsPage({ teamsEnabled }) {
|
|
|
1948
2335
|
"aria-label": `Select ${org.name}`
|
|
1949
2336
|
}
|
|
1950
2337
|
) }),
|
|
1951
|
-
/* @__PURE__ */ jsx(TD$
|
|
2338
|
+
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
|
|
1952
2339
|
/* @__PURE__ */ jsx(OrgAvatar, { name: org.name, logo: org.logo }),
|
|
1953
2340
|
/* @__PURE__ */ jsx(OrgName, { children: org.name })
|
|
1954
2341
|
] }) }),
|
|
1955
|
-
/* @__PURE__ */ jsx(TD$
|
|
1956
|
-
/* @__PURE__ */ jsx(TD$
|
|
1957
|
-
/* @__PURE__ */ jsx(TD$
|
|
2342
|
+
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(SlugChip, { children: org.slug }) }),
|
|
2343
|
+
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(CountChip, { children: org.memberCount }) }),
|
|
2344
|
+
/* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
|
|
1958
2345
|
/* @__PURE__ */ jsx(TDActions$1, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", children: [
|
|
1959
2346
|
/* @__PURE__ */ jsx(
|
|
1960
2347
|
IconButton,
|
|
@@ -2101,95 +2488,143 @@ function Avatar({
|
|
|
2101
2488
|
const color = getColor(name || "?");
|
|
2102
2489
|
return /* @__PURE__ */ jsx(Circle, { $bg: color.bg, $fg: color.fg, $size: size, children: getInitials(name || "?") });
|
|
2103
2490
|
}
|
|
2104
|
-
const
|
|
2105
|
-
|
|
2491
|
+
const T = {
|
|
2492
|
+
bg: "#f6f6f9",
|
|
2493
|
+
bgCard: "#ffffff",
|
|
2494
|
+
border: "#eaeaef",
|
|
2495
|
+
borderHover: "rgba(73,69,255,0.35)",
|
|
2496
|
+
accent: "#4945ff",
|
|
2497
|
+
green: "#5cb176",
|
|
2498
|
+
greenDim: "rgba(92,177,118,0.12)",
|
|
2499
|
+
amber: "#d9822f",
|
|
2500
|
+
red: "#d02b20",
|
|
2501
|
+
redDim: "rgba(208,43,32,0.1)",
|
|
2502
|
+
purple: "#8460b8",
|
|
2503
|
+
textPrimary: "#32324d",
|
|
2504
|
+
textSecondary: "#666687",
|
|
2505
|
+
textMuted: "#b8b8c7",
|
|
2506
|
+
mono: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`
|
|
2507
|
+
};
|
|
2508
|
+
const fadeUp$1 = keyframes`
|
|
2509
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
2106
2510
|
to { opacity: 1; transform: translateY(0); }
|
|
2107
2511
|
`;
|
|
2108
|
-
const Wrap$
|
|
2109
|
-
padding: 28px 32px;
|
|
2110
|
-
background:
|
|
2512
|
+
const Wrap$1 = styled.div`
|
|
2513
|
+
padding: 28px 32px 48px;
|
|
2514
|
+
background: ${T.bg};
|
|
2111
2515
|
min-height: 100%;
|
|
2112
2516
|
display: flex;
|
|
2113
2517
|
flex-direction: column;
|
|
2114
|
-
gap:
|
|
2518
|
+
gap: 22px;
|
|
2115
2519
|
`;
|
|
2116
|
-
const
|
|
2117
|
-
display:
|
|
2118
|
-
|
|
2119
|
-
gap:
|
|
2120
|
-
|
|
2121
|
-
@media (max-width: 768px) { grid-template-columns: repeat(2, 1fr); }
|
|
2520
|
+
const SectionDivider = styled.div`
|
|
2521
|
+
display: flex;
|
|
2522
|
+
align-items: center;
|
|
2523
|
+
gap: 10px;
|
|
2524
|
+
margin-bottom: -6px;
|
|
2122
2525
|
`;
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2526
|
+
const DivLabel = styled.span`
|
|
2527
|
+
font-size: 9px;
|
|
2528
|
+
font-weight: 700;
|
|
2529
|
+
letter-spacing: 0.14em;
|
|
2530
|
+
text-transform: uppercase;
|
|
2531
|
+
color: ${T.textMuted};
|
|
2532
|
+
white-space: nowrap;
|
|
2128
2533
|
`;
|
|
2129
|
-
const
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2534
|
+
const DivLine = styled.div`
|
|
2535
|
+
flex: 1;
|
|
2536
|
+
height: 1px;
|
|
2537
|
+
background: ${T.border};
|
|
2133
2538
|
`;
|
|
2134
2539
|
const Card = styled.div`
|
|
2135
|
-
background:
|
|
2136
|
-
border: 1px solid
|
|
2137
|
-
border-radius:
|
|
2540
|
+
background: ${T.bgCard};
|
|
2541
|
+
border: 1px solid ${T.border};
|
|
2542
|
+
border-radius: 12px;
|
|
2138
2543
|
overflow: hidden;
|
|
2139
2544
|
position: relative;
|
|
2140
|
-
animation: ${fadeUp$
|
|
2545
|
+
animation: ${fadeUp$1} 380ms ease both;
|
|
2141
2546
|
animation-delay: ${(p) => (p.$delay ?? 0) * 55}ms;
|
|
2142
|
-
transition: border-color
|
|
2143
|
-
|
|
2144
|
-
${(p) => p.$accent && `
|
|
2145
|
-
&::before {
|
|
2146
|
-
content: '';
|
|
2147
|
-
position: absolute;
|
|
2148
|
-
top: 0; left: 0; right: 0;
|
|
2149
|
-
height: 3px;
|
|
2150
|
-
background: ${p.$accent};
|
|
2151
|
-
z-index: 1;
|
|
2152
|
-
}
|
|
2153
|
-
`}
|
|
2154
|
-
|
|
2547
|
+
transition: border-color 200ms, box-shadow 200ms;
|
|
2155
2548
|
&:hover {
|
|
2156
|
-
border-color:
|
|
2157
|
-
box-shadow: 0
|
|
2549
|
+
border-color: ${T.borderHover};
|
|
2550
|
+
box-shadow: 0 0 0 1px ${T.borderHover}, 0 8px 32px rgba(124,109,250,0.07);
|
|
2158
2551
|
}
|
|
2159
2552
|
`;
|
|
2160
|
-
const
|
|
2161
|
-
|
|
2553
|
+
const PillGroup = styled.div`
|
|
2554
|
+
display: flex;
|
|
2555
|
+
background: rgba(0,0,0,0.03);
|
|
2556
|
+
border: 1px solid ${T.border};
|
|
2557
|
+
border-radius: 9px;
|
|
2558
|
+
padding: 3px;
|
|
2559
|
+
gap: 2px;
|
|
2162
2560
|
`;
|
|
2163
|
-
const
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2561
|
+
const Pill = styled.button`
|
|
2562
|
+
appearance: none;
|
|
2563
|
+
border: none;
|
|
2564
|
+
cursor: pointer;
|
|
2565
|
+
padding: 5px 16px;
|
|
2566
|
+
border-radius: 6px;
|
|
2567
|
+
font-size: 11px;
|
|
2568
|
+
font-weight: 600;
|
|
2569
|
+
letter-spacing: 0.03em;
|
|
2570
|
+
transition: background 180ms, color 180ms;
|
|
2571
|
+
background: ${(p) => p.$active ? T.accent : "transparent"};
|
|
2572
|
+
color: ${(p) => p.$active ? "#fff" : T.textSecondary};
|
|
2573
|
+
&:hover {
|
|
2574
|
+
background: ${(p) => p.$active ? T.accent : "rgba(0,0,0,0.05)"};
|
|
2575
|
+
color: ${(p) => p.$active ? "#fff" : T.textPrimary};
|
|
2576
|
+
}
|
|
2577
|
+
`;
|
|
2578
|
+
const StatGrid = styled.div`
|
|
2579
|
+
display: grid;
|
|
2580
|
+
grid-template-columns: repeat(5, 1fr);
|
|
2581
|
+
gap: 10px;
|
|
2582
|
+
@media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
|
|
2583
|
+
`;
|
|
2584
|
+
const StatCard = styled(Card)`
|
|
2585
|
+
padding: 18px 20px 0;
|
|
2586
|
+
&::after {
|
|
2587
|
+
content: '';
|
|
2588
|
+
position: absolute;
|
|
2589
|
+
top: 0; left: 0; right: 0;
|
|
2590
|
+
height: 2px;
|
|
2591
|
+
background: ${(p) => p.$accent};
|
|
2592
|
+
opacity: 0.75;
|
|
2593
|
+
}
|
|
2594
|
+
`;
|
|
2595
|
+
const StatLabel = styled.div`
|
|
2596
|
+
font-size: 9px;
|
|
2597
|
+
font-weight: 700;
|
|
2598
|
+
letter-spacing: 0.1em;
|
|
2167
2599
|
text-transform: uppercase;
|
|
2168
|
-
color:
|
|
2169
|
-
margin-bottom:
|
|
2600
|
+
color: ${T.textMuted};
|
|
2601
|
+
margin-bottom: 10px;
|
|
2170
2602
|
`;
|
|
2171
2603
|
const StatValue = styled.div`
|
|
2172
2604
|
font-size: 30px;
|
|
2173
2605
|
font-weight: 800;
|
|
2174
|
-
color:
|
|
2606
|
+
color: ${T.textPrimary};
|
|
2175
2607
|
line-height: 1;
|
|
2176
|
-
letter-spacing: -0.
|
|
2608
|
+
letter-spacing: -0.045em;
|
|
2177
2609
|
font-variant-numeric: tabular-nums;
|
|
2178
2610
|
margin-bottom: 10px;
|
|
2179
2611
|
`;
|
|
2180
2612
|
const TrendBadge = styled.span`
|
|
2181
2613
|
display: inline-flex;
|
|
2182
2614
|
align-items: center;
|
|
2183
|
-
gap:
|
|
2184
|
-
padding: 2px
|
|
2615
|
+
gap: 3px;
|
|
2616
|
+
padding: 2px 8px;
|
|
2185
2617
|
border-radius: 20px;
|
|
2186
|
-
font-size:
|
|
2618
|
+
font-size: 9px;
|
|
2187
2619
|
font-weight: 700;
|
|
2188
|
-
|
|
2189
|
-
|
|
2620
|
+
letter-spacing: 0.03em;
|
|
2621
|
+
background: ${(p) => p.$pos ? T.greenDim : T.redDim};
|
|
2622
|
+
color: ${(p) => p.$pos ? T.green : T.red};
|
|
2623
|
+
margin-bottom: 12px;
|
|
2190
2624
|
`;
|
|
2191
2625
|
const SparkWrap = styled.div`
|
|
2192
|
-
height:
|
|
2626
|
+
height: 44px;
|
|
2627
|
+
margin: 0 -20px;
|
|
2193
2628
|
`;
|
|
2194
2629
|
const ChartCard = styled(Card)`
|
|
2195
2630
|
padding: 20px 20px 12px;
|
|
@@ -2201,31 +2636,42 @@ const ChartHeader = styled.div`
|
|
|
2201
2636
|
display: flex;
|
|
2202
2637
|
align-items: center;
|
|
2203
2638
|
justify-content: space-between;
|
|
2639
|
+
gap: 12px;
|
|
2204
2640
|
flex-shrink: 0;
|
|
2205
2641
|
`;
|
|
2206
2642
|
const ChartTitle = styled.div`
|
|
2207
|
-
font-size:
|
|
2643
|
+
font-size: 12px;
|
|
2208
2644
|
font-weight: 700;
|
|
2209
|
-
color:
|
|
2645
|
+
color: ${T.textPrimary};
|
|
2646
|
+
letter-spacing: 0.01em;
|
|
2210
2647
|
`;
|
|
2211
|
-
const
|
|
2648
|
+
const SeriesRow = styled.div`
|
|
2212
2649
|
display: flex;
|
|
2213
2650
|
align-items: center;
|
|
2214
|
-
gap:
|
|
2651
|
+
gap: 6px;
|
|
2215
2652
|
`;
|
|
2216
|
-
const
|
|
2653
|
+
const SeriesBtn = styled.button`
|
|
2654
|
+
appearance: none;
|
|
2655
|
+
border: 1px solid ${(p) => p.$on ? `${p.$c}55` : T.border};
|
|
2656
|
+
background: ${(p) => p.$on ? `${p.$c}18` : "transparent"};
|
|
2657
|
+
color: ${(p) => p.$on ? p.$c : T.textMuted};
|
|
2658
|
+
font-size: 10px;
|
|
2659
|
+
font-weight: 600;
|
|
2660
|
+
padding: 3px 10px 3px 8px;
|
|
2661
|
+
border-radius: 6px;
|
|
2662
|
+
cursor: pointer;
|
|
2217
2663
|
display: flex;
|
|
2218
2664
|
align-items: center;
|
|
2219
2665
|
gap: 5px;
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2666
|
+
transition: all 160ms;
|
|
2667
|
+
&:hover {
|
|
2668
|
+
border-color: ${(p) => `${p.$c}88`};
|
|
2669
|
+
color: ${(p) => p.$c};
|
|
2670
|
+
background: ${(p) => `${p.$c}22`};
|
|
2671
|
+
}
|
|
2225
2672
|
`;
|
|
2226
|
-
const
|
|
2227
|
-
width:
|
|
2228
|
-
height: 8px;
|
|
2673
|
+
const SDot = styled.span`
|
|
2674
|
+
width: 6px; height: 6px;
|
|
2229
2675
|
border-radius: 50%;
|
|
2230
2676
|
background: ${(p) => p.$c};
|
|
2231
2677
|
display: inline-block;
|
|
@@ -2233,140 +2679,188 @@ const LegendDot = styled.span`
|
|
|
2233
2679
|
`;
|
|
2234
2680
|
const HoverInfo = styled.div`
|
|
2235
2681
|
font-size: 11px;
|
|
2236
|
-
color:
|
|
2682
|
+
color: ${T.textSecondary};
|
|
2237
2683
|
font-variant-numeric: tabular-nums;
|
|
2238
|
-
|
|
2684
|
+
font-family: ${T.mono};
|
|
2685
|
+
min-height: 15px;
|
|
2686
|
+
`;
|
|
2687
|
+
const TwoPanel = styled.div`
|
|
2688
|
+
display: grid;
|
|
2689
|
+
grid-template-columns: ${(p) => p.$ratio ?? "1fr 288px"};
|
|
2690
|
+
gap: 10px;
|
|
2691
|
+
align-items: stretch;
|
|
2692
|
+
@media (max-width: 900px) { grid-template-columns: 1fr; }
|
|
2239
2693
|
`;
|
|
2240
2694
|
const FeedCard = styled(Card)`
|
|
2241
2695
|
display: flex;
|
|
2242
2696
|
flex-direction: column;
|
|
2243
|
-
|
|
2697
|
+
overflow: hidden;
|
|
2244
2698
|
`;
|
|
2245
|
-
const
|
|
2246
|
-
padding: 16px
|
|
2247
|
-
border-bottom: 1px solid
|
|
2248
|
-
|
|
2249
|
-
font-size: 10px;
|
|
2699
|
+
const FeedHead = styled.div`
|
|
2700
|
+
padding: 13px 16px 9px;
|
|
2701
|
+
border-bottom: 1px solid ${T.border};
|
|
2702
|
+
font-size: 11px;
|
|
2250
2703
|
font-weight: 700;
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2704
|
+
color: ${T.textPrimary};
|
|
2705
|
+
flex-shrink: 0;
|
|
2706
|
+
display: flex;
|
|
2707
|
+
justify-content: space-between;
|
|
2708
|
+
align-items: center;
|
|
2254
2709
|
`;
|
|
2255
|
-
const
|
|
2710
|
+
const FeedSelect = styled.select`
|
|
2711
|
+
appearance: none;
|
|
2712
|
+
border: 1px solid ${T.border};
|
|
2713
|
+
border-radius: 6px;
|
|
2714
|
+
background: ${T.bgCard};
|
|
2715
|
+
color: ${T.textSecondary};
|
|
2716
|
+
font-size: 10px;
|
|
2717
|
+
font-weight: 600;
|
|
2718
|
+
padding: 3px 22px 3px 8px;
|
|
2719
|
+
cursor: pointer;
|
|
2720
|
+
outline: none;
|
|
2721
|
+
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");
|
|
2722
|
+
background-repeat: no-repeat;
|
|
2723
|
+
background-position: right 7px center;
|
|
2724
|
+
transition: border-color 160ms;
|
|
2725
|
+
&:hover, &:focus { border-color: ${T.accent}; color: ${T.textPrimary}; }
|
|
2726
|
+
`;
|
|
2727
|
+
const FeedScroll = styled.div`
|
|
2256
2728
|
flex: 1;
|
|
2257
2729
|
overflow-y: auto;
|
|
2258
|
-
|
|
2259
|
-
&::-webkit-scrollbar { width:
|
|
2260
|
-
&::-webkit-scrollbar-
|
|
2261
|
-
&::-webkit-scrollbar-thumb { background: #d9d8ff; border-radius: 2px; }
|
|
2730
|
+
min-height: 0;
|
|
2731
|
+
&::-webkit-scrollbar { width: 4px; }
|
|
2732
|
+
&::-webkit-scrollbar-thumb { background: ${T.textMuted}; border-radius: 2px; }
|
|
2262
2733
|
`;
|
|
2263
2734
|
const FeedItem = styled.div`
|
|
2735
|
+
padding: 9px 14px;
|
|
2736
|
+
border-bottom: 1px solid #f0f0f5;
|
|
2264
2737
|
display: flex;
|
|
2265
|
-
|
|
2266
|
-
gap:
|
|
2267
|
-
|
|
2268
|
-
&:hover { background: #
|
|
2738
|
+
flex-direction: column;
|
|
2739
|
+
gap: 3px;
|
|
2740
|
+
transition: background 130ms;
|
|
2741
|
+
&:hover { background: #fafaff; }
|
|
2742
|
+
&:last-child { border-bottom: none; }
|
|
2269
2743
|
`;
|
|
2270
|
-
const
|
|
2271
|
-
|
|
2272
|
-
|
|
2744
|
+
const FeedTop = styled.div`
|
|
2745
|
+
display: flex;
|
|
2746
|
+
align-items: center;
|
|
2747
|
+
justify-content: space-between;
|
|
2748
|
+
gap: 6px;
|
|
2273
2749
|
`;
|
|
2274
|
-
const FeedName = styled.
|
|
2275
|
-
font-size:
|
|
2750
|
+
const FeedName = styled.span`
|
|
2751
|
+
font-size: 11px;
|
|
2276
2752
|
font-weight: 600;
|
|
2277
|
-
color:
|
|
2278
|
-
white-space: nowrap;
|
|
2753
|
+
color: ${T.textPrimary};
|
|
2279
2754
|
overflow: hidden;
|
|
2280
2755
|
text-overflow: ellipsis;
|
|
2756
|
+
white-space: nowrap;
|
|
2281
2757
|
`;
|
|
2282
|
-
const
|
|
2758
|
+
const FeedEmail = styled.span`
|
|
2283
2759
|
font-size: 10px;
|
|
2284
|
-
color:
|
|
2285
|
-
white-space: nowrap;
|
|
2760
|
+
color: ${T.textSecondary};
|
|
2286
2761
|
overflow: hidden;
|
|
2287
2762
|
text-overflow: ellipsis;
|
|
2288
|
-
|
|
2763
|
+
white-space: nowrap;
|
|
2764
|
+
display: block;
|
|
2289
2765
|
`;
|
|
2290
|
-
const
|
|
2291
|
-
font-size:
|
|
2292
|
-
color:
|
|
2766
|
+
const FeedMeta = styled.span`
|
|
2767
|
+
font-size: 9px;
|
|
2768
|
+
color: ${T.textMuted};
|
|
2293
2769
|
white-space: nowrap;
|
|
2294
2770
|
font-variant-numeric: tabular-nums;
|
|
2295
|
-
margin-top: 1px;
|
|
2296
2771
|
`;
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2772
|
+
const RtnCard = styled(Card)`
|
|
2773
|
+
padding: 20px;
|
|
2774
|
+
display: flex;
|
|
2775
|
+
flex-direction: column;
|
|
2776
|
+
gap: 12px;
|
|
2300
2777
|
`;
|
|
2301
|
-
const
|
|
2302
|
-
|
|
2303
|
-
|
|
2778
|
+
const RtnRow = styled.div`
|
|
2779
|
+
display: flex;
|
|
2780
|
+
align-items: center;
|
|
2781
|
+
gap: 10px;
|
|
2782
|
+
padding: 4px 6px;
|
|
2783
|
+
border-radius: 7px;
|
|
2784
|
+
transition: background 130ms;
|
|
2785
|
+
background: ${(p) => p.$hov ? "#f0f0f6" : "transparent"};
|
|
2786
|
+
cursor: default;
|
|
2304
2787
|
`;
|
|
2305
|
-
const
|
|
2306
|
-
text-align: left;
|
|
2307
|
-
padding: 10px 14px;
|
|
2788
|
+
const RtnLabel = styled.span`
|
|
2308
2789
|
font-size: 10px;
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
text-
|
|
2312
|
-
letter-spacing: 0.06em;
|
|
2313
|
-
border-bottom: 1px solid #eaeaef;
|
|
2314
|
-
background: #fafafa;
|
|
2790
|
+
color: ${T.textSecondary};
|
|
2791
|
+
min-width: 80px;
|
|
2792
|
+
text-align: right;
|
|
2315
2793
|
white-space: nowrap;
|
|
2316
|
-
&:first-child { padding-left: 20px; }
|
|
2317
|
-
&:last-child { padding-right: 20px; }
|
|
2318
2794
|
`;
|
|
2319
|
-
const
|
|
2320
|
-
|
|
2321
|
-
|
|
2795
|
+
const RtnSize = styled.span`
|
|
2796
|
+
font-size: 9px;
|
|
2797
|
+
color: ${T.textMuted};
|
|
2798
|
+
min-width: 52px;
|
|
2799
|
+
text-align: right;
|
|
2800
|
+
white-space: nowrap;
|
|
2801
|
+
font-variant-numeric: tabular-nums;
|
|
2322
2802
|
`;
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
border-
|
|
2328
|
-
|
|
2329
|
-
&:first-child { padding-left: 20px; }
|
|
2330
|
-
&:last-child { padding-right: 20px; }
|
|
2803
|
+
const RtnTrack = styled.div`
|
|
2804
|
+
flex: 1;
|
|
2805
|
+
height: 10px;
|
|
2806
|
+
background: #eaeaef;
|
|
2807
|
+
border-radius: 4px;
|
|
2808
|
+
overflow: hidden;
|
|
2331
2809
|
`;
|
|
2332
|
-
const
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2810
|
+
const RtnBar = styled.div`
|
|
2811
|
+
width: ${(p) => Math.max(p.$w, 0.5)}%;
|
|
2812
|
+
height: 100%;
|
|
2813
|
+
background: hsl(${(p) => p.$hue}, 60%, 50%);
|
|
2814
|
+
border-radius: 4px;
|
|
2815
|
+
transition: width 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
2816
|
+
`;
|
|
2817
|
+
const RtnPct = styled.span`
|
|
2338
2818
|
font-size: 10px;
|
|
2339
2819
|
font-weight: 700;
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2820
|
+
color: hsl(${(p) => p.$hue}, 60%, 60%);
|
|
2821
|
+
min-width: 38px;
|
|
2822
|
+
text-align: right;
|
|
2823
|
+
font-variant-numeric: tabular-nums;
|
|
2824
|
+
font-family: ${T.mono};
|
|
2825
|
+
`;
|
|
2826
|
+
const RtnTip = styled.div`
|
|
2827
|
+
font-size: 10px;
|
|
2828
|
+
color: ${T.textSecondary};
|
|
2829
|
+
min-height: 14px;
|
|
2830
|
+
font-variant-numeric: tabular-nums;
|
|
2831
|
+
padding: 0 6px;
|
|
2351
2832
|
`;
|
|
2352
|
-
const
|
|
2833
|
+
const RingGrid = styled.div`
|
|
2353
2834
|
display: flex;
|
|
2354
|
-
|
|
2835
|
+
flex-direction: column;
|
|
2355
2836
|
gap: 10px;
|
|
2356
|
-
margin-bottom: -8px;
|
|
2357
2837
|
`;
|
|
2358
|
-
const
|
|
2359
|
-
|
|
2838
|
+
const RingCard = styled(Card)`
|
|
2839
|
+
padding: 18px 20px;
|
|
2840
|
+
display: flex;
|
|
2841
|
+
align-items: center;
|
|
2842
|
+
gap: 16px;
|
|
2843
|
+
flex: 1;
|
|
2844
|
+
`;
|
|
2845
|
+
const RingInfo = styled.div`
|
|
2846
|
+
display: flex;
|
|
2847
|
+
flex-direction: column;
|
|
2848
|
+
gap: 4px;
|
|
2849
|
+
`;
|
|
2850
|
+
const RingVal = styled.div`
|
|
2851
|
+
font-size: 24px;
|
|
2852
|
+
font-weight: 800;
|
|
2853
|
+
color: ${T.textPrimary};
|
|
2854
|
+
letter-spacing: -0.04em;
|
|
2855
|
+
font-variant-numeric: tabular-nums;
|
|
2856
|
+
line-height: 1;
|
|
2857
|
+
`;
|
|
2858
|
+
const RingLabel = styled.div`
|
|
2859
|
+
font-size: 9px;
|
|
2360
2860
|
font-weight: 700;
|
|
2361
2861
|
text-transform: uppercase;
|
|
2362
|
-
letter-spacing: 0.
|
|
2363
|
-
color:
|
|
2364
|
-
white-space: nowrap;
|
|
2365
|
-
`;
|
|
2366
|
-
const DivLine = styled.div`
|
|
2367
|
-
flex: 1;
|
|
2368
|
-
height: 1px;
|
|
2369
|
-
background: #eaeaef;
|
|
2862
|
+
letter-spacing: 0.1em;
|
|
2863
|
+
color: ${T.textMuted};
|
|
2370
2864
|
`;
|
|
2371
2865
|
const Empty = styled.div`
|
|
2372
2866
|
display: flex;
|
|
@@ -2374,32 +2868,52 @@ const Empty = styled.div`
|
|
|
2374
2868
|
align-items: center;
|
|
2375
2869
|
justify-content: center;
|
|
2376
2870
|
padding: 32px;
|
|
2377
|
-
color:
|
|
2871
|
+
color: ${T.textMuted};
|
|
2378
2872
|
font-size: 12px;
|
|
2379
|
-
gap:
|
|
2873
|
+
gap: 8px;
|
|
2380
2874
|
`;
|
|
2875
|
+
function useCountUp(target, duration = 850) {
|
|
2876
|
+
const [count, setCount] = useState(0);
|
|
2877
|
+
const prevRef = useRef(0);
|
|
2878
|
+
useEffect(() => {
|
|
2879
|
+
let raf;
|
|
2880
|
+
let start = 0;
|
|
2881
|
+
const from = prevRef.current;
|
|
2882
|
+
const diff = target - from;
|
|
2883
|
+
const step = (ts) => {
|
|
2884
|
+
if (!start) start = ts;
|
|
2885
|
+
const progress = Math.min((ts - start) / duration, 1);
|
|
2886
|
+
const eased = 1 - (1 - progress) ** 3;
|
|
2887
|
+
setCount(Math.round(from + eased * diff));
|
|
2888
|
+
if (progress < 1) raf = requestAnimationFrame(step);
|
|
2889
|
+
else prevRef.current = target;
|
|
2890
|
+
};
|
|
2891
|
+
raf = requestAnimationFrame(step);
|
|
2892
|
+
return () => cancelAnimationFrame(raf);
|
|
2893
|
+
}, [target, duration]);
|
|
2894
|
+
return count;
|
|
2895
|
+
}
|
|
2381
2896
|
function Sparkline({
|
|
2382
2897
|
data,
|
|
2383
2898
|
color,
|
|
2384
2899
|
id
|
|
2385
2900
|
}) {
|
|
2386
2901
|
const W = 300;
|
|
2387
|
-
const H =
|
|
2902
|
+
const H = 44;
|
|
2388
2903
|
if (data.length < 2) return /* @__PURE__ */ jsx("svg", { width: "100%", height: H });
|
|
2389
2904
|
const min = Math.min(...data);
|
|
2390
2905
|
const max = Math.max(...data, min + 1);
|
|
2391
|
-
const range = max - min;
|
|
2392
2906
|
const pts = data.map((v, i) => ({
|
|
2393
2907
|
x: i / (data.length - 1) * W,
|
|
2394
|
-
y: H -
|
|
2908
|
+
y: H - 3 - (v - min) / (max - min) * (H - 8)
|
|
2395
2909
|
}));
|
|
2396
|
-
const line = pts.reduce((
|
|
2910
|
+
const line = pts.reduce((a, p, i) => {
|
|
2397
2911
|
if (i === 0) return `M ${p.x} ${p.y}`;
|
|
2398
2912
|
const pr = pts[i - 1];
|
|
2399
2913
|
const cx = (pr.x + p.x) / 2;
|
|
2400
|
-
return `${
|
|
2914
|
+
return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
|
|
2401
2915
|
}, "");
|
|
2402
|
-
const area = `${line} L ${pts
|
|
2916
|
+
const area = `${line} L ${pts.at(-1).x} ${H} L 0 ${H} Z`;
|
|
2403
2917
|
const gid = `spk-${id}`;
|
|
2404
2918
|
return /* @__PURE__ */ jsxs(
|
|
2405
2919
|
"svg",
|
|
@@ -2408,10 +2922,10 @@ function Sparkline({
|
|
|
2408
2922
|
height: H,
|
|
2409
2923
|
viewBox: `0 0 ${W} ${H}`,
|
|
2410
2924
|
preserveAspectRatio: "none",
|
|
2411
|
-
"aria-hidden":
|
|
2925
|
+
"aria-hidden": true,
|
|
2412
2926
|
children: [
|
|
2413
2927
|
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: gid, x1: "0", y1: "0", x2: "0", y2: "1", children: [
|
|
2414
|
-
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.
|
|
2928
|
+
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.32" }),
|
|
2415
2929
|
/* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
|
|
2416
2930
|
] }) }),
|
|
2417
2931
|
/* @__PURE__ */ jsx("path", { d: area, fill: `url(#${gid})` }),
|
|
@@ -2420,57 +2934,58 @@ function Sparkline({
|
|
|
2420
2934
|
{
|
|
2421
2935
|
d: line,
|
|
2422
2936
|
stroke: color,
|
|
2423
|
-
strokeWidth: "
|
|
2937
|
+
strokeWidth: "1.6",
|
|
2424
2938
|
fill: "none",
|
|
2425
|
-
strokeLinecap: "round"
|
|
2426
|
-
strokeLinejoin: "round"
|
|
2939
|
+
strokeLinecap: "round"
|
|
2427
2940
|
}
|
|
2428
2941
|
)
|
|
2429
2942
|
]
|
|
2430
2943
|
}
|
|
2431
2944
|
);
|
|
2432
2945
|
}
|
|
2433
|
-
const
|
|
2434
|
-
{ key: "totalUsers", color:
|
|
2435
|
-
{ key: "newUsers", color:
|
|
2436
|
-
{ key: "activeUsers", color:
|
|
2946
|
+
const ALL_SERIES = [
|
|
2947
|
+
{ key: "totalUsers", color: T.accent, label: "Total" },
|
|
2948
|
+
{ key: "newUsers", color: T.green, label: "New" },
|
|
2949
|
+
{ key: "activeUsers", color: T.amber, label: "Active" }
|
|
2437
2950
|
];
|
|
2438
|
-
function
|
|
2951
|
+
function GrowthChart({
|
|
2439
2952
|
data,
|
|
2440
2953
|
hovered,
|
|
2441
|
-
onHover
|
|
2954
|
+
onHover,
|
|
2955
|
+
activeSeries
|
|
2442
2956
|
}) {
|
|
2443
2957
|
const W = 600;
|
|
2444
|
-
const H =
|
|
2445
|
-
const PL =
|
|
2958
|
+
const H = 200;
|
|
2959
|
+
const PL = 40;
|
|
2446
2960
|
const PR = 12;
|
|
2447
2961
|
const PT = 12;
|
|
2448
2962
|
const PB = 28;
|
|
2449
2963
|
const CW = W - PL - PR;
|
|
2450
2964
|
const CH = H - PT - PB;
|
|
2451
|
-
if (data.length
|
|
2965
|
+
if (!data.length) {
|
|
2452
2966
|
return /* @__PURE__ */ jsxs(Empty, { children: [
|
|
2453
2967
|
/* @__PURE__ */ jsx("div", { style: { fontSize: 24 }, children: "📊" }),
|
|
2454
2968
|
/* @__PURE__ */ jsx("div", { children: "No growth data for this period" })
|
|
2455
2969
|
] });
|
|
2456
2970
|
}
|
|
2457
|
-
const
|
|
2971
|
+
const active = ALL_SERIES.filter((s) => activeSeries.has(s.key));
|
|
2972
|
+
const allVals = data.flatMap((d) => active.map((s) => d[s.key]));
|
|
2458
2973
|
const maxV = Math.max(...allVals, 10);
|
|
2459
2974
|
const yMax = Math.ceil(maxV * 1.15);
|
|
2460
2975
|
const xp = (i) => PL + i / Math.max(data.length - 1, 1) * CW;
|
|
2461
2976
|
const yp = (v) => PT + (1 - v / yMax) * CH;
|
|
2462
2977
|
const smooth = (vals) => {
|
|
2463
2978
|
const pts = vals.map((v, i) => ({ x: xp(i), y: yp(v) }));
|
|
2464
|
-
const path = pts.reduce((
|
|
2979
|
+
const path = pts.reduce((a, p, i) => {
|
|
2465
2980
|
if (i === 0) return `M ${p.x} ${p.y}`;
|
|
2466
2981
|
const pr = pts[i - 1];
|
|
2467
2982
|
const cx = (pr.x + p.x) / 2;
|
|
2468
|
-
return `${
|
|
2983
|
+
return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
|
|
2469
2984
|
}, "");
|
|
2470
|
-
const area = `${path} L ${pts
|
|
2985
|
+
const area = `${path} L ${pts.at(-1).x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
|
|
2471
2986
|
return { pts, path, area };
|
|
2472
2987
|
};
|
|
2473
|
-
const lines =
|
|
2988
|
+
const lines = active.map((s) => ({
|
|
2474
2989
|
...s,
|
|
2475
2990
|
...smooth(data.map((d) => d[s.key]))
|
|
2476
2991
|
}));
|
|
@@ -2493,17 +3008,17 @@ function AreaChart({
|
|
|
2493
3008
|
/* @__PURE__ */ jsx("defs", { children: lines.map((s) => /* @__PURE__ */ jsxs(
|
|
2494
3009
|
"linearGradient",
|
|
2495
3010
|
{
|
|
2496
|
-
id: `ag-${s.
|
|
3011
|
+
id: `ag-${s.key}`,
|
|
2497
3012
|
x1: "0",
|
|
2498
3013
|
y1: "0",
|
|
2499
3014
|
x2: "0",
|
|
2500
3015
|
y2: "1",
|
|
2501
3016
|
children: [
|
|
2502
|
-
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.
|
|
3017
|
+
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.18" }),
|
|
2503
3018
|
/* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: s.color, stopOpacity: "0" })
|
|
2504
3019
|
]
|
|
2505
3020
|
},
|
|
2506
|
-
s.
|
|
3021
|
+
s.key
|
|
2507
3022
|
)) }),
|
|
2508
3023
|
yTicks.map((t) => /* @__PURE__ */ jsxs("g", { children: [
|
|
2509
3024
|
/* @__PURE__ */ jsx(
|
|
@@ -2523,7 +3038,7 @@ function AreaChart({
|
|
|
2523
3038
|
x: PL - 6,
|
|
2524
3039
|
y: t.y + 4,
|
|
2525
3040
|
textAnchor: "end",
|
|
2526
|
-
fill:
|
|
3041
|
+
fill: T.textMuted,
|
|
2527
3042
|
fontSize: "9",
|
|
2528
3043
|
children: t.v >= 1e3 ? `${(t.v / 1e3).toFixed(1)}k` : t.v
|
|
2529
3044
|
}
|
|
@@ -2540,25 +3055,18 @@ function AreaChart({
|
|
|
2540
3055
|
strokeWidth: "1"
|
|
2541
3056
|
}
|
|
2542
3057
|
),
|
|
2543
|
-
lines.map((s) => /* @__PURE__ */ jsx(
|
|
2544
|
-
"path",
|
|
2545
|
-
{
|
|
2546
|
-
d: s.area,
|
|
2547
|
-
fill: `url(#ag-${s.color.replace("#", "")})`
|
|
2548
|
-
},
|
|
2549
|
-
`a-${s.color}`
|
|
2550
|
-
)),
|
|
3058
|
+
lines.map((s) => /* @__PURE__ */ jsx("path", { d: s.area, fill: `url(#ag-${s.key})` }, `area-${s.key}`)),
|
|
2551
3059
|
lines.map((s) => /* @__PURE__ */ jsx(
|
|
2552
3060
|
"path",
|
|
2553
3061
|
{
|
|
2554
3062
|
d: s.path,
|
|
2555
3063
|
stroke: s.color,
|
|
2556
|
-
strokeWidth: "
|
|
3064
|
+
strokeWidth: "2",
|
|
2557
3065
|
fill: "none",
|
|
2558
3066
|
strokeLinecap: "round",
|
|
2559
3067
|
strokeLinejoin: "round"
|
|
2560
3068
|
},
|
|
2561
|
-
`
|
|
3069
|
+
`line-${s.key}`
|
|
2562
3070
|
)),
|
|
2563
3071
|
data.map((d, i) => {
|
|
2564
3072
|
if (i % step !== 0 && i !== data.length - 1) return null;
|
|
@@ -2568,7 +3076,7 @@ function AreaChart({
|
|
|
2568
3076
|
x: xp(i),
|
|
2569
3077
|
y: H - 6,
|
|
2570
3078
|
textAnchor: "middle",
|
|
2571
|
-
fill:
|
|
3079
|
+
fill: T.textMuted,
|
|
2572
3080
|
fontSize: "9",
|
|
2573
3081
|
children: d.label
|
|
2574
3082
|
},
|
|
@@ -2578,7 +3086,7 @@ function AreaChart({
|
|
|
2578
3086
|
data.map((d, i) => {
|
|
2579
3087
|
const isHov = hovered === i;
|
|
2580
3088
|
return (
|
|
2581
|
-
// biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
|
|
3089
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
|
|
2582
3090
|
/* @__PURE__ */ jsxs(
|
|
2583
3091
|
"g",
|
|
2584
3092
|
{
|
|
@@ -2603,10 +3111,9 @@ function AreaChart({
|
|
|
2603
3111
|
y1: PT,
|
|
2604
3112
|
x2: xp(i),
|
|
2605
3113
|
y2: PT + CH,
|
|
2606
|
-
stroke: "
|
|
3114
|
+
stroke: "rgba(50,50,77,0.2)",
|
|
2607
3115
|
strokeWidth: "1",
|
|
2608
|
-
strokeDasharray: "3 3"
|
|
2609
|
-
opacity: "0.35"
|
|
3116
|
+
strokeDasharray: "3 3"
|
|
2610
3117
|
}
|
|
2611
3118
|
),
|
|
2612
3119
|
lines.map((s) => /* @__PURE__ */ jsx(
|
|
@@ -2616,11 +3123,11 @@ function AreaChart({
|
|
|
2616
3123
|
cy: yp(d[s.key]),
|
|
2617
3124
|
r: isHov ? 4 : 2.5,
|
|
2618
3125
|
fill: s.color,
|
|
2619
|
-
stroke:
|
|
3126
|
+
stroke: T.bg,
|
|
2620
3127
|
strokeWidth: isHov ? 2 : 1,
|
|
2621
|
-
opacity: isHov ? 1 : 0.
|
|
3128
|
+
opacity: isHov ? 1 : 0.5
|
|
2622
3129
|
},
|
|
2623
|
-
s.
|
|
3130
|
+
s.key
|
|
2624
3131
|
))
|
|
2625
3132
|
]
|
|
2626
3133
|
},
|
|
@@ -2632,23 +3139,70 @@ function AreaChart({
|
|
|
2632
3139
|
}
|
|
2633
3140
|
);
|
|
2634
3141
|
}
|
|
3142
|
+
function ProgressRing({
|
|
3143
|
+
value,
|
|
3144
|
+
max,
|
|
3145
|
+
color,
|
|
3146
|
+
size = 56
|
|
3147
|
+
}) {
|
|
3148
|
+
const r = (size - 9) / 2;
|
|
3149
|
+
const circ = 2 * Math.PI * r;
|
|
3150
|
+
const pct = Math.min(value / Math.max(max, 1), 1);
|
|
3151
|
+
const offset = circ * (1 - pct);
|
|
3152
|
+
return /* @__PURE__ */ jsxs(
|
|
3153
|
+
"svg",
|
|
3154
|
+
{
|
|
3155
|
+
width: size,
|
|
3156
|
+
height: size,
|
|
3157
|
+
style: { flexShrink: 0 },
|
|
3158
|
+
"aria-hidden": true,
|
|
3159
|
+
children: [
|
|
3160
|
+
/* @__PURE__ */ jsx(
|
|
3161
|
+
"circle",
|
|
3162
|
+
{
|
|
3163
|
+
cx: size / 2,
|
|
3164
|
+
cy: size / 2,
|
|
3165
|
+
r,
|
|
3166
|
+
fill: "none",
|
|
3167
|
+
stroke: "#e5e7eb",
|
|
3168
|
+
strokeWidth: "7"
|
|
3169
|
+
}
|
|
3170
|
+
),
|
|
3171
|
+
/* @__PURE__ */ jsx(
|
|
3172
|
+
"circle",
|
|
3173
|
+
{
|
|
3174
|
+
cx: size / 2,
|
|
3175
|
+
cy: size / 2,
|
|
3176
|
+
r,
|
|
3177
|
+
fill: "none",
|
|
3178
|
+
stroke: color,
|
|
3179
|
+
strokeWidth: "7",
|
|
3180
|
+
strokeDasharray: `${circ} ${circ}`,
|
|
3181
|
+
strokeDashoffset: offset,
|
|
3182
|
+
strokeLinecap: "round",
|
|
3183
|
+
transform: `rotate(-90 ${size / 2} ${size / 2})`,
|
|
3184
|
+
style: {
|
|
3185
|
+
transition: "stroke-dashoffset 0.9s cubic-bezier(0.4,0,0.2,1)"
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
)
|
|
3189
|
+
]
|
|
3190
|
+
}
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
2635
3193
|
function relTime(date) {
|
|
2636
|
-
const
|
|
2637
|
-
const
|
|
2638
|
-
if (
|
|
2639
|
-
if (
|
|
2640
|
-
const
|
|
2641
|
-
if (
|
|
2642
|
-
return `${Math.floor(
|
|
3194
|
+
const diff = Date.now() - new Date(date).getTime();
|
|
3195
|
+
const mins = Math.floor(diff / 6e4);
|
|
3196
|
+
if (mins < 1) return "just now";
|
|
3197
|
+
if (mins < 60) return `${mins}m`;
|
|
3198
|
+
const hrs = Math.floor(mins / 60);
|
|
3199
|
+
if (hrs < 24) return `${hrs}h`;
|
|
3200
|
+
return `${Math.floor(hrs / 24)}d`;
|
|
2643
3201
|
}
|
|
2644
|
-
function
|
|
2645
|
-
return
|
|
2646
|
-
month: "short",
|
|
2647
|
-
day: "numeric",
|
|
2648
|
-
year: "numeric"
|
|
2649
|
-
});
|
|
3202
|
+
function rateHue(r) {
|
|
3203
|
+
return r >= 70 ? 142 : r >= 40 ? 38 : 4;
|
|
2650
3204
|
}
|
|
2651
|
-
function
|
|
3205
|
+
function StatItem({
|
|
2652
3206
|
id,
|
|
2653
3207
|
label,
|
|
2654
3208
|
value,
|
|
@@ -2657,26 +3211,39 @@ function StatCardItem({
|
|
|
2657
3211
|
color,
|
|
2658
3212
|
delay = 0
|
|
2659
3213
|
}) {
|
|
3214
|
+
const animated = useCountUp(value);
|
|
2660
3215
|
const isPos = pct === void 0 || pct >= 0;
|
|
2661
|
-
return /* @__PURE__ */ jsxs(
|
|
2662
|
-
/* @__PURE__ */
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
"%"
|
|
2670
|
-
] })
|
|
3216
|
+
return /* @__PURE__ */ jsxs(StatCard, { $delay: delay, $accent: color, children: [
|
|
3217
|
+
/* @__PURE__ */ jsx(StatLabel, { children: label }),
|
|
3218
|
+
/* @__PURE__ */ jsx(StatValue, { children: animated.toLocaleString() }),
|
|
3219
|
+
pct !== void 0 && /* @__PURE__ */ jsxs(TrendBadge, { $pos: isPos, children: [
|
|
3220
|
+
isPos ? "↑" : "↓",
|
|
3221
|
+
" ",
|
|
3222
|
+
Math.abs(pct).toFixed(1),
|
|
3223
|
+
"%"
|
|
2671
3224
|
] }),
|
|
2672
3225
|
sparkline && sparkline.length > 1 && /* @__PURE__ */ jsx(SparkWrap, { children: /* @__PURE__ */ jsx(Sparkline, { data: sparkline, color, id }) })
|
|
2673
3226
|
] });
|
|
2674
3227
|
}
|
|
2675
3228
|
function OverviewPage() {
|
|
3229
|
+
const { get } = useFetchClient();
|
|
2676
3230
|
const [period, setPeriod] = useState(
|
|
2677
3231
|
"weekly"
|
|
2678
3232
|
);
|
|
3233
|
+
const [feedMode, setFeedMode] = useState("signups");
|
|
2679
3234
|
const [hovIdx, setHovIdx] = useState(null);
|
|
3235
|
+
const [rtnHov, setRtnHov] = useState(null);
|
|
3236
|
+
const [activeSeries, setActiveSeries] = useState(
|
|
3237
|
+
() => /* @__PURE__ */ new Set(["totalUsers", "newUsers", "activeUsers"])
|
|
3238
|
+
);
|
|
3239
|
+
const toggleSeries = (key) => {
|
|
3240
|
+
setActiveSeries((prev) => {
|
|
3241
|
+
const next = new Set(prev);
|
|
3242
|
+
if (next.has(key) && next.size > 1) next.delete(key);
|
|
3243
|
+
else next.add(key);
|
|
3244
|
+
return next;
|
|
3245
|
+
});
|
|
3246
|
+
};
|
|
2680
3247
|
const statsQuery = useQuery({
|
|
2681
3248
|
queryKey: ["dash-user-stats"],
|
|
2682
3249
|
queryFn: async () => {
|
|
@@ -2693,12 +3260,10 @@ function OverviewPage() {
|
|
|
2693
3260
|
return r.data;
|
|
2694
3261
|
}
|
|
2695
3262
|
});
|
|
2696
|
-
const
|
|
2697
|
-
queryKey: ["dash-
|
|
3263
|
+
const retentionQuery = useQuery({
|
|
3264
|
+
queryKey: ["dash-user-retention", period],
|
|
2698
3265
|
queryFn: async () => {
|
|
2699
|
-
const r = await client.dash.
|
|
2700
|
-
query: { page: 1, limit: 10 }
|
|
2701
|
-
});
|
|
3266
|
+
const r = await client.dash.userRetentionData({ query: { period } });
|
|
2702
3267
|
if (r.error) throw new Error(r.error.message ?? "Failed");
|
|
2703
3268
|
return r.data;
|
|
2704
3269
|
}
|
|
@@ -2707,12 +3272,22 @@ function OverviewPage() {
|
|
|
2707
3272
|
queryKey: ["dash-recent-users"],
|
|
2708
3273
|
queryFn: async () => {
|
|
2709
3274
|
const r = await client.dash.listUsers({
|
|
2710
|
-
query: {
|
|
3275
|
+
query: { limit: 12, offset: 0, sortBy: "createdAt", sortOrder: "desc" }
|
|
2711
3276
|
});
|
|
2712
3277
|
if (r.error) throw new Error(r.error.message ?? "Failed");
|
|
2713
3278
|
return r.data;
|
|
2714
3279
|
}
|
|
2715
3280
|
});
|
|
3281
|
+
const sessionsQuery = useQuery({
|
|
3282
|
+
queryKey: ["dash-recent-sessions"],
|
|
3283
|
+
queryFn: async () => {
|
|
3284
|
+
const { data } = await get(
|
|
3285
|
+
"/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=createdAt:desc"
|
|
3286
|
+
);
|
|
3287
|
+
return data.results ?? [];
|
|
3288
|
+
},
|
|
3289
|
+
refetchInterval: 3e4
|
|
3290
|
+
});
|
|
2716
3291
|
const orgsQuery = useQuery({
|
|
2717
3292
|
queryKey: ["dash-orgs-count"],
|
|
2718
3293
|
queryFn: async () => {
|
|
@@ -2723,595 +3298,385 @@ function OverviewPage() {
|
|
|
2723
3298
|
return r.data;
|
|
2724
3299
|
}
|
|
2725
3300
|
});
|
|
3301
|
+
const sessionsRaw = sessionsQuery.data ?? [];
|
|
3302
|
+
const activeUserIds = [];
|
|
3303
|
+
const lastActiveByUserId = /* @__PURE__ */ new Map();
|
|
3304
|
+
for (const s of sessionsRaw) {
|
|
3305
|
+
if (!lastActiveByUserId.has(s.userId)) {
|
|
3306
|
+
lastActiveByUserId.set(s.userId, s.createdAt);
|
|
3307
|
+
activeUserIds.push(s.userId);
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
const activeUsersQuery = useQuery({
|
|
3311
|
+
queryKey: ["dash-active-users", activeUserIds],
|
|
3312
|
+
queryFn: async () => {
|
|
3313
|
+
if (activeUserIds.length === 0) return [];
|
|
3314
|
+
const params = new URLSearchParams({ uid: "plugin::better-auth.user" });
|
|
3315
|
+
for (let i = 0; i < activeUserIds.length; i++) {
|
|
3316
|
+
params.set(`filters[id][$in][${i}]`, activeUserIds[i]);
|
|
3317
|
+
}
|
|
3318
|
+
params.set("pagination[pageSize]", "12");
|
|
3319
|
+
const { data } = await get(
|
|
3320
|
+
`/better-auth-dashboard/db?${params}`
|
|
3321
|
+
);
|
|
3322
|
+
return data.results ?? [];
|
|
3323
|
+
},
|
|
3324
|
+
enabled: feedMode === "active" && activeUserIds.length > 0
|
|
3325
|
+
});
|
|
3326
|
+
const chartRef = useRef(null);
|
|
3327
|
+
const [chartHeight, setChartHeight] = useState(0);
|
|
3328
|
+
useEffect(() => {
|
|
3329
|
+
const el = chartRef.current;
|
|
3330
|
+
if (!el) return;
|
|
3331
|
+
const ro = new ResizeObserver((entries) => {
|
|
3332
|
+
for (const entry of entries) setChartHeight(entry.contentRect.height);
|
|
3333
|
+
});
|
|
3334
|
+
ro.observe(el);
|
|
3335
|
+
return () => ro.disconnect();
|
|
3336
|
+
}, []);
|
|
2726
3337
|
if (statsQuery.isLoading) {
|
|
2727
|
-
return /* @__PURE__ */ jsx(
|
|
3338
|
+
return /* @__PURE__ */ jsx(
|
|
3339
|
+
Flex,
|
|
3340
|
+
{
|
|
3341
|
+
justifyContent: "center",
|
|
3342
|
+
alignItems: "center",
|
|
3343
|
+
padding: 12,
|
|
3344
|
+
style: { background: T.bg, minHeight: "100%" },
|
|
3345
|
+
children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
|
|
3346
|
+
}
|
|
3347
|
+
);
|
|
2728
3348
|
}
|
|
2729
3349
|
const stats = statsQuery.data;
|
|
2730
3350
|
if (!stats) return null;
|
|
2731
3351
|
const graphData = graphQuery.data?.data ?? [];
|
|
2732
|
-
const
|
|
2733
|
-
const
|
|
3352
|
+
const rtnData = retentionQuery.data?.data ?? [];
|
|
3353
|
+
const users = usersQuery.data?.users ?? [];
|
|
2734
3354
|
const orgCount = orgsQuery.data?.total ?? 0;
|
|
3355
|
+
const activeUserMap = /* @__PURE__ */ new Map();
|
|
3356
|
+
for (const u of activeUsersQuery.data ?? []) {
|
|
3357
|
+
activeUserMap.set(String(u.id), u);
|
|
3358
|
+
}
|
|
3359
|
+
const sortedActiveUsers = activeUserIds.map((uid) => activeUserMap.get(uid)).filter((u) => u !== void 0);
|
|
2735
3360
|
const totalSpark = graphData.map((d) => d.totalUsers);
|
|
2736
3361
|
const newSpark = graphData.map((d) => d.newUsers);
|
|
2737
3362
|
const activeSpark = graphData.map((d) => d.activeUsers);
|
|
2738
3363
|
const hovRow = hovIdx !== null ? graphData[hovIdx] : null;
|
|
2739
|
-
|
|
3364
|
+
const rtnHovRow = rtnHov !== null ? rtnData[rtnHov] : null;
|
|
3365
|
+
const activeMax = Math.max(
|
|
3366
|
+
stats.activeUsers.daily.active ?? 0,
|
|
3367
|
+
stats.activeUsers.weekly.active ?? 0,
|
|
3368
|
+
stats.activeUsers.monthly.active ?? 0,
|
|
3369
|
+
1
|
|
3370
|
+
);
|
|
3371
|
+
return /* @__PURE__ */ jsxs(Wrap$1, { "data-testid": "overview-page", children: [
|
|
2740
3372
|
/* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-end", children: [
|
|
2741
|
-
/* @__PURE__ */ jsxs(
|
|
2742
|
-
/* @__PURE__ */ jsx(
|
|
2743
|
-
|
|
3373
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
3374
|
+
/* @__PURE__ */ jsx(
|
|
3375
|
+
"div",
|
|
3376
|
+
{
|
|
3377
|
+
style: {
|
|
3378
|
+
fontSize: 22,
|
|
3379
|
+
fontWeight: 800,
|
|
3380
|
+
color: T.textPrimary,
|
|
3381
|
+
letterSpacing: "-0.03em"
|
|
3382
|
+
},
|
|
3383
|
+
children: "Overview"
|
|
3384
|
+
}
|
|
3385
|
+
),
|
|
3386
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: T.textSecondary, marginTop: 4 }, children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
|
|
2744
3387
|
weekday: "long",
|
|
2745
3388
|
year: "numeric",
|
|
2746
3389
|
month: "long",
|
|
2747
3390
|
day: "numeric"
|
|
2748
|
-
}) })
|
|
3391
|
+
}) })
|
|
2749
3392
|
] }),
|
|
2750
|
-
/* @__PURE__ */ jsx(
|
|
2751
|
-
SingleSelect,
|
|
2752
|
-
{
|
|
2753
|
-
value: period,
|
|
2754
|
-
onChange: (v) => setPeriod(v),
|
|
2755
|
-
size: "S",
|
|
2756
|
-
"aria-label": "Select period",
|
|
2757
|
-
children: [
|
|
2758
|
-
/* @__PURE__ */ jsx(SingleSelectOption, { value: "daily", children: "Daily" }),
|
|
2759
|
-
/* @__PURE__ */ jsx(SingleSelectOption, { value: "weekly", children: "Weekly" }),
|
|
2760
|
-
/* @__PURE__ */ jsx(SingleSelectOption, { value: "monthly", children: "Monthly" })
|
|
2761
|
-
]
|
|
2762
|
-
}
|
|
2763
|
-
) })
|
|
3393
|
+
/* @__PURE__ */ jsx(PillGroup, { children: ["daily", "weekly", "monthly"].map((p) => /* @__PURE__ */ jsx(Pill, { $active: period === p, onClick: () => setPeriod(p), children: p[0].toUpperCase() + p.slice(1) }, p)) })
|
|
2764
3394
|
] }),
|
|
2765
|
-
/* @__PURE__ */ jsxs(
|
|
2766
|
-
/* @__PURE__ */ jsx(DivLabel, { children: "
|
|
3395
|
+
/* @__PURE__ */ jsxs(SectionDivider, { children: [
|
|
3396
|
+
/* @__PURE__ */ jsx(DivLabel, { children: "Metrics" }),
|
|
2767
3397
|
/* @__PURE__ */ jsx(DivLine, {})
|
|
2768
3398
|
] }),
|
|
2769
|
-
/* @__PURE__ */ jsxs(
|
|
3399
|
+
/* @__PURE__ */ jsxs(StatGrid, { children: [
|
|
2770
3400
|
/* @__PURE__ */ jsx(
|
|
2771
|
-
|
|
3401
|
+
StatItem,
|
|
2772
3402
|
{
|
|
2773
3403
|
id: "total",
|
|
2774
3404
|
label: "Total Users",
|
|
2775
3405
|
value: stats.total ?? 0,
|
|
2776
3406
|
sparkline: totalSpark,
|
|
2777
|
-
color:
|
|
3407
|
+
color: T.accent,
|
|
2778
3408
|
delay: 0
|
|
2779
3409
|
}
|
|
2780
3410
|
),
|
|
2781
3411
|
/* @__PURE__ */ jsx(
|
|
2782
|
-
|
|
3412
|
+
StatItem,
|
|
2783
3413
|
{
|
|
2784
3414
|
id: "d-sig",
|
|
2785
3415
|
label: "Daily Sign-ups",
|
|
2786
3416
|
value: stats.daily.signUps ?? 0,
|
|
2787
3417
|
pct: stats.daily.percentage ?? void 0,
|
|
2788
3418
|
sparkline: newSpark,
|
|
2789
|
-
color:
|
|
3419
|
+
color: T.green,
|
|
2790
3420
|
delay: 1
|
|
2791
3421
|
}
|
|
2792
3422
|
),
|
|
2793
3423
|
/* @__PURE__ */ jsx(
|
|
2794
|
-
|
|
3424
|
+
StatItem,
|
|
2795
3425
|
{
|
|
2796
3426
|
id: "w-sig",
|
|
2797
3427
|
label: "Weekly Sign-ups",
|
|
2798
3428
|
value: stats.weekly.signUps ?? 0,
|
|
2799
3429
|
pct: stats.weekly.percentage ?? void 0,
|
|
2800
3430
|
sparkline: newSpark,
|
|
2801
|
-
color:
|
|
3431
|
+
color: T.green,
|
|
2802
3432
|
delay: 2
|
|
2803
3433
|
}
|
|
2804
3434
|
),
|
|
2805
3435
|
/* @__PURE__ */ jsx(
|
|
2806
|
-
|
|
3436
|
+
StatItem,
|
|
2807
3437
|
{
|
|
2808
3438
|
id: "m-sig",
|
|
2809
3439
|
label: "Monthly Sign-ups",
|
|
2810
3440
|
value: stats.monthly.signUps ?? 0,
|
|
2811
3441
|
pct: stats.monthly.percentage ?? void 0,
|
|
2812
3442
|
sparkline: newSpark,
|
|
2813
|
-
color:
|
|
3443
|
+
color: T.green,
|
|
2814
3444
|
delay: 3
|
|
2815
3445
|
}
|
|
2816
3446
|
),
|
|
2817
3447
|
/* @__PURE__ */ jsx(
|
|
2818
|
-
|
|
3448
|
+
StatItem,
|
|
2819
3449
|
{
|
|
2820
3450
|
id: "orgs",
|
|
2821
3451
|
label: "Organizations",
|
|
2822
3452
|
value: orgCount,
|
|
2823
|
-
color:
|
|
3453
|
+
color: T.purple,
|
|
2824
3454
|
delay: 4
|
|
2825
3455
|
}
|
|
2826
3456
|
)
|
|
2827
3457
|
] }),
|
|
2828
|
-
/* @__PURE__ */ jsxs(
|
|
2829
|
-
/* @__PURE__ */
|
|
3458
|
+
/* @__PURE__ */ jsxs(SectionDivider, { children: [
|
|
3459
|
+
/* @__PURE__ */ jsx(DivLabel, { children: "Growth" }),
|
|
3460
|
+
/* @__PURE__ */ jsx(DivLine, {})
|
|
3461
|
+
] }),
|
|
3462
|
+
/* @__PURE__ */ jsxs(TwoPanel, { children: [
|
|
3463
|
+
/* @__PURE__ */ jsxs(ChartCard, { ref: chartRef, $delay: 5, children: [
|
|
2830
3464
|
/* @__PURE__ */ jsxs(ChartHeader, { children: [
|
|
2831
3465
|
/* @__PURE__ */ jsx(ChartTitle, { children: "User Growth" }),
|
|
2832
|
-
/* @__PURE__ */ jsx(
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
3466
|
+
/* @__PURE__ */ jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxs(
|
|
3467
|
+
SeriesBtn,
|
|
3468
|
+
{
|
|
3469
|
+
$on: activeSeries.has(s.key),
|
|
3470
|
+
$c: s.color,
|
|
3471
|
+
onClick: () => toggleSeries(s.key),
|
|
3472
|
+
children: [
|
|
3473
|
+
/* @__PURE__ */ jsx(SDot, { $c: s.color }),
|
|
3474
|
+
s.label
|
|
3475
|
+
]
|
|
3476
|
+
},
|
|
3477
|
+
s.key
|
|
3478
|
+
)) })
|
|
3479
|
+
] }),
|
|
3480
|
+
/* @__PURE__ */ jsx(HoverInfo, { children: hovRow ? `${hovRow.label} · ${hovRow.totalUsers.toLocaleString()} total · +${hovRow.newUsers.toLocaleString()} new · ${hovRow.activeUsers.toLocaleString()} active` : "Hover the chart to inspect a data point" }),
|
|
2838
3481
|
graphQuery.isLoading ? /* @__PURE__ */ jsx(
|
|
2839
3482
|
Flex,
|
|
2840
3483
|
{
|
|
2841
3484
|
justifyContent: "center",
|
|
2842
3485
|
alignItems: "center",
|
|
2843
|
-
style: { flex: 1, minHeight:
|
|
3486
|
+
style: { flex: 1, minHeight: 170 },
|
|
2844
3487
|
children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
|
|
2845
3488
|
}
|
|
2846
|
-
) : /* @__PURE__ */ jsx(
|
|
3489
|
+
) : /* @__PURE__ */ jsx(
|
|
3490
|
+
GrowthChart,
|
|
3491
|
+
{
|
|
3492
|
+
data: graphData,
|
|
3493
|
+
hovered: hovIdx,
|
|
3494
|
+
onHover: setHovIdx,
|
|
3495
|
+
activeSeries
|
|
3496
|
+
}
|
|
3497
|
+
)
|
|
2847
3498
|
] }),
|
|
2848
|
-
/* @__PURE__ */ jsxs(
|
|
2849
|
-
|
|
2850
|
-
/* @__PURE__ */ jsx(FeedList, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Empty, { children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : sessions.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No recent sessions" }) : sessions.map((s) => /* @__PURE__ */ jsxs(FeedItem, { children: [
|
|
2851
|
-
/* @__PURE__ */ jsx(
|
|
2852
|
-
Avatar,
|
|
2853
|
-
{
|
|
2854
|
-
name: s.user?.name ?? "?",
|
|
2855
|
-
src: s.user?.image,
|
|
2856
|
-
size: 28
|
|
2857
|
-
}
|
|
2858
|
-
),
|
|
2859
|
-
/* @__PURE__ */ jsxs(FeedContent, { children: [
|
|
2860
|
-
/* @__PURE__ */ jsx(FeedName, { children: s.user?.name ?? "Unknown" }),
|
|
2861
|
-
/* @__PURE__ */ jsx(FeedSub, { children: s.user?.email ?? "" }),
|
|
2862
|
-
/* @__PURE__ */ jsx(FeedTime, { children: relTime(s.createdAt) })
|
|
2863
|
-
] })
|
|
2864
|
-
] }, s.id)) })
|
|
2865
|
-
] })
|
|
2866
|
-
] }),
|
|
2867
|
-
/* @__PURE__ */ jsxs(Divider, { children: [
|
|
2868
|
-
/* @__PURE__ */ jsx(DivLabel, { children: "Active Users" }),
|
|
2869
|
-
/* @__PURE__ */ jsx(DivLine, {})
|
|
2870
|
-
] }),
|
|
2871
|
-
/* @__PURE__ */ jsxs(ActiveRow, { children: [
|
|
2872
|
-
/* @__PURE__ */ jsx(
|
|
2873
|
-
StatCardItem,
|
|
2874
|
-
{
|
|
2875
|
-
id: "d-act",
|
|
2876
|
-
label: "Daily Active",
|
|
2877
|
-
value: stats.activeUsers.daily.active ?? 0,
|
|
2878
|
-
pct: stats.activeUsers.daily.percentage ?? void 0,
|
|
2879
|
-
sparkline: activeSpark,
|
|
2880
|
-
color: "#E57553",
|
|
2881
|
-
delay: 7
|
|
2882
|
-
}
|
|
2883
|
-
),
|
|
2884
|
-
/* @__PURE__ */ jsx(
|
|
2885
|
-
StatCardItem,
|
|
2886
|
-
{
|
|
2887
|
-
id: "w-act",
|
|
2888
|
-
label: "Weekly Active",
|
|
2889
|
-
value: stats.activeUsers.weekly.active ?? 0,
|
|
2890
|
-
pct: stats.activeUsers.weekly.percentage ?? void 0,
|
|
2891
|
-
sparkline: activeSpark,
|
|
2892
|
-
color: "#E57553",
|
|
2893
|
-
delay: 8
|
|
2894
|
-
}
|
|
2895
|
-
),
|
|
2896
|
-
/* @__PURE__ */ jsx(
|
|
2897
|
-
StatCardItem,
|
|
3499
|
+
/* @__PURE__ */ jsxs(
|
|
3500
|
+
FeedCard,
|
|
2898
3501
|
{
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
3502
|
+
$delay: 6,
|
|
3503
|
+
style: chartHeight > 0 ? { height: chartHeight } : void 0,
|
|
3504
|
+
children: [
|
|
3505
|
+
/* @__PURE__ */ jsxs(FeedHead, { children: [
|
|
3506
|
+
/* @__PURE__ */ jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recently Active" }),
|
|
3507
|
+
/* @__PURE__ */ jsxs(
|
|
3508
|
+
FeedSelect,
|
|
3509
|
+
{
|
|
3510
|
+
value: feedMode,
|
|
3511
|
+
onChange: (e) => setFeedMode(e.target.value),
|
|
3512
|
+
children: [
|
|
3513
|
+
/* @__PURE__ */ jsx("option", { value: "signups", children: "Recent Sign-ups" }),
|
|
3514
|
+
/* @__PURE__ */ jsx("option", { value: "active", children: "Recently Active" })
|
|
3515
|
+
]
|
|
3516
|
+
}
|
|
3517
|
+
)
|
|
3518
|
+
] }),
|
|
3519
|
+
/* @__PURE__ */ jsx(FeedScroll, { children: feedMode === "signups" ? usersQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : users.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No users yet" }) : users.map((u) => /* @__PURE__ */ jsxs(FeedItem, { children: [
|
|
3520
|
+
/* @__PURE__ */ jsxs(FeedTop, { children: [
|
|
3521
|
+
/* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
|
|
3522
|
+
/* @__PURE__ */ jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
|
|
3523
|
+
/* @__PURE__ */ jsx(FeedName, { title: u.name, children: u.name })
|
|
3524
|
+
] }),
|
|
3525
|
+
/* @__PURE__ */ jsx(FeedMeta, { children: relTime(u.createdAt) })
|
|
3526
|
+
] }),
|
|
3527
|
+
/* @__PURE__ */ jsx(FeedEmail, { title: u.email, children: u.email })
|
|
3528
|
+
] }, u.id)) : activeUsersQuery.isLoading || sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : sortedActiveUsers.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No recent activity" }) : sortedActiveUsers.map((u) => /* @__PURE__ */ jsxs(FeedItem, { children: [
|
|
3529
|
+
/* @__PURE__ */ jsxs(FeedTop, { children: [
|
|
3530
|
+
/* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
|
|
3531
|
+
/* @__PURE__ */ jsx(
|
|
3532
|
+
Avatar,
|
|
3533
|
+
{
|
|
3534
|
+
name: u.name,
|
|
3535
|
+
src: u.image ?? void 0,
|
|
3536
|
+
size: 20
|
|
3537
|
+
}
|
|
3538
|
+
),
|
|
3539
|
+
/* @__PURE__ */ jsx(FeedName, { title: u.name, children: u.name })
|
|
3540
|
+
] }),
|
|
3541
|
+
/* @__PURE__ */ jsx(FeedMeta, { children: relTime(
|
|
3542
|
+
lastActiveByUserId.get(String(u.id)) ?? u.createdAt
|
|
3543
|
+
) })
|
|
3544
|
+
] }),
|
|
3545
|
+
/* @__PURE__ */ jsx(FeedEmail, { title: u.email, children: u.email })
|
|
3546
|
+
] }, u.documentId)) })
|
|
3547
|
+
]
|
|
2906
3548
|
}
|
|
2907
3549
|
)
|
|
2908
3550
|
] }),
|
|
2909
|
-
/* @__PURE__ */ jsxs(
|
|
2910
|
-
/* @__PURE__ */ jsx(DivLabel, { children: "
|
|
3551
|
+
/* @__PURE__ */ jsxs(SectionDivider, { children: [
|
|
3552
|
+
/* @__PURE__ */ jsx(DivLabel, { children: "Retention & Activity" }),
|
|
2911
3553
|
/* @__PURE__ */ jsx(DivLine, {})
|
|
2912
3554
|
] }),
|
|
2913
|
-
/* @__PURE__ */
|
|
2914
|
-
/* @__PURE__ */
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
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
|
-
TD$1,
|
|
2944
|
-
{
|
|
2945
|
-
style: {
|
|
2946
|
-
color: "#8e8ea9",
|
|
2947
|
-
fontSize: 11,
|
|
2948
|
-
whiteSpace: "nowrap"
|
|
3555
|
+
/* @__PURE__ */ jsxs(TwoPanel, { children: [
|
|
3556
|
+
/* @__PURE__ */ jsxs(RtnCard, { $delay: 7, children: [
|
|
3557
|
+
/* @__PURE__ */ jsxs(ChartHeader, { children: [
|
|
3558
|
+
/* @__PURE__ */ jsx(ChartTitle, { children: "Cohort Retention" }),
|
|
3559
|
+
/* @__PURE__ */ jsx(Flex, { alignItems: "center", gap: 2, children: [
|
|
3560
|
+
{ hue: 142, label: "≥70%" },
|
|
3561
|
+
{ hue: 38, label: "40–70%" },
|
|
3562
|
+
{ hue: 4, label: "<40%" }
|
|
3563
|
+
].map(({ hue, label }) => /* @__PURE__ */ jsxs(
|
|
3564
|
+
Flex,
|
|
3565
|
+
{
|
|
3566
|
+
alignItems: "center",
|
|
3567
|
+
gap: 1,
|
|
3568
|
+
style: { marginLeft: 8 },
|
|
3569
|
+
children: [
|
|
3570
|
+
/* @__PURE__ */ jsx(
|
|
3571
|
+
"span",
|
|
3572
|
+
{
|
|
3573
|
+
style: {
|
|
3574
|
+
width: 8,
|
|
3575
|
+
height: 8,
|
|
3576
|
+
borderRadius: "50%",
|
|
3577
|
+
background: `hsl(${hue},60%,50%)`,
|
|
3578
|
+
display: "inline-block",
|
|
3579
|
+
flexShrink: 0
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
),
|
|
3583
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: T.textMuted }, children: label })
|
|
3584
|
+
]
|
|
2949
3585
|
},
|
|
2950
|
-
|
|
2951
|
-
}
|
|
2952
|
-
),
|
|
2953
|
-
/* @__PURE__ */ jsx(
|
|
2954
|
-
|
|
3586
|
+
hue
|
|
3587
|
+
)) })
|
|
3588
|
+
] }),
|
|
3589
|
+
/* @__PURE__ */ jsx(RtnTip, { children: rtnHovRow ? `Cohort ${rtnHovRow.label} · ${rtnHovRow.cohortSize.toLocaleString()} users · active ${rtnHovRow.activeStart} – ${rtnHovRow.activeEnd}` : "Hover a row to see cohort details" }),
|
|
3590
|
+
retentionQuery.isLoading ? /* @__PURE__ */ jsx(
|
|
3591
|
+
Flex,
|
|
2955
3592
|
{
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
3593
|
+
justifyContent: "center",
|
|
3594
|
+
alignItems: "center",
|
|
3595
|
+
style: { minHeight: 80 },
|
|
3596
|
+
children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
|
|
2959
3597
|
}
|
|
2960
|
-
)
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
display: flex;
|
|
2985
|
-
flex-direction: column;
|
|
2986
|
-
gap: 4px;
|
|
2987
|
-
`;
|
|
2988
|
-
const PageTitle$1 = styled.h1`
|
|
2989
|
-
margin: 0;
|
|
2990
|
-
font-size: 22px;
|
|
2991
|
-
font-weight: 800;
|
|
2992
|
-
color: #32324d;
|
|
2993
|
-
letter-spacing: -0.03em;
|
|
2994
|
-
`;
|
|
2995
|
-
const PageSubtitle$1 = styled.p`
|
|
2996
|
-
margin: 0;
|
|
2997
|
-
font-size: 12px;
|
|
2998
|
-
color: #8e8ea9;
|
|
2999
|
-
`;
|
|
3000
|
-
const TableCard$1 = styled.div`
|
|
3001
|
-
background: #ffffff;
|
|
3002
|
-
border: 1px solid #eaeaef;
|
|
3003
|
-
border-radius: 10px;
|
|
3004
|
-
overflow: hidden;
|
|
3005
|
-
`;
|
|
3006
|
-
const ColumnHeader = styled.div`
|
|
3007
|
-
padding: 10px 20px;
|
|
3008
|
-
display: flex;
|
|
3009
|
-
align-items: center;
|
|
3010
|
-
gap: 12px;
|
|
3011
|
-
background: #fafafa;
|
|
3012
|
-
border-bottom: 1px solid #eaeaef;
|
|
3013
|
-
font-size: 10px;
|
|
3014
|
-
font-weight: 700;
|
|
3015
|
-
text-transform: uppercase;
|
|
3016
|
-
letter-spacing: 0.08em;
|
|
3017
|
-
color: #8e8ea9;
|
|
3018
|
-
`;
|
|
3019
|
-
const ColumnHeaderRight = styled.div`
|
|
3020
|
-
margin-left: auto;
|
|
3021
|
-
font-size: 10px;
|
|
3022
|
-
font-weight: 700;
|
|
3023
|
-
text-transform: uppercase;
|
|
3024
|
-
letter-spacing: 0.08em;
|
|
3025
|
-
color: #8e8ea9;
|
|
3026
|
-
`;
|
|
3027
|
-
const UserGroup = styled.div`
|
|
3028
|
-
border-bottom: 1px solid #eaeaef;
|
|
3029
|
-
animation: ${fadeUp$1} 280ms ease both;
|
|
3030
|
-
animation-delay: ${(p) => (p.$i ?? 0) * 30}ms;
|
|
3031
|
-
&:last-child { border-bottom: none; }
|
|
3032
|
-
`;
|
|
3033
|
-
const UserRowHeader = styled.div`
|
|
3034
|
-
padding: 14px 20px;
|
|
3035
|
-
display: flex;
|
|
3036
|
-
align-items: center;
|
|
3037
|
-
gap: 12px;
|
|
3038
|
-
border-bottom: 1px solid #f5f5f9;
|
|
3039
|
-
cursor: default;
|
|
3040
|
-
&:hover { background: #fafafe; }
|
|
3041
|
-
`;
|
|
3042
|
-
const UserInfo = styled.div`
|
|
3043
|
-
flex: 1;
|
|
3044
|
-
display: flex;
|
|
3045
|
-
flex-direction: column;
|
|
3046
|
-
gap: 2px;
|
|
3047
|
-
`;
|
|
3048
|
-
const UserName$1 = styled.span`
|
|
3049
|
-
font-size: 12px;
|
|
3050
|
-
font-weight: 600;
|
|
3051
|
-
color: #32324d;
|
|
3052
|
-
`;
|
|
3053
|
-
const UserEmail = styled.span`
|
|
3054
|
-
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
3055
|
-
font-size: 11px;
|
|
3056
|
-
color: #8e8ea9;
|
|
3057
|
-
`;
|
|
3058
|
-
const SessionCountChip = styled.span`
|
|
3059
|
-
display: inline-flex;
|
|
3060
|
-
align-items: center;
|
|
3061
|
-
padding: 2px 8px;
|
|
3062
|
-
border-radius: 20px;
|
|
3063
|
-
font-size: 10px;
|
|
3064
|
-
font-weight: 700;
|
|
3065
|
-
background: #f0f0ff;
|
|
3066
|
-
color: #4945ff;
|
|
3067
|
-
`;
|
|
3068
|
-
const SessionCard = styled.div`
|
|
3069
|
-
padding: 10px 20px 10px 64px;
|
|
3070
|
-
border-top: 1px solid #f5f5f9;
|
|
3071
|
-
background: #fafafa;
|
|
3072
|
-
display: flex;
|
|
3073
|
-
align-items: flex-start;
|
|
3074
|
-
justify-content: space-between;
|
|
3075
|
-
gap: 16px;
|
|
3076
|
-
&:hover { background: #f5f5ff; }
|
|
3077
|
-
`;
|
|
3078
|
-
const SessionMeta = styled.div`
|
|
3079
|
-
display: flex;
|
|
3080
|
-
flex-direction: column;
|
|
3081
|
-
gap: 3px;
|
|
3082
|
-
flex: 1;
|
|
3083
|
-
min-width: 0;
|
|
3084
|
-
`;
|
|
3085
|
-
const IpChip = styled.span`
|
|
3086
|
-
display: inline-block;
|
|
3087
|
-
background: #f0f0ff;
|
|
3088
|
-
color: #4945ff;
|
|
3089
|
-
border-radius: 5px;
|
|
3090
|
-
padding: 2px 6px;
|
|
3091
|
-
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
3092
|
-
font-size: 11px;
|
|
3093
|
-
font-weight: 600;
|
|
3094
|
-
`;
|
|
3095
|
-
const TimestampText = styled.span`
|
|
3096
|
-
font-size: 11px;
|
|
3097
|
-
color: #8e8ea9;
|
|
3098
|
-
font-variant-numeric: tabular-nums;
|
|
3099
|
-
`;
|
|
3100
|
-
const AgentText = styled.span`
|
|
3101
|
-
font-size: 11px;
|
|
3102
|
-
color: #b8b8c7;
|
|
3103
|
-
overflow: hidden;
|
|
3104
|
-
text-overflow: ellipsis;
|
|
3105
|
-
white-space: nowrap;
|
|
3106
|
-
display: block;
|
|
3107
|
-
max-width: 400px;
|
|
3108
|
-
`;
|
|
3109
|
-
const EmptyState = styled.div`
|
|
3110
|
-
display: flex;
|
|
3111
|
-
justify-content: center;
|
|
3112
|
-
align-items: center;
|
|
3113
|
-
padding: 48px;
|
|
3114
|
-
font-size: 12px;
|
|
3115
|
-
color: #8e8ea9;
|
|
3116
|
-
`;
|
|
3117
|
-
function SessionsPage() {
|
|
3118
|
-
const qc = useQueryClient();
|
|
3119
|
-
const [page, setPage] = useState(1);
|
|
3120
|
-
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
3121
|
-
const [confirmRevokeSessionId, setConfirmRevokeSessionId] = useState(null);
|
|
3122
|
-
const [confirmRevokeMany, setConfirmRevokeMany] = useState(false);
|
|
3123
|
-
const sessionsQuery = useQuery({
|
|
3124
|
-
queryKey: ["dash-all-sessions", page],
|
|
3125
|
-
queryFn: async () => {
|
|
3126
|
-
const result = await client.dash.listAllSessions({});
|
|
3127
|
-
if (result.error)
|
|
3128
|
-
throw new Error(result.error.message ?? "Failed to load sessions");
|
|
3129
|
-
return result.data ?? [];
|
|
3130
|
-
},
|
|
3131
|
-
keepPreviousData: true
|
|
3132
|
-
});
|
|
3133
|
-
const revokeSessionMutation = useMutation({
|
|
3134
|
-
mutationFn: async (sessionId) => {
|
|
3135
|
-
const result = await client.dash.sessions.revoke(
|
|
3136
|
-
{},
|
|
3137
|
-
withContext({ sessionId })
|
|
3138
|
-
);
|
|
3139
|
-
if (result.error)
|
|
3140
|
-
throw new Error(result.error.message ?? "Revoke failed");
|
|
3141
|
-
},
|
|
3142
|
-
onSuccess: () => {
|
|
3143
|
-
setConfirmRevokeSessionId(null);
|
|
3144
|
-
qc.invalidateQueries({ queryKey: ["dash-all-sessions"] });
|
|
3145
|
-
}
|
|
3146
|
-
});
|
|
3147
|
-
const revokeManyMutation = useMutation({
|
|
3148
|
-
mutationFn: async (userIds) => {
|
|
3149
|
-
const result = await client.dash.sessions.revokeMany(
|
|
3150
|
-
{},
|
|
3151
|
-
withContext({ userIds })
|
|
3152
|
-
);
|
|
3153
|
-
if (result.error)
|
|
3154
|
-
throw new Error(result.error.message ?? "Revoke failed");
|
|
3155
|
-
return result.data;
|
|
3156
|
-
},
|
|
3157
|
-
onSuccess: () => {
|
|
3158
|
-
setConfirmRevokeMany(false);
|
|
3159
|
-
setSelected(/* @__PURE__ */ new Set());
|
|
3160
|
-
qc.invalidateQueries({ queryKey: ["dash-all-sessions"] });
|
|
3161
|
-
}
|
|
3162
|
-
});
|
|
3163
|
-
const usersWithSessions = sessionsQuery.data ?? [];
|
|
3164
|
-
const allUserIds = usersWithSessions.map((u) => u.id);
|
|
3165
|
-
const allSelected = allUserIds.length > 0 && allUserIds.every((id) => selected.has(id));
|
|
3166
|
-
const someSelected = selected.size > 0;
|
|
3167
|
-
const toggleSelect = (id) => {
|
|
3168
|
-
setSelected((prev) => {
|
|
3169
|
-
const next = new Set(prev);
|
|
3170
|
-
if (next.has(id)) next.delete(id);
|
|
3171
|
-
else next.add(id);
|
|
3172
|
-
return next;
|
|
3173
|
-
});
|
|
3174
|
-
};
|
|
3175
|
-
const handleSelectAll = () => {
|
|
3176
|
-
if (allSelected) setSelected(/* @__PURE__ */ new Set());
|
|
3177
|
-
else setSelected(new Set(allUserIds));
|
|
3178
|
-
};
|
|
3179
|
-
return /* @__PURE__ */ jsxs(Wrap$1, { "data-testid": "sessions-page", children: [
|
|
3180
|
-
/* @__PURE__ */ jsxs(PageHeader$1, { children: [
|
|
3181
|
-
/* @__PURE__ */ jsxs(TitleBlock$1, { children: [
|
|
3182
|
-
/* @__PURE__ */ jsx(PageTitle$1, { children: "Sessions" }),
|
|
3183
|
-
/* @__PURE__ */ jsx(PageSubtitle$1, { children: usersWithSessions.length > 0 ? `${usersWithSessions.length} user${usersWithSessions.length !== 1 ? "s" : ""} with active sessions` : "Active sessions across all users" })
|
|
3598
|
+
) : rtnData.length === 0 ? /* @__PURE__ */ jsxs(Empty, { children: [
|
|
3599
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 22 }, children: "📉" }),
|
|
3600
|
+
/* @__PURE__ */ jsx("div", { children: "No retention data for this period" })
|
|
3601
|
+
] }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 5 }, children: rtnData.map((row, i) => {
|
|
3602
|
+
const hue = rateHue(row.retentionRate);
|
|
3603
|
+
return /* @__PURE__ */ jsxs(
|
|
3604
|
+
RtnRow,
|
|
3605
|
+
{
|
|
3606
|
+
$hov: rtnHov === i,
|
|
3607
|
+
onMouseEnter: () => setRtnHov(i),
|
|
3608
|
+
onMouseLeave: () => setRtnHov(null),
|
|
3609
|
+
children: [
|
|
3610
|
+
/* @__PURE__ */ jsx(RtnLabel, { children: row.label }),
|
|
3611
|
+
/* @__PURE__ */ jsx(RtnSize, { children: row.cohortSize.toLocaleString() }),
|
|
3612
|
+
/* @__PURE__ */ jsx(RtnTrack, { children: /* @__PURE__ */ jsx(RtnBar, { $w: row.retentionRate, $hue: hue }) }),
|
|
3613
|
+
/* @__PURE__ */ jsxs(RtnPct, { $hue: hue, children: [
|
|
3614
|
+
row.retentionRate.toFixed(1),
|
|
3615
|
+
"%"
|
|
3616
|
+
] })
|
|
3617
|
+
]
|
|
3618
|
+
},
|
|
3619
|
+
row.n
|
|
3620
|
+
);
|
|
3621
|
+
}) })
|
|
3184
3622
|
] }),
|
|
3185
|
-
|
|
3186
|
-
Button,
|
|
3623
|
+
/* @__PURE__ */ jsx(RingGrid, { children: [
|
|
3187
3624
|
{
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3625
|
+
label: "Daily Active",
|
|
3626
|
+
value: stats.activeUsers.daily.active,
|
|
3627
|
+
pct: stats.activeUsers.daily.percentage,
|
|
3628
|
+
color: T.amber,
|
|
3629
|
+
sparkline: activeSpark,
|
|
3630
|
+
delay: 8
|
|
3631
|
+
},
|
|
3632
|
+
{
|
|
3633
|
+
label: "Weekly Active",
|
|
3634
|
+
value: stats.activeUsers.weekly.active,
|
|
3635
|
+
pct: stats.activeUsers.weekly.percentage,
|
|
3636
|
+
color: T.green,
|
|
3637
|
+
sparkline: activeSpark,
|
|
3638
|
+
delay: 9
|
|
3639
|
+
},
|
|
3640
|
+
{
|
|
3641
|
+
label: "Monthly Active",
|
|
3642
|
+
value: stats.activeUsers.monthly.active,
|
|
3643
|
+
pct: stats.activeUsers.monthly.percentage,
|
|
3644
|
+
color: T.accent,
|
|
3645
|
+
sparkline: activeSpark,
|
|
3646
|
+
delay: 10
|
|
3198
3647
|
}
|
|
3199
|
-
)
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
/* @__PURE__ */ jsx(TableCard$1, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading sessions…" }) }) : usersWithSessions.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { children: "No active sessions" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3203
|
-
/* @__PURE__ */ jsxs(ColumnHeader, { children: [
|
|
3204
|
-
/* @__PURE__ */ jsx(
|
|
3205
|
-
Checkbox,
|
|
3206
|
-
{
|
|
3207
|
-
checked: allSelected,
|
|
3208
|
-
onCheckedChange: handleSelectAll,
|
|
3209
|
-
"aria-label": "Select all users"
|
|
3210
|
-
}
|
|
3211
|
-
),
|
|
3212
|
-
/* @__PURE__ */ jsx("span", { children: "User / Sessions" }),
|
|
3213
|
-
/* @__PURE__ */ jsx(ColumnHeaderRight, { children: "Sessions" })
|
|
3214
|
-
] }),
|
|
3215
|
-
usersWithSessions.map((userRow, i) => /* @__PURE__ */ jsxs(UserGroup, { $i: i, "data-testid": "session-user-row", children: [
|
|
3216
|
-
/* @__PURE__ */ jsxs(UserRowHeader, { children: [
|
|
3648
|
+
].map(({ label, value, pct, color, delay }) => {
|
|
3649
|
+
const isPos = pct == null || pct >= 0;
|
|
3650
|
+
return /* @__PURE__ */ jsxs(RingCard, { $delay: delay, children: [
|
|
3217
3651
|
/* @__PURE__ */ jsx(
|
|
3218
|
-
|
|
3652
|
+
ProgressRing,
|
|
3219
3653
|
{
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3654
|
+
value: value ?? 0,
|
|
3655
|
+
max: activeMax,
|
|
3656
|
+
color,
|
|
3657
|
+
size: 56
|
|
3223
3658
|
}
|
|
3224
3659
|
),
|
|
3225
|
-
/* @__PURE__ */
|
|
3226
|
-
|
|
3227
|
-
/* @__PURE__ */ jsx(
|
|
3228
|
-
/* @__PURE__ */
|
|
3229
|
-
|
|
3230
|
-
/* @__PURE__ */ jsxs(SessionCountChip, { children: [
|
|
3231
|
-
userRow.sessions.length,
|
|
3232
|
-
" session",
|
|
3233
|
-
userRow.sessions.length !== 1 ? "s" : ""
|
|
3234
|
-
] })
|
|
3235
|
-
] }),
|
|
3236
|
-
userRow.sessions.map((session) => /* @__PURE__ */ jsxs(SessionCard, { "data-testid": "session-row", children: [
|
|
3237
|
-
/* @__PURE__ */ jsxs(SessionMeta, { children: [
|
|
3238
|
-
/* @__PURE__ */ jsxs(
|
|
3239
|
-
Flex,
|
|
3660
|
+
/* @__PURE__ */ jsxs(RingInfo, { children: [
|
|
3661
|
+
/* @__PURE__ */ jsx(RingLabel, { children: label }),
|
|
3662
|
+
/* @__PURE__ */ jsx(RingVal, { children: (value ?? 0).toLocaleString() }),
|
|
3663
|
+
pct != null && /* @__PURE__ */ jsxs(
|
|
3664
|
+
TrendBadge,
|
|
3240
3665
|
{
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
style: { flexWrap: "wrap" },
|
|
3666
|
+
$pos: isPos,
|
|
3667
|
+
style: { marginTop: 4, marginBottom: 0 },
|
|
3244
3668
|
children: [
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
" ",
|
|
3250
|
-
"· Expires",
|
|
3251
|
-
" ",
|
|
3252
|
-
new Date(session.expiresAt).toLocaleString()
|
|
3253
|
-
] })
|
|
3669
|
+
isPos ? "↑" : "↓",
|
|
3670
|
+
" ",
|
|
3671
|
+
Math.abs(pct).toFixed(1),
|
|
3672
|
+
"%"
|
|
3254
3673
|
]
|
|
3255
3674
|
}
|
|
3256
|
-
)
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
{
|
|
3262
|
-
label: "Revoke session",
|
|
3263
|
-
onClick: () => setConfirmRevokeSessionId(session.id),
|
|
3264
|
-
style: { flexShrink: 0 },
|
|
3265
|
-
"data-testid": "revoke-session-btn",
|
|
3266
|
-
children: /* @__PURE__ */ jsx(Trash, {})
|
|
3267
|
-
}
|
|
3268
|
-
)
|
|
3269
|
-
] }, session.id))
|
|
3270
|
-
] }, userRow.id))
|
|
3271
|
-
] }) }),
|
|
3272
|
-
usersWithSessions.length === PAGE_SIZE$1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
3273
|
-
/* @__PURE__ */ jsx(
|
|
3274
|
-
Button,
|
|
3275
|
-
{
|
|
3276
|
-
variant: "tertiary",
|
|
3277
|
-
size: "S",
|
|
3278
|
-
disabled: page === 1,
|
|
3279
|
-
onClick: () => setPage((p) => p - 1),
|
|
3280
|
-
children: "Previous"
|
|
3281
|
-
}
|
|
3282
|
-
),
|
|
3283
|
-
/* @__PURE__ */ jsx(
|
|
3284
|
-
Button,
|
|
3285
|
-
{
|
|
3286
|
-
variant: "tertiary",
|
|
3287
|
-
size: "S",
|
|
3288
|
-
onClick: () => setPage((p) => p + 1),
|
|
3289
|
-
children: "Next"
|
|
3290
|
-
}
|
|
3291
|
-
)
|
|
3292
|
-
] }) }),
|
|
3293
|
-
confirmRevokeSessionId && /* @__PURE__ */ jsx(
|
|
3294
|
-
ConfirmDialog,
|
|
3295
|
-
{
|
|
3296
|
-
title: "Revoke session",
|
|
3297
|
-
message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
|
|
3298
|
-
confirmLabel: "Revoke",
|
|
3299
|
-
loading: revokeSessionMutation.isLoading,
|
|
3300
|
-
onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
|
|
3301
|
-
onCancel: () => setConfirmRevokeSessionId(null)
|
|
3302
|
-
}
|
|
3303
|
-
),
|
|
3304
|
-
confirmRevokeMany && /* @__PURE__ */ jsx(
|
|
3305
|
-
ConfirmDialog,
|
|
3306
|
-
{
|
|
3307
|
-
title: `Revoke sessions for ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
|
|
3308
|
-
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.`,
|
|
3309
|
-
confirmLabel: "Revoke all",
|
|
3310
|
-
loading: revokeManyMutation.isLoading,
|
|
3311
|
-
onConfirm: () => revokeManyMutation.mutate([...selected]),
|
|
3312
|
-
onCancel: () => setConfirmRevokeMany(false)
|
|
3313
|
-
}
|
|
3314
|
-
)
|
|
3675
|
+
)
|
|
3676
|
+
] })
|
|
3677
|
+
] }, label);
|
|
3678
|
+
}) })
|
|
3679
|
+
] })
|
|
3315
3680
|
] });
|
|
3316
3681
|
}
|
|
3317
3682
|
function useUsers(options = {}) {
|
|
@@ -3552,6 +3917,45 @@ const ReadOnlyCodeInput = styled.div`
|
|
|
3552
3917
|
word-break: break-all;
|
|
3553
3918
|
overflow-wrap: anywhere;
|
|
3554
3919
|
`;
|
|
3920
|
+
const SessionCard = styled.div`
|
|
3921
|
+
display: flex;
|
|
3922
|
+
align-items: flex-start;
|
|
3923
|
+
justify-content: space-between;
|
|
3924
|
+
gap: 12px;
|
|
3925
|
+
padding: 10px 14px;
|
|
3926
|
+
background: white;
|
|
3927
|
+
border: 1px solid #eaeaef;
|
|
3928
|
+
border-radius: 8px;
|
|
3929
|
+
`;
|
|
3930
|
+
const SessionMeta = styled.div`
|
|
3931
|
+
display: flex;
|
|
3932
|
+
flex-direction: column;
|
|
3933
|
+
gap: 3px;
|
|
3934
|
+
flex: 1;
|
|
3935
|
+
min-width: 0;
|
|
3936
|
+
`;
|
|
3937
|
+
const IpChip = styled.span`
|
|
3938
|
+
display: inline-block;
|
|
3939
|
+
background: #f0f0ff;
|
|
3940
|
+
color: #4945ff;
|
|
3941
|
+
border-radius: 5px;
|
|
3942
|
+
padding: 2px 6px;
|
|
3943
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
3944
|
+
font-size: 11px;
|
|
3945
|
+
font-weight: 600;
|
|
3946
|
+
`;
|
|
3947
|
+
const TimestampText = styled.span`
|
|
3948
|
+
font-size: 11px;
|
|
3949
|
+
color: #8e8ea9;
|
|
3950
|
+
`;
|
|
3951
|
+
const AgentText = styled.span`
|
|
3952
|
+
font-size: 11px;
|
|
3953
|
+
color: #b8b8c7;
|
|
3954
|
+
overflow: hidden;
|
|
3955
|
+
text-overflow: ellipsis;
|
|
3956
|
+
white-space: nowrap;
|
|
3957
|
+
display: block;
|
|
3958
|
+
`;
|
|
3555
3959
|
const STANDARD_FIELDS = /* @__PURE__ */ new Set([
|
|
3556
3960
|
"id",
|
|
3557
3961
|
"name",
|
|
@@ -3573,6 +3977,7 @@ function UserDetailDrawer({
|
|
|
3573
3977
|
}) {
|
|
3574
3978
|
const qc = useQueryClient();
|
|
3575
3979
|
const { toggleNotification } = useNotification();
|
|
3980
|
+
const { get, put } = useFetchClient();
|
|
3576
3981
|
const schemaQuery = useModelSchema("user");
|
|
3577
3982
|
const userQuery = useQuery({
|
|
3578
3983
|
queryKey: ["dash-user", userId],
|
|
@@ -3594,6 +3999,27 @@ function UserDetailDrawer({
|
|
|
3594
3999
|
return result.data?.organizations ?? [];
|
|
3595
4000
|
}
|
|
3596
4001
|
});
|
|
4002
|
+
const sessionsQuery = useQuery({
|
|
4003
|
+
queryKey: ["dash-user-sessions", userId],
|
|
4004
|
+
queryFn: async () => {
|
|
4005
|
+
const { data } = await get(
|
|
4006
|
+
`/better-auth-dashboard/db?uid=plugin::better-auth.session&filters[userId][$eq]=${userId}&sort[0]=createdAt:desc&pagination[pageSize]=50`
|
|
4007
|
+
);
|
|
4008
|
+
return data.results ?? [];
|
|
4009
|
+
}
|
|
4010
|
+
});
|
|
4011
|
+
const strapiUserQuery = useQuery({
|
|
4012
|
+
queryKey: ["dash-strapi-user", userId],
|
|
4013
|
+
enabled: !!schemaQuery.data,
|
|
4014
|
+
queryFn: async () => {
|
|
4015
|
+
const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
|
|
4016
|
+
const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
|
|
4017
|
+
const { data } = await get(
|
|
4018
|
+
`/better-auth-dashboard/db?uid=plugin::better-auth.user&filters[id][$eq]=${userId}&pagination[pageSize]=1${populateParam}`
|
|
4019
|
+
);
|
|
4020
|
+
return data.results?.[0] ?? null;
|
|
4021
|
+
}
|
|
4022
|
+
});
|
|
3597
4023
|
const [activeTab, setActiveTab] = useState("profile");
|
|
3598
4024
|
const [editName, setEditName] = useState(void 0);
|
|
3599
4025
|
const [editEmail, setEditEmail] = useState(void 0);
|
|
@@ -3604,6 +4030,8 @@ function UserDetailDrawer({
|
|
|
3604
4030
|
const [banReason, setBanReason] = useState("");
|
|
3605
4031
|
const [banExpiresDays, setBanExpiresDays] = useState("");
|
|
3606
4032
|
const [confirmRevokeAll, setConfirmRevokeAll] = useState(false);
|
|
4033
|
+
const [confirmRevokeSessionId, setConfirmRevokeSessionId] = useState(null);
|
|
4034
|
+
const [confirmBan, setConfirmBan] = useState(false);
|
|
3607
4035
|
const [confirmUnban, setConfirmUnban] = useState(false);
|
|
3608
4036
|
const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = useState(null);
|
|
3609
4037
|
const [confirmDisable2FA, setConfirmDisable2FA] = useState(false);
|
|
@@ -3622,7 +4050,7 @@ function UserDetailDrawer({
|
|
|
3622
4050
|
setEditExtra((prev) => ({ ...prev, [name]: value }));
|
|
3623
4051
|
};
|
|
3624
4052
|
const extraData = {
|
|
3625
|
-
...
|
|
4053
|
+
...strapiUserQuery.data ?? {},
|
|
3626
4054
|
...editExtra
|
|
3627
4055
|
};
|
|
3628
4056
|
const updateMutation = useMutation({
|
|
@@ -3633,17 +4061,17 @@ function UserDetailDrawer({
|
|
|
3633
4061
|
if (editEmailVerified !== void 0)
|
|
3634
4062
|
body.emailVerified = editEmailVerified;
|
|
3635
4063
|
if (editImage !== void 0) body.image = editImage;
|
|
3636
|
-
const
|
|
3637
|
-
|
|
3638
|
-
|
|
4064
|
+
const documentId = strapiUserQuery.data?.documentId;
|
|
4065
|
+
if (!documentId) throw new Error("Could not resolve documentId for user");
|
|
4066
|
+
await put(
|
|
4067
|
+
`/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
|
|
4068
|
+
body
|
|
3639
4069
|
);
|
|
3640
|
-
if (result.error)
|
|
3641
|
-
throw new Error(result.error.message ?? "Update failed");
|
|
3642
|
-
return result.data;
|
|
3643
4070
|
},
|
|
3644
4071
|
onSuccess: () => {
|
|
3645
4072
|
qc.invalidateQueries({ queryKey: ["dash-user", userId] });
|
|
3646
4073
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
4074
|
+
qc.invalidateQueries({ queryKey: ["dash-strapi-user", userId] });
|
|
3647
4075
|
setEditName(void 0);
|
|
3648
4076
|
setEditEmail(void 0);
|
|
3649
4077
|
setEditEmailVerified(void 0);
|
|
@@ -3662,6 +4090,27 @@ function UserDetailDrawer({
|
|
|
3662
4090
|
});
|
|
3663
4091
|
}
|
|
3664
4092
|
});
|
|
4093
|
+
const revokeSessionMutation = useMutation({
|
|
4094
|
+
mutationFn: async (sessionId) => {
|
|
4095
|
+
const result = await client.dash.sessions.revoke(
|
|
4096
|
+
{},
|
|
4097
|
+
withContext({ sessionId })
|
|
4098
|
+
);
|
|
4099
|
+
if (result.error)
|
|
4100
|
+
throw new Error(result.error.message ?? "Revoke failed");
|
|
4101
|
+
},
|
|
4102
|
+
onSuccess: () => {
|
|
4103
|
+
setConfirmRevokeSessionId(null);
|
|
4104
|
+
qc.invalidateQueries({ queryKey: ["dash-user-sessions", userId] });
|
|
4105
|
+
toggleNotification({ type: "success", message: "Session revoked" });
|
|
4106
|
+
},
|
|
4107
|
+
onError: (err) => {
|
|
4108
|
+
toggleNotification({
|
|
4109
|
+
type: "danger",
|
|
4110
|
+
message: err.message ?? "Failed to revoke session"
|
|
4111
|
+
});
|
|
4112
|
+
}
|
|
4113
|
+
});
|
|
3665
4114
|
const passwordMutation = useMutation({
|
|
3666
4115
|
mutationFn: async () => {
|
|
3667
4116
|
const result = await client.dash.setPassword(
|
|
@@ -3699,6 +4148,7 @@ function UserDetailDrawer({
|
|
|
3699
4148
|
return result.data;
|
|
3700
4149
|
},
|
|
3701
4150
|
onSuccess: () => {
|
|
4151
|
+
setConfirmBan(false);
|
|
3702
4152
|
qc.invalidateQueries({ queryKey: ["dash-user", userId] });
|
|
3703
4153
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
3704
4154
|
setBanReason("");
|
|
@@ -4314,7 +4764,8 @@ function UserDetailDrawer({
|
|
|
4314
4764
|
] })
|
|
4315
4765
|
] })
|
|
4316
4766
|
] }) }),
|
|
4317
|
-
banEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap:
|
|
4767
|
+
banEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxs(FormSection, { children: [
|
|
4768
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Ban status" }),
|
|
4318
4769
|
/* @__PURE__ */ jsxs(WarnCard, { children: [
|
|
4319
4770
|
/* @__PURE__ */ jsx(
|
|
4320
4771
|
Typography,
|
|
@@ -4329,7 +4780,7 @@ function UserDetailDrawer({
|
|
|
4329
4780
|
"Reason: ",
|
|
4330
4781
|
user.banReason
|
|
4331
4782
|
] }),
|
|
4332
|
-
user.banExpires
|
|
4783
|
+
user.banExpires ? /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
|
|
4333
4784
|
"Expires:",
|
|
4334
4785
|
" ",
|
|
4335
4786
|
new Date(user.banExpires).toLocaleDateString(
|
|
@@ -4341,8 +4792,7 @@ function UserDetailDrawer({
|
|
|
4341
4792
|
day: "numeric"
|
|
4342
4793
|
}
|
|
4343
4794
|
)
|
|
4344
|
-
] }),
|
|
4345
|
-
!user.banExpires && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
|
|
4795
|
+
] }) : /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
|
|
4346
4796
|
] }),
|
|
4347
4797
|
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
|
|
4348
4798
|
Button,
|
|
@@ -4352,27 +4802,28 @@ function UserDetailDrawer({
|
|
|
4352
4802
|
children: "Lift ban"
|
|
4353
4803
|
}
|
|
4354
4804
|
) })
|
|
4355
|
-
] }) : /* @__PURE__ */ jsxs(
|
|
4805
|
+
] }) : /* @__PURE__ */ jsxs(FormSection, { children: [
|
|
4806
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Apply ban" }),
|
|
4807
|
+
/* @__PURE__ */ jsxs(
|
|
4808
|
+
Field.Root,
|
|
4809
|
+
{
|
|
4810
|
+
hint: "Optional — will be shown to the user",
|
|
4811
|
+
style: { width: "100%" },
|
|
4812
|
+
children: [
|
|
4813
|
+
/* @__PURE__ */ jsx(Field.Label, { children: "Reason" }),
|
|
4814
|
+
/* @__PURE__ */ jsx(
|
|
4815
|
+
TextInput,
|
|
4816
|
+
{
|
|
4817
|
+
value: banReason,
|
|
4818
|
+
onChange: (e) => setBanReason(e.target.value),
|
|
4819
|
+
placeholder: "e.g. Violated terms of service"
|
|
4820
|
+
}
|
|
4821
|
+
),
|
|
4822
|
+
/* @__PURE__ */ jsx(Field.Hint, {})
|
|
4823
|
+
]
|
|
4824
|
+
}
|
|
4825
|
+
),
|
|
4356
4826
|
/* @__PURE__ */ jsxs(Grid.Root, { gap: 4, children: [
|
|
4357
|
-
/* @__PURE__ */ jsx(Grid.Item, { col: 12, children: /* @__PURE__ */ jsxs(
|
|
4358
|
-
Field.Root,
|
|
4359
|
-
{
|
|
4360
|
-
hint: "Optional — will be shown to the user",
|
|
4361
|
-
style: { width: "100%" },
|
|
4362
|
-
children: [
|
|
4363
|
-
/* @__PURE__ */ jsx(Field.Label, { children: "Reason" }),
|
|
4364
|
-
/* @__PURE__ */ jsx(
|
|
4365
|
-
TextInput,
|
|
4366
|
-
{
|
|
4367
|
-
value: banReason,
|
|
4368
|
-
onChange: (e) => setBanReason(e.target.value),
|
|
4369
|
-
placeholder: "e.g. Violated terms of service"
|
|
4370
|
-
}
|
|
4371
|
-
),
|
|
4372
|
-
/* @__PURE__ */ jsx(Field.Hint, {})
|
|
4373
|
-
]
|
|
4374
|
-
}
|
|
4375
|
-
) }),
|
|
4376
4827
|
/* @__PURE__ */ jsx(Grid.Item, { col: 6, children: /* @__PURE__ */ jsxs(
|
|
4377
4828
|
Field.Root,
|
|
4378
4829
|
{
|
|
@@ -4393,24 +4844,13 @@ function UserDetailDrawer({
|
|
|
4393
4844
|
]
|
|
4394
4845
|
}
|
|
4395
4846
|
) }),
|
|
4396
|
-
/* @__PURE__ */ jsx(Grid.Item, { col: 6, children:
|
|
4397
|
-
Flex,
|
|
4398
|
-
{
|
|
4399
|
-
direction: "column",
|
|
4400
|
-
justifyContent: "flex-end",
|
|
4401
|
-
style: { height: "100%", paddingBottom: 4 },
|
|
4402
|
-
children: /* @__PURE__ */ jsxs(PreviewPill, { children: [
|
|
4403
|
-
"⏱ Expires ",
|
|
4404
|
-
banExpiryPreview
|
|
4405
|
-
] })
|
|
4406
|
-
}
|
|
4407
|
-
) : /* @__PURE__ */ jsx(
|
|
4847
|
+
/* @__PURE__ */ jsx(Grid.Item, { col: 6, children: /* @__PURE__ */ jsx(
|
|
4408
4848
|
Flex,
|
|
4409
4849
|
{
|
|
4410
4850
|
direction: "column",
|
|
4411
4851
|
justifyContent: "flex-end",
|
|
4412
4852
|
style: { height: "100%", paddingBottom: 4 },
|
|
4413
|
-
children: /* @__PURE__ */ jsx(PreviewPill, { children: "♾ Permanent ban" })
|
|
4853
|
+
children: /* @__PURE__ */ jsx(PreviewPill, { children: banExpiryPreview ? `⏱ Expires ${banExpiryPreview}` : "♾ Permanent ban" })
|
|
4414
4854
|
}
|
|
4415
4855
|
) })
|
|
4416
4856
|
] }),
|
|
@@ -4418,31 +4858,68 @@ function UserDetailDrawer({
|
|
|
4418
4858
|
Button,
|
|
4419
4859
|
{
|
|
4420
4860
|
variant: "danger",
|
|
4421
|
-
|
|
4422
|
-
onClick: () => banMutation.mutate(),
|
|
4861
|
+
onClick: () => setConfirmBan(true),
|
|
4423
4862
|
children: "Ban user"
|
|
4424
4863
|
}
|
|
4425
4864
|
) })
|
|
4426
4865
|
] }) }) }),
|
|
4427
|
-
/* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", children: /* @__PURE__ */
|
|
4428
|
-
/* @__PURE__ */
|
|
4429
|
-
|
|
4430
|
-
/* @__PURE__ */ jsxs(
|
|
4431
|
-
/* @__PURE__ */
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4866
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
|
|
4867
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
4868
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Session management" }),
|
|
4869
|
+
/* @__PURE__ */ jsxs(AccountRow, { children: [
|
|
4870
|
+
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
|
|
4871
|
+
/* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
|
|
4872
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
|
|
4873
|
+
] }),
|
|
4874
|
+
/* @__PURE__ */ jsx(
|
|
4875
|
+
Button,
|
|
4876
|
+
{
|
|
4877
|
+
variant: "danger-light",
|
|
4878
|
+
size: "S",
|
|
4879
|
+
onClick: () => setConfirmRevokeAll(true),
|
|
4880
|
+
style: { flexShrink: 0 },
|
|
4881
|
+
children: "Revoke all"
|
|
4882
|
+
}
|
|
4883
|
+
)
|
|
4884
|
+
] })
|
|
4885
|
+
] }),
|
|
4886
|
+
/* @__PURE__ */ jsxs(FormSection, { children: [
|
|
4887
|
+
/* @__PURE__ */ jsx(SectionLabel, { children: "Active sessions" }),
|
|
4888
|
+
sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading sessions…" }) }) : (sessionsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "No active sessions." }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxs(SessionCard, { children: [
|
|
4889
|
+
/* @__PURE__ */ jsxs(SessionMeta, { children: [
|
|
4890
|
+
/* @__PURE__ */ jsxs(
|
|
4891
|
+
Flex,
|
|
4892
|
+
{
|
|
4893
|
+
gap: 2,
|
|
4894
|
+
alignItems: "center",
|
|
4895
|
+
style: { flexWrap: "wrap" },
|
|
4896
|
+
children: [
|
|
4897
|
+
session.ipAddress && /* @__PURE__ */ jsx(IpChip, { children: session.ipAddress }),
|
|
4898
|
+
/* @__PURE__ */ jsxs(TimestampText, { children: [
|
|
4899
|
+
"Created",
|
|
4900
|
+
" ",
|
|
4901
|
+
new Date(session.createdAt).toLocaleString(),
|
|
4902
|
+
" · Expires",
|
|
4903
|
+
" ",
|
|
4904
|
+
new Date(session.expiresAt).toLocaleString()
|
|
4905
|
+
] })
|
|
4906
|
+
]
|
|
4907
|
+
}
|
|
4908
|
+
),
|
|
4909
|
+
session.userAgent && /* @__PURE__ */ jsx(AgentText, { children: session.userAgent })
|
|
4910
|
+
] }),
|
|
4911
|
+
/* @__PURE__ */ jsx(
|
|
4912
|
+
IconButton,
|
|
4913
|
+
{
|
|
4914
|
+
label: "Revoke session",
|
|
4915
|
+
onClick: () => setConfirmRevokeSessionId(String(session.id)),
|
|
4916
|
+
style: { flexShrink: 0 },
|
|
4917
|
+
children: /* @__PURE__ */ jsx(Trash, {})
|
|
4918
|
+
}
|
|
4919
|
+
)
|
|
4920
|
+
] }, session.documentId)) })
|
|
4444
4921
|
] })
|
|
4445
|
-
] }) })
|
|
4922
|
+
] }) }),
|
|
4446
4923
|
/* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxs(FormSection, { children: [
|
|
4447
4924
|
/* @__PURE__ */ jsx(SectionLabel, { children: "Memberships" }),
|
|
4448
4925
|
orgsQuery.isLoading ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral500", children: "Loading…" }) : (orgsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Not a member of any organizations." }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: (orgsQuery.data ?? []).map(
|
|
@@ -4479,6 +4956,29 @@ function UserDetailDrawer({
|
|
|
4479
4956
|
onCancel: () => setConfirmRevokeAll(false)
|
|
4480
4957
|
}
|
|
4481
4958
|
),
|
|
4959
|
+
confirmRevokeSessionId && /* @__PURE__ */ jsx(
|
|
4960
|
+
ConfirmDialog,
|
|
4961
|
+
{
|
|
4962
|
+
title: "Revoke session",
|
|
4963
|
+
message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
|
|
4964
|
+
confirmLabel: "Revoke",
|
|
4965
|
+
loading: revokeSessionMutation.isLoading,
|
|
4966
|
+
onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
|
|
4967
|
+
onCancel: () => setConfirmRevokeSessionId(null)
|
|
4968
|
+
}
|
|
4969
|
+
),
|
|
4970
|
+
confirmBan && /* @__PURE__ */ jsx(
|
|
4971
|
+
ConfirmDialog,
|
|
4972
|
+
{
|
|
4973
|
+
title: "Ban user",
|
|
4974
|
+
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.",
|
|
4975
|
+
confirmLabel: "Ban user",
|
|
4976
|
+
variant: "danger",
|
|
4977
|
+
loading: banMutation.isLoading,
|
|
4978
|
+
onConfirm: () => banMutation.mutate(),
|
|
4979
|
+
onCancel: () => setConfirmBan(false)
|
|
4980
|
+
}
|
|
4981
|
+
),
|
|
4482
4982
|
confirmUnban && /* @__PURE__ */ jsx(
|
|
4483
4983
|
ConfirmDialog,
|
|
4484
4984
|
{
|
|
@@ -4709,6 +5209,7 @@ const Toolbar = styled.div`
|
|
|
4709
5209
|
`;
|
|
4710
5210
|
function UsersPage({ config }) {
|
|
4711
5211
|
const qc = useQueryClient();
|
|
5212
|
+
const { toggleNotification } = useNotification();
|
|
4712
5213
|
const banEnabled = hasPlugin(config, "admin");
|
|
4713
5214
|
const emailVerificationEnabled = config.emailVerification.sendVerificationEmailEnabled;
|
|
4714
5215
|
const twoFactorEnabled = hasPlugin(config, "two-factor");
|
|
@@ -4740,6 +5241,13 @@ function UsersPage({ config }) {
|
|
|
4740
5241
|
setConfirmDelete(null);
|
|
4741
5242
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
4742
5243
|
qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
|
|
5244
|
+
toggleNotification({ type: "success", message: "User deleted" });
|
|
5245
|
+
},
|
|
5246
|
+
onError: (err) => {
|
|
5247
|
+
toggleNotification({
|
|
5248
|
+
type: "danger",
|
|
5249
|
+
message: err.message ?? "Failed to delete user"
|
|
5250
|
+
});
|
|
4743
5251
|
}
|
|
4744
5252
|
});
|
|
4745
5253
|
const deleteManyMutation = useMutation({
|
|
@@ -4752,11 +5260,21 @@ function UsersPage({ config }) {
|
|
|
4752
5260
|
throw new Error(result.error.message ?? "Delete failed");
|
|
4753
5261
|
return result.data;
|
|
4754
5262
|
},
|
|
4755
|
-
onSuccess: () => {
|
|
5263
|
+
onSuccess: (_data, userIds) => {
|
|
4756
5264
|
setConfirmDeleteMany(false);
|
|
4757
5265
|
setSelected(/* @__PURE__ */ new Set());
|
|
4758
5266
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
4759
5267
|
qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
|
|
5268
|
+
toggleNotification({
|
|
5269
|
+
type: "success",
|
|
5270
|
+
message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} deleted`
|
|
5271
|
+
});
|
|
5272
|
+
},
|
|
5273
|
+
onError: (err) => {
|
|
5274
|
+
toggleNotification({
|
|
5275
|
+
type: "danger",
|
|
5276
|
+
message: err.message ?? "Failed to delete users"
|
|
5277
|
+
});
|
|
4760
5278
|
}
|
|
4761
5279
|
});
|
|
4762
5280
|
const banManyMutation = useMutation({
|
|
@@ -4768,10 +5286,20 @@ function UsersPage({ config }) {
|
|
|
4768
5286
|
if (result.error) throw new Error(result.error.message ?? "Ban failed");
|
|
4769
5287
|
return result.data;
|
|
4770
5288
|
},
|
|
4771
|
-
onSuccess: () => {
|
|
5289
|
+
onSuccess: (_data, userIds) => {
|
|
4772
5290
|
setConfirmBanMany(false);
|
|
4773
5291
|
setSelected(/* @__PURE__ */ new Set());
|
|
4774
5292
|
qc.invalidateQueries({ queryKey: ["dash-users"] });
|
|
5293
|
+
toggleNotification({
|
|
5294
|
+
type: "success",
|
|
5295
|
+
message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} banned`
|
|
5296
|
+
});
|
|
5297
|
+
},
|
|
5298
|
+
onError: (err) => {
|
|
5299
|
+
toggleNotification({
|
|
5300
|
+
type: "danger",
|
|
5301
|
+
message: err.message ?? "Failed to ban users"
|
|
5302
|
+
});
|
|
4775
5303
|
}
|
|
4776
5304
|
});
|
|
4777
5305
|
const toggleSelect = (id) => {
|
|
@@ -4950,14 +5478,28 @@ function UsersPage({ config }) {
|
|
|
4950
5478
|
user.id
|
|
4951
5479
|
)) })
|
|
4952
5480
|
] }) }),
|
|
4953
|
-
pageCount > 1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
5481
|
+
pageCount > 1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
5482
|
+
/* @__PURE__ */ jsx(
|
|
5483
|
+
Button,
|
|
5484
|
+
{
|
|
5485
|
+
variant: "tertiary",
|
|
5486
|
+
size: "S",
|
|
5487
|
+
disabled: page === 1,
|
|
5488
|
+
onClick: () => setPage((p) => p - 1),
|
|
5489
|
+
children: "Previous"
|
|
5490
|
+
}
|
|
5491
|
+
),
|
|
5492
|
+
/* @__PURE__ */ jsx(
|
|
5493
|
+
Button,
|
|
5494
|
+
{
|
|
5495
|
+
variant: "tertiary",
|
|
5496
|
+
size: "S",
|
|
5497
|
+
disabled: page >= pageCount,
|
|
5498
|
+
onClick: () => setPage((p) => p + 1),
|
|
5499
|
+
children: "Next"
|
|
5500
|
+
}
|
|
5501
|
+
)
|
|
5502
|
+
] }) }),
|
|
4961
5503
|
showCreate && /* @__PURE__ */ jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
|
|
4962
5504
|
detailUserId && /* @__PURE__ */ jsx(
|
|
4963
5505
|
UserDetailDrawer,
|
|
@@ -5013,8 +5555,7 @@ const Accent = styled.div`
|
|
|
5013
5555
|
const BrandIcon = styled.div`
|
|
5014
5556
|
width: 30px;
|
|
5015
5557
|
height: 30px;
|
|
5016
|
-
|
|
5017
|
-
background: linear-gradient(135deg, #4945ff 0%, #7b79ff 100%);
|
|
5558
|
+
background: black;
|
|
5018
5559
|
display: flex;
|
|
5019
5560
|
align-items: center;
|
|
5020
5561
|
justify-content: center;
|
|
@@ -5090,7 +5631,7 @@ function App() {
|
|
|
5090
5631
|
paddingBottom: 4,
|
|
5091
5632
|
children: [
|
|
5092
5633
|
/* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
|
|
5093
|
-
/* @__PURE__ */ jsx(BrandIcon, { children:
|
|
5634
|
+
/* @__PURE__ */ jsx(BrandIcon, { children: /* @__PURE__ */ jsx(PluginIcon, {}) }),
|
|
5094
5635
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
5095
5636
|
/* @__PURE__ */ jsx(Typography, { variant: "beta", textColor: "neutral800", children: "Better Auth" }),
|
|
5096
5637
|
/* @__PURE__ */ jsx(Box, { paddingTop: "2px", children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Authentication Dashboard" }) })
|
|
@@ -5110,8 +5651,7 @@ function App() {
|
|
|
5110
5651
|
"data-testid": "nav-organizations",
|
|
5111
5652
|
children: "Organizations"
|
|
5112
5653
|
}
|
|
5113
|
-
)
|
|
5114
|
-
/* @__PURE__ */ jsx(Tabs.Trigger, { value: "sessions", "data-testid": "nav-sessions", children: "Sessions" })
|
|
5654
|
+
)
|
|
5115
5655
|
] })
|
|
5116
5656
|
]
|
|
5117
5657
|
}
|
|
@@ -5121,8 +5661,7 @@ function App() {
|
|
|
5121
5661
|
),
|
|
5122
5662
|
/* @__PURE__ */ jsx(Tabs.Content, { value: "overview", "data-testid": "tab-overview", children: /* @__PURE__ */ jsx(OverviewPage, {}) }),
|
|
5123
5663
|
/* @__PURE__ */ jsx(Tabs.Content, { value: "users", "data-testid": "tab-users", children: /* @__PURE__ */ jsx(UsersPage, { config }) }),
|
|
5124
|
-
orgEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsx(OrganizationsPage, { teamsEnabled }) })
|
|
5125
|
-
/* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", "data-testid": "tab-sessions", children: /* @__PURE__ */ jsx(SessionsPage, {}) })
|
|
5664
|
+
orgEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsx(OrganizationsPage, { teamsEnabled }) })
|
|
5126
5665
|
] }) });
|
|
5127
5666
|
}
|
|
5128
5667
|
function Root() {
|