@streamoid/settings 0.14.0 → 0.15.0

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/index.js CHANGED
@@ -34,7 +34,9 @@ __export(index_exports, {
34
34
  CreditPoolMemberModal: () => CreditPoolMemberModal,
35
35
  DEFAULT_PERMISSIONS: () => DEFAULT_PERMISSIONS,
36
36
  DEFAULT_USER_LANGUAGE: () => DEFAULT_USER_LANGUAGE,
37
+ EMPTY_USAGE_LOGS_PAGE: () => EMPTY_USAGE_LOGS_PAGE,
37
38
  ExportDateModal: () => ExportDateModal,
39
+ INITIAL_USAGE_LOGS_STATE: () => INITIAL_USAGE_LOGS_STATE,
38
40
  InviteUpdateModal: () => InviteUpdateModal,
39
41
  MemberCreditLimitModal: () => MemberCreditLimitModal,
40
42
  MemberListFrame: () => MemberListFrame,
@@ -67,7 +69,12 @@ __export(index_exports, {
67
69
  TeamsContent: () => TeamsContent,
68
70
  TokensContent: () => TokensContent,
69
71
  URL_TO_TAB: () => URL_TO_TAB,
72
+ USAGE_LOGS_EXPORT_BATCH_SIZE: () => USAGE_LOGS_EXPORT_BATCH_SIZE,
73
+ USAGE_LOGS_EXPORT_EXTRA_REQUESTS: () => USAGE_LOGS_EXPORT_EXTRA_REQUESTS,
74
+ USAGE_LOGS_EXPORT_MAX_ROWS: () => USAGE_LOGS_EXPORT_MAX_ROWS,
75
+ USAGE_LOGS_PAGE_SIZE: () => USAGE_LOGS_PAGE_SIZE,
70
76
  USER_LANGUAGES: () => USER_LANGUAGES,
77
+ UsageLogsPager: () => UsageLogsPager,
71
78
  WORKSPACE_LIST_PAGE_SIZE: () => WORKSPACE_LIST_PAGE_SIZE,
72
79
  WorkspaceGeneralTab: () => WorkspaceGeneralTab,
73
80
  WorkspaceListTab: () => WorkspaceListTab,
@@ -79,6 +86,7 @@ __export(index_exports, {
79
86
  cleanCreditLimitInput: () => cleanCreditLimitInput,
80
87
  cleanPoolValueInput: () => cleanPoolValueInput,
81
88
  countLabel: () => countLabel,
89
+ createUsedByNamesLoader: () => createUsedByNamesLoader,
82
90
  creditCycleUnit: () => creditCycleUnit,
83
91
  creditsMeter: () => creditsMeter,
84
92
  deriveWorkspaceInvitePermission: () => deriveWorkspaceInvitePermission,
@@ -88,6 +96,7 @@ __export(index_exports, {
88
96
  entitlementFromBody: () => entitlementFromBody,
89
97
  entitlementFromFailure: () => entitlementFromFailure,
90
98
  expiryChoiceFor: () => expiryChoiceFor,
99
+ fetchUsageLogsInRange: () => fetchUsageLogsInRange,
91
100
  findBillingError: () => findBillingError,
92
101
  findPoolError: () => findPoolError,
93
102
  formatCreditLimit: () => formatCreditLimit,
@@ -122,8 +131,16 @@ __export(index_exports, {
122
131
  settingsReturnTarget: () => settingsReturnTarget,
123
132
  sharedPoolBannerText: () => sharedPoolBannerText,
124
133
  sharedPoolEndsText: () => sharedPoolEndsText,
134
+ sliceUsageLogsPage: () => sliceUsageLogsPage,
125
135
  sponsoredCreditsText: () => sponsoredCreditsText,
136
+ toCsv: () => toCsv,
126
137
  toLocalDateInput: () => toLocalDateInput,
138
+ usageLogsCsvRows: () => usageLogsCsvRows,
139
+ usageLogsDateRange: () => usageLogsDateRange,
140
+ usageLogsExportFileName: () => usageLogsExportFileName,
141
+ usageLogsPage: () => usageLogsPage,
142
+ usageLogsReducer: () => usageLogsReducer,
143
+ usageLogsSpentByOthers: () => usageLogsSpentByOthers,
127
144
  useBillingEntitlement: () => useBillingEntitlement,
128
145
  useSettingsAdapters: () => useSettingsAdapters,
129
146
  useWorkspaceInvitePermission: () => useWorkspaceInvitePermission,
@@ -7897,7 +7914,10 @@ function formatDateRange(date) {
7897
7914
  function ExportDateModal({
7898
7915
  title,
7899
7916
  onClose,
7900
- onExport
7917
+ onExport,
7918
+ busy = false,
7919
+ busyText,
7920
+ notice = null
7901
7921
  }) {
7902
7922
  useOverlayOpen(true, onClose);
7903
7923
  const now = /* @__PURE__ */ new Date();
@@ -7984,6 +8004,7 @@ function ExportDateModal({
7984
8004
  [rangeStart, rangeEnd, isFuture]
7985
8005
  );
7986
8006
  const handleExport = () => {
8007
+ if (busy) return;
7987
8008
  if (rangeStart && rangeEnd) onExport(rangeStart, rangeEnd);
7988
8009
  };
7989
8010
  const getCellVisuals = (cell, idx) => {
@@ -8205,24 +8226,42 @@ function ExportDateModal({
8205
8226
  borderTop: "1px solid var(--alias-border-subtle)"
8206
8227
  },
8207
8228
  children: [
8208
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
8209
- "p",
8210
- {
8211
- className: "flex-1 min-w-0 truncate",
8212
- style: {
8213
- fontFamily: "var(--font-family-font-family-body)",
8214
- fontSize: "var(--font-size-font-size-md)",
8215
- fontWeight: 400,
8216
- lineHeight: "var(--line-height-line-height-md)",
8217
- color: "var(--alias-text---icons-primary)"
8218
- },
8219
- children: rangeStart && rangeEnd ? `${formatDateRange(rangeStart)} - ${formatDateRange(rangeEnd)}` : rangeStart ? `${formatDateRange(rangeStart)} - Select end date` : "Select date range"
8220
- }
8221
- ),
8229
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex-1 min-w-0 flex flex-col", children: [
8230
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
8231
+ "p",
8232
+ {
8233
+ className: "min-w-0 truncate",
8234
+ style: {
8235
+ fontFamily: "var(--font-family-font-family-body)",
8236
+ fontSize: "var(--font-size-font-size-md)",
8237
+ fontWeight: 400,
8238
+ lineHeight: "var(--line-height-line-height-md)",
8239
+ color: "var(--alias-text---icons-primary)"
8240
+ },
8241
+ "aria-live": "polite",
8242
+ children: busy && busyText ? busyText : rangeStart && rangeEnd ? `${formatDateRange(rangeStart)} - ${formatDateRange(rangeEnd)}` : rangeStart ? `${formatDateRange(rangeStart)} - Select end date` : "Select date range"
8243
+ }
8244
+ ),
8245
+ notice && !busy && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
8246
+ "p",
8247
+ {
8248
+ role: "alert",
8249
+ style: {
8250
+ fontFamily: "var(--font-family-font-family-body)",
8251
+ fontSize: "var(--font-size-font-size-sm)",
8252
+ fontWeight: 400,
8253
+ lineHeight: "var(--line-height-line-height-sm)",
8254
+ color: "var(--alias-text---icons-error)"
8255
+ },
8256
+ children: notice
8257
+ }
8258
+ )
8259
+ ] }),
8222
8260
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
8223
8261
  "div",
8224
8262
  {
8225
- className: `flex items-center justify-center shrink-0 ${rangeStart && rangeEnd ? "cursor-pointer" : "opacity-50 cursor-not-allowed"}`,
8263
+ className: `flex items-center justify-center shrink-0 ${rangeStart && rangeEnd && !busy ? "cursor-pointer" : "opacity-50 cursor-not-allowed"}`,
8264
+ "aria-disabled": busy || !(rangeStart && rangeEnd),
8226
8265
  style: {
8227
8266
  backgroundColor: "var(--alias-text---icons-primary)",
8228
8267
  borderRadius: "var(--radius-xl)",
@@ -8240,7 +8279,7 @@ function ExportDateModal({
8240
8279
  lineHeight: "var(--line-height-line-height-sm)",
8241
8280
  color: "var(--alias-text---icons-inverse)"
8242
8281
  },
8243
- children: "Export"
8282
+ children: busy ? "Exporting\u2026" : "Export"
8244
8283
  }
8245
8284
  )
8246
8285
  }
@@ -10659,6 +10698,196 @@ async function loadCurrentPlan(fetchCurrentPlan) {
10659
10698
  return { subscription, entitlement, paid: hasPaidFeatures({ entitlement, subscription }) };
10660
10699
  }
10661
10700
 
10701
+ // src/usage-logs.ts
10702
+ var USAGE_LOGS_PAGE_SIZE = 50;
10703
+ var USAGE_LOGS_EXPORT_BATCH_SIZE = 100;
10704
+ var USAGE_LOGS_EXPORT_MAX_ROWS = 2e4;
10705
+ var USAGE_LOGS_EXPORT_EXTRA_REQUESTS = 10;
10706
+ function rowsOf(res) {
10707
+ return Array.isArray(res?.data) ? res.data : [];
10708
+ }
10709
+ function sliceUsageLogsPage(all, page, pageSize = USAGE_LOGS_PAGE_SIZE) {
10710
+ const total = all.length;
10711
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
10712
+ const safePage = Math.min(Math.max(1, Math.floor(page) || 1), totalPages);
10713
+ return {
10714
+ rows: all.slice((safePage - 1) * pageSize, safePage * pageSize),
10715
+ page: safePage,
10716
+ totalPages,
10717
+ total,
10718
+ unpaged: all
10719
+ };
10720
+ }
10721
+ function usageLogsPage(res, page, pageSize = USAGE_LOGS_PAGE_SIZE) {
10722
+ const data = rowsOf(res);
10723
+ const pagination = res?.pagination;
10724
+ if (!pagination || !Number.isFinite(pagination.total)) {
10725
+ return sliceUsageLogsPage(data, page, pageSize);
10726
+ }
10727
+ const total = Math.max(0, pagination.total);
10728
+ const limit = pagination.limit > 0 ? pagination.limit : pageSize;
10729
+ const totalPages = Math.max(
10730
+ 1,
10731
+ pagination.totalPages > 0 ? pagination.totalPages : Math.ceil(total / limit)
10732
+ );
10733
+ return {
10734
+ rows: data.slice(0, pageSize),
10735
+ page: pagination.page > 0 ? pagination.page : page,
10736
+ totalPages,
10737
+ total,
10738
+ unpaged: null
10739
+ };
10740
+ }
10741
+ var EMPTY_USAGE_LOGS_PAGE = {
10742
+ rows: [],
10743
+ page: 1,
10744
+ totalPages: 1,
10745
+ total: 0,
10746
+ unpaged: null
10747
+ };
10748
+ var INITIAL_USAGE_LOGS_STATE = {
10749
+ logs: EMPTY_USAGE_LOGS_PAGE,
10750
+ requestedPage: 1,
10751
+ loading: false,
10752
+ error: null
10753
+ };
10754
+ function usageLogsReducer(state, action) {
10755
+ switch (action.type) {
10756
+ case "request":
10757
+ return { ...state, requestedPage: action.page, loading: true, error: null };
10758
+ case "loaded":
10759
+ return {
10760
+ logs: action.logs,
10761
+ requestedPage: action.logs.page,
10762
+ loading: false,
10763
+ error: null
10764
+ };
10765
+ case "failed":
10766
+ return { ...state, loading: false, error: action.error };
10767
+ case "turn": {
10768
+ if (!state.logs.unpaged || state.error) return state;
10769
+ const logs = sliceUsageLogsPage(state.logs.unpaged, action.page);
10770
+ return { ...state, logs, requestedPage: logs.page };
10771
+ }
10772
+ case "reset":
10773
+ return INITIAL_USAGE_LOGS_STATE;
10774
+ }
10775
+ }
10776
+ function usageLogsSpentByOthers(logs, workspaceId) {
10777
+ return (logs.unpaged ?? logs.rows).some(
10778
+ (log) => usedByWorkspaceKey(log, workspaceId) !== null
10779
+ );
10780
+ }
10781
+ function createUsedByNamesLoader(save) {
10782
+ let savedFor = null;
10783
+ let inFlight = null;
10784
+ return {
10785
+ async load(workspaceId, fetchCreditPool) {
10786
+ if (savedFor === workspaceId || inFlight?.workspaceId === workspaceId) {
10787
+ return;
10788
+ }
10789
+ const lookup = { workspaceId };
10790
+ inFlight = lookup;
10791
+ try {
10792
+ const pool = await fetchCreditPool();
10793
+ if (inFlight !== lookup) return;
10794
+ if (save(workspaceId, usedByNamesFrom(pool))) savedFor = workspaceId;
10795
+ } catch {
10796
+ } finally {
10797
+ if (inFlight === lookup) inFlight = null;
10798
+ }
10799
+ },
10800
+ reset() {
10801
+ savedFor = null;
10802
+ inFlight = null;
10803
+ }
10804
+ };
10805
+ }
10806
+ function usageLogsExportFileName(start, end) {
10807
+ return `usage_logs_${toLocalDateInput(start)}_${toLocalDateInput(end)}.csv`;
10808
+ }
10809
+ function usageLogsDateRange(start, end) {
10810
+ const from = new Date(start);
10811
+ from.setHours(0, 0, 0, 0);
10812
+ const to = new Date(end);
10813
+ to.setHours(23, 59, 59, 999);
10814
+ return { startDate: from.toISOString(), endDate: to.toISOString() };
10815
+ }
10816
+ async function fetchUsageLogsInRange(fetchUsageLogs, range, {
10817
+ batchSize = USAGE_LOGS_EXPORT_BATCH_SIZE,
10818
+ maxRows = USAGE_LOGS_EXPORT_MAX_ROWS,
10819
+ maxRequests,
10820
+ onProgress,
10821
+ signal
10822
+ } = {}) {
10823
+ const first = await fetchUsageLogs({ ...range, page: 1, limit: batchSize });
10824
+ if (signal?.aborted) return { status: "cancelled" };
10825
+ if (!first?.pagination || !Number.isFinite(first.pagination.total)) {
10826
+ const from = Date.parse(range.startDate);
10827
+ const to = Date.parse(range.endDate);
10828
+ const rows2 = rowsOf(first).filter((row) => {
10829
+ const at = Date.parse(row.createdAt);
10830
+ return at >= from && at <= to;
10831
+ });
10832
+ if (rows2.length > maxRows) return { status: "too-large", total: rows2.length };
10833
+ onProgress?.(rows2.length, rows2.length);
10834
+ return { status: "ok", rows: rows2, total: rows2.length };
10835
+ }
10836
+ let total = Math.max(0, first.pagination.total);
10837
+ if (total > maxRows) return { status: "too-large", total };
10838
+ const limit = first.pagination.limit > 0 ? first.pagination.limit : batchSize;
10839
+ const requestCap = maxRequests ?? Math.ceil(maxRows / limit) + USAGE_LOGS_EXPORT_EXTRA_REQUESTS;
10840
+ const seen = /* @__PURE__ */ new Set();
10841
+ const rows = [];
10842
+ const add = (batch) => {
10843
+ for (const row of batch) {
10844
+ if (rows.length >= maxRows) return;
10845
+ if (seen.has(row._id)) continue;
10846
+ seen.add(row._id);
10847
+ rows.push(row);
10848
+ }
10849
+ };
10850
+ const hasNextAfter = (pagination, page) => typeof pagination?.hasNextPage === "boolean" ? pagination.hasNextPage : page * limit < total;
10851
+ add(rowsOf(first));
10852
+ onProgress?.(rows.length, total);
10853
+ let hasNext = hasNextAfter(first.pagination, 1);
10854
+ let requests = 1;
10855
+ for (let page = 2; hasNext && rows.length < total; page++) {
10856
+ if (requests >= requestCap) {
10857
+ return { status: "incomplete", fetched: rows.length, total };
10858
+ }
10859
+ if (signal?.aborted) return { status: "cancelled" };
10860
+ const res = await fetchUsageLogs({ ...range, page, limit });
10861
+ requests++;
10862
+ if (signal?.aborted) return { status: "cancelled" };
10863
+ if (res?.pagination && Number.isFinite(res.pagination.total)) {
10864
+ total = Math.max(0, res.pagination.total);
10865
+ if (total > maxRows) return { status: "too-large", total };
10866
+ }
10867
+ const batch = rowsOf(res);
10868
+ if (batch.length === 0) break;
10869
+ add(batch);
10870
+ onProgress?.(rows.length, total);
10871
+ hasNext = hasNextAfter(res?.pagination, page);
10872
+ }
10873
+ return { status: "ok", rows, total };
10874
+ }
10875
+ function usageLogsCsvRows(logs, userMap, formatDate5) {
10876
+ return [
10877
+ ["Date", "Type", "Credits", "Service", "User ID"],
10878
+ ...logs.map((log) => [
10879
+ formatDate5(log.createdAt),
10880
+ log.type,
10881
+ log.amount.toLocaleString("en-US"),
10882
+ log.service?.serviceGroupName ?? log.meta?.info ?? "",
10883
+ log.triggeredByUserId != null ? userMap[log.triggeredByUserId] ?? log.triggeredByUserId : ""
10884
+ ])
10885
+ ];
10886
+ }
10887
+ function toCsv(rows) {
10888
+ return rows.map((r) => r.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")).join("\n");
10889
+ }
10890
+
10662
10891
  // src/billing-content.tsx
10663
10892
  var import_jsx_runtime29 = require("react/jsx-runtime");
10664
10893
  var DEFAULT_TOPUP_SERVICE = "catalogix";
@@ -11544,6 +11773,8 @@ function UsageLogsTable({
11544
11773
  logs,
11545
11774
  userMap,
11546
11775
  loading,
11776
+ error = null,
11777
+ onRetry,
11547
11778
  currentWorkspaceId = null,
11548
11779
  usedByNames = {}
11549
11780
  }) {
@@ -11574,6 +11805,50 @@ function UsageLogsTable({
11574
11805
  }
11575
11806
  );
11576
11807
  }
11808
+ if (error) {
11809
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
11810
+ "div",
11811
+ {
11812
+ role: "alert",
11813
+ className: "flex flex-col items-center justify-center w-full",
11814
+ style: {
11815
+ backgroundColor: "var(--alias-surface-base)",
11816
+ borderRadius: "var(--radius-3xl)",
11817
+ border: "1px solid var(--alias-border-subtle)",
11818
+ padding: "var(--spacing-6xl)",
11819
+ gap: "var(--spacing-md)"
11820
+ },
11821
+ children: [
11822
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
11823
+ "span",
11824
+ {
11825
+ style: {
11826
+ fontFamily: "var(--font-family-font-family-body)",
11827
+ fontSize: "var(--font-size-font-size-sm)",
11828
+ fontWeight: 400,
11829
+ lineHeight: "var(--line-height-line-height-sm)",
11830
+ color: "var(--alias-text---icons-muted)",
11831
+ textAlign: "center"
11832
+ },
11833
+ children: [
11834
+ "Couldn't load usage logs. ",
11835
+ error
11836
+ ]
11837
+ }
11838
+ ),
11839
+ onRetry && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
11840
+ import_ui17.ScButton,
11841
+ {
11842
+ text: "Try again",
11843
+ variant: "tertiary",
11844
+ size: "sm",
11845
+ onClick: onRetry
11846
+ }
11847
+ )
11848
+ ]
11849
+ }
11850
+ );
11851
+ }
11577
11852
  return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
11578
11853
  "div",
11579
11854
  {
@@ -11749,6 +12024,71 @@ function UsageLogsTable({
11749
12024
  }
11750
12025
  );
11751
12026
  }
12027
+ function UsageLogsPager({
12028
+ page,
12029
+ totalPages,
12030
+ total,
12031
+ loading = false,
12032
+ onPageChange
12033
+ }) {
12034
+ if (total <= 0) return null;
12035
+ const canPrevious = !loading && page > 1;
12036
+ const canNext = !loading && page < totalPages;
12037
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
12038
+ "div",
12039
+ {
12040
+ className: "flex items-center justify-between shrink-0 w-full",
12041
+ style: { gap: "var(--spacing-md)" },
12042
+ children: [
12043
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
12044
+ "span",
12045
+ {
12046
+ style: {
12047
+ color: "var(--alias-text---icons-muted)",
12048
+ fontFamily: "var(--font-family-font-family-body)",
12049
+ fontSize: "var(--font-size-font-size-sm)",
12050
+ lineHeight: "var(--line-height-line-height-sm)"
12051
+ },
12052
+ children: [
12053
+ "Page ",
12054
+ page.toLocaleString("en-US"),
12055
+ " of",
12056
+ " ",
12057
+ totalPages.toLocaleString("en-US"),
12058
+ " \xB7",
12059
+ " ",
12060
+ total.toLocaleString("en-US"),
12061
+ " ",
12062
+ total === 1 ? "log" : "logs"
12063
+ ]
12064
+ }
12065
+ ),
12066
+ totalPages > 1 && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex items-center", style: { gap: "var(--spacing-md)" }, children: [
12067
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
12068
+ import_ui17.ScButton,
12069
+ {
12070
+ text: "Previous",
12071
+ variant: "tertiary",
12072
+ size: "sm",
12073
+ state: canPrevious ? "default" : "disabled",
12074
+ onClick: canPrevious ? () => onPageChange(page - 1) : void 0
12075
+ }
12076
+ ),
12077
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
12078
+ import_ui17.ScButton,
12079
+ {
12080
+ text: "Next",
12081
+ variant: "tertiary",
12082
+ size: "sm",
12083
+ state: canNext ? "default" : "disabled",
12084
+ onClick: canNext ? () => onPageChange(page + 1) : void 0
12085
+ }
12086
+ )
12087
+ ] })
12088
+ ]
12089
+ }
12090
+ );
12091
+ }
11752
12092
  function BackToBilling({ onClick }) {
11753
12093
  return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
11754
12094
  "div",
@@ -11828,6 +12168,9 @@ function BillingContent({
11828
12168
  else setShowSharedCreditsInternal(next);
11829
12169
  };
11830
12170
  const [showExportModal, setShowExportModal] = (0, import_react23.useState)(false);
12171
+ const [exportProgress, setExportProgress] = (0, import_react23.useState)(null);
12172
+ const [exportNotice, setExportNotice] = (0, import_react23.useState)(null);
12173
+ const exportAbortRef = (0, import_react23.useRef)(null);
11831
12174
  const [showBuyCreditsModal, setShowBuyCreditsModal] = (0, import_react23.useState)(false);
11832
12175
  const [subscription, setSubscription] = (0, import_react23.useState)(null);
11833
12176
  const [entitlement, setEntitlement] = (0, import_react23.useState)(
@@ -11839,7 +12182,24 @@ function BillingContent({
11839
12182
  const [products, setProducts] = (0, import_react23.useState)([]);
11840
12183
  const [invoices, setInvoices] = (0, import_react23.useState)([]);
11841
12184
  const [totalCredits, setTotalCredits] = (0, import_react23.useState)(null);
11842
- const [usageLogs, setUsageLogs] = (0, import_react23.useState)([]);
12185
+ const [usageLogsState, dispatchUsageLogs] = (0, import_react23.useReducer)(
12186
+ usageLogsReducer,
12187
+ INITIAL_USAGE_LOGS_STATE
12188
+ );
12189
+ const {
12190
+ logs: usageLogs,
12191
+ loading: loadingUsageLogs,
12192
+ error: usageLogsError
12193
+ } = usageLogsState;
12194
+ const usageLogsRequestRef = (0, import_react23.useRef)(0);
12195
+ const workspaceIdRef = (0, import_react23.useRef)(workspaceId);
12196
+ workspaceIdRef.current = workspaceId;
12197
+ const loadUsedByNamesRef = (0, import_react23.useRef)(null);
12198
+ loadUsedByNamesRef.current ??= createUsedByNamesLoader((forWorkspace, names) => {
12199
+ if (forWorkspace !== workspaceIdRef.current) return false;
12200
+ setUsedByNames(names);
12201
+ return true;
12202
+ });
11843
12203
  const [userMap, setUserMap] = (0, import_react23.useState)({});
11844
12204
  const [customPlanCredits, setCustomPlanCredits] = (0, import_react23.useState)(
11845
12205
  null
@@ -11923,8 +12283,68 @@ function BillingContent({
11923
12283
  } catch {
11924
12284
  }
11925
12285
  }, [api]);
12286
+ const ensureUsedByNames = (0, import_react23.useCallback)(
12287
+ (logs) => {
12288
+ const fetchCreditPool = api.fetchCreditPool;
12289
+ if (!fetchCreditPool || !workspaceId || !usageLogsSpentByOthers(logs, workspaceId)) {
12290
+ return;
12291
+ }
12292
+ void loadUsedByNamesRef.current?.load(
12293
+ workspaceId,
12294
+ () => fetchCreditPool.call(api)
12295
+ );
12296
+ },
12297
+ [api, workspaceId]
12298
+ );
12299
+ const loadUsageLogsPage = (0, import_react23.useCallback)(
12300
+ async (page) => {
12301
+ const request = ++usageLogsRequestRef.current;
12302
+ if (!api.fetchUsageLogs) {
12303
+ dispatchUsageLogs({ type: "loaded", logs: EMPTY_USAGE_LOGS_PAGE });
12304
+ return;
12305
+ }
12306
+ dispatchUsageLogs({ type: "request", page });
12307
+ try {
12308
+ const res = await api.fetchUsageLogs({
12309
+ page,
12310
+ limit: USAGE_LOGS_PAGE_SIZE
12311
+ });
12312
+ if (request !== usageLogsRequestRef.current) return;
12313
+ const next = usageLogsPage(res, page);
12314
+ dispatchUsageLogs({ type: "loaded", logs: next });
12315
+ ensureUsedByNames(next);
12316
+ } catch (err) {
12317
+ if (request !== usageLogsRequestRef.current) return;
12318
+ dispatchUsageLogs({
12319
+ type: "failed",
12320
+ error: errorMessage(err, { method: "GET" })
12321
+ });
12322
+ }
12323
+ },
12324
+ [api, ensureUsedByNames]
12325
+ );
12326
+ const goToUsageLogsPage = (0, import_react23.useCallback)(
12327
+ (page) => {
12328
+ if (usageLogs.unpaged && !usageLogsError) {
12329
+ dispatchUsageLogs({ type: "turn", page });
12330
+ ensureUsedByNames(usageLogs);
12331
+ return;
12332
+ }
12333
+ void loadUsageLogsPage(page);
12334
+ },
12335
+ [usageLogs, usageLogsError, loadUsageLogsPage, ensureUsedByNames]
12336
+ );
12337
+ (0, import_react23.useEffect)(() => {
12338
+ if (!loadingUsageLogs && !usageLogsError && usageLogs.unpaged === null && usageLogs.rows.length === 0 && usageLogs.total > 0 && usageLogs.page > usageLogs.totalPages) {
12339
+ void loadUsageLogsPage(usageLogs.totalPages);
12340
+ }
12341
+ }, [usageLogs, loadingUsageLogs, usageLogsError, loadUsageLogsPage]);
11926
12342
  const fetchTableData = (0, import_react23.useCallback)(
11927
12343
  async (tab) => {
12344
+ if (tab === "usage") {
12345
+ await loadUsageLogsPage(1);
12346
+ return;
12347
+ }
11928
12348
  setLoadingTable(true);
11929
12349
  try {
11930
12350
  if (tab === "invoice") {
@@ -11936,25 +12356,14 @@ function BillingContent({
11936
12356
  (item) => item.expiresAt && item.expiresAt.split("T")[0] > today
11937
12357
  ).reduce((total, item) => total + item.amount, 0)
11938
12358
  );
11939
- } else if (api.fetchUsageLogs) {
11940
- const res = await api.fetchUsageLogs();
11941
- const logs = res.data?.length ? res.data : [];
11942
- setUsageLogs(logs);
11943
- if (api.fetchCreditPool && logs.some((log) => usedByWorkspaceKey(log, workspaceId))) {
11944
- const pool = await api.fetchCreditPool().catch(() => null);
11945
- setUsedByNames(usedByNamesFrom(pool));
11946
- }
11947
- } else {
11948
- setUsageLogs([]);
11949
12359
  }
11950
12360
  } catch {
11951
- if (tab === "invoice") setInvoices([]);
11952
- else setUsageLogs([]);
12361
+ setInvoices([]);
11953
12362
  } finally {
11954
12363
  setLoadingTable(false);
11955
12364
  }
11956
12365
  },
11957
- [api, workspaceId]
12366
+ [api, loadUsageLogsPage]
11958
12367
  );
11959
12368
  const handlePlanAction = (0, import_react23.useCallback)(
11960
12369
  async ({ productId, priceId, planName: planName2 }) => {
@@ -12061,6 +12470,10 @@ function BillingContent({
12061
12470
  setProducts([]);
12062
12471
  setTotalCredits(null);
12063
12472
  setCustomPlanCredits(null);
12473
+ usageLogsRequestRef.current++;
12474
+ dispatchUsageLogs({ type: "reset" });
12475
+ loadUsedByNamesRef.current?.reset();
12476
+ setUsedByNames({});
12064
12477
  }, [workspaceId]);
12065
12478
  (0, import_react23.useEffect)(() => {
12066
12479
  setInitialFetchTriggered(true);
@@ -12163,35 +12576,74 @@ function BillingContent({
12163
12576
  toast.success(`Opened ${successCount} invoice PDF(s).`);
12164
12577
  }
12165
12578
  } else {
12166
- const filtered = usageLogs.filter((log) => {
12167
- const d = new Date(log.createdAt);
12168
- return d >= startDate && d <= end;
12579
+ await exportUsageLogs(startDate, endDate);
12580
+ }
12581
+ };
12582
+ const closeExportModal = () => {
12583
+ exportAbortRef.current?.abort();
12584
+ exportAbortRef.current = null;
12585
+ setExportProgress(null);
12586
+ setExportNotice(null);
12587
+ setShowExportModal(false);
12588
+ };
12589
+ (0, import_react23.useEffect)(() => () => exportAbortRef.current?.abort(), []);
12590
+ const exportUsageLogs = async (startDate, endDate) => {
12591
+ if (!api.fetchUsageLogs) {
12592
+ toast.info?.("Usage logs are not available in this workspace.");
12593
+ closeExportModal();
12594
+ return;
12595
+ }
12596
+ exportAbortRef.current?.abort();
12597
+ const controller = new AbortController();
12598
+ exportAbortRef.current = controller;
12599
+ setExportNotice(null);
12600
+ setExportProgress({ fetched: 0, total: 0 });
12601
+ const range = usageLogsDateRange(startDate, endDate);
12602
+ try {
12603
+ const result = await fetchUsageLogsInRange(api.fetchUsageLogs, range, {
12604
+ signal: controller.signal,
12605
+ onProgress: (fetched, total) => {
12606
+ if (!controller.signal.aborted) setExportProgress({ fetched, total });
12607
+ }
12169
12608
  });
12170
- if (filtered.length === 0) {
12609
+ if (result.status === "cancelled" || controller.signal.aborted) return;
12610
+ if (result.status === "too-large") {
12611
+ setExportNotice(
12612
+ `${result.total.toLocaleString("en-US")} logs in this range; an export holds up to ${USAGE_LOGS_EXPORT_MAX_ROWS.toLocaleString("en-US")}. Choose a shorter range.`
12613
+ );
12614
+ return;
12615
+ }
12616
+ if (result.status === "incomplete") {
12617
+ setExportNotice(
12618
+ "Usage logs kept changing while exporting, so some were missed. Try again."
12619
+ );
12620
+ return;
12621
+ }
12622
+ if (result.rows.length === 0) {
12171
12623
  toast.info?.("No usage logs found in the selected date range.");
12172
- setShowExportModal(false);
12624
+ closeExportModal();
12173
12625
  return;
12174
12626
  }
12175
- const headers = ["Date", "Type", "Credits", "Service", "User ID"];
12176
- const rows = filtered.map((log) => [
12177
- formatDate3(log.createdAt),
12178
- log.type,
12179
- log.amount.toLocaleString("en-US"),
12180
- log.service?.serviceGroupName ?? log.meta?.info ?? "",
12181
- log.triggeredByUserId != null ? userMap[log.triggeredByUserId] ?? log.triggeredByUserId : ""
12182
- ]);
12183
12627
  downloadCsv(
12184
- [headers, ...rows],
12185
- `usage_logs_${fmtFilename(startDate)}_${fmtFilename(end)}.csv`
12628
+ usageLogsCsvRows(result.rows, userMap, formatDate3),
12629
+ usageLogsExportFileName(startDate, endDate)
12186
12630
  );
12187
- toast.success(`Exported ${filtered.length} log(s).`);
12188
- setShowExportModal(false);
12631
+ toast.success(`Exported ${result.rows.length.toLocaleString("en-US")} log(s).`);
12632
+ closeExportModal();
12633
+ } catch (err) {
12634
+ if (controller.signal.aborted) return;
12635
+ setExportNotice(
12636
+ `Couldn't export usage logs. ${errorMessage(err, { method: "GET" })}`
12637
+ );
12638
+ } finally {
12639
+ if (exportAbortRef.current === controller) {
12640
+ exportAbortRef.current = null;
12641
+ setExportProgress(null);
12642
+ }
12189
12643
  }
12190
12644
  };
12191
12645
  function downloadCsv(rows, filename) {
12192
- const csv = rows.map(
12193
- (r) => r.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")
12194
- ).join("\n");
12646
+ const csv = toCsv(rows);
12195
12647
  const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
12196
12648
  const url = URL.createObjectURL(blob);
12197
12649
  const a = document.createElement("a");
@@ -12202,9 +12654,6 @@ function BillingContent({
12202
12654
  document.body.removeChild(a);
12203
12655
  URL.revokeObjectURL(url);
12204
12656
  }
12205
- function fmtFilename(d) {
12206
- return d.toISOString().slice(0, 10);
12207
- }
12208
12657
  async function handleBuyCredits(credits2, price) {
12209
12658
  if (!workspaceId) {
12210
12659
  toast.error("Missing workspace details for credit top-up");
@@ -12657,22 +13106,39 @@ function BillingContent({
12657
13106
  loading: loadingTable,
12658
13107
  workspaceCountry: workspace.current?.country
12659
13108
  }
12660
- ) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
12661
- UsageLogsTable,
12662
- {
12663
- logs: usageLogs,
12664
- userMap,
12665
- loading: loadingTable,
12666
- currentWorkspaceId: workspaceId,
12667
- usedByNames
12668
- }
12669
- ),
13109
+ ) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
13110
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
13111
+ UsageLogsTable,
13112
+ {
13113
+ logs: usageLogs.rows,
13114
+ userMap,
13115
+ loading: loadingUsageLogs,
13116
+ error: usageLogsError,
13117
+ onRetry: () => void loadUsageLogsPage(usageLogsState.requestedPage),
13118
+ currentWorkspaceId: workspaceId,
13119
+ usedByNames
13120
+ }
13121
+ ),
13122
+ !usageLogsError && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
13123
+ UsageLogsPager,
13124
+ {
13125
+ page: usageLogs.page,
13126
+ totalPages: usageLogs.totalPages,
13127
+ total: usageLogs.total,
13128
+ loading: loadingUsageLogs,
13129
+ onPageChange: goToUsageLogsPage
13130
+ }
13131
+ )
13132
+ ] }),
12670
13133
  showExportModal && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
12671
13134
  ExportDateModal,
12672
13135
  {
12673
13136
  title: activeTab === "invoice" ? "Export invoices" : "Export log",
12674
- onClose: () => setShowExportModal(false),
12675
- onExport: handleExport
13137
+ onClose: closeExportModal,
13138
+ onExport: handleExport,
13139
+ busy: exportProgress !== null,
13140
+ busyText: exportProgress && exportProgress.total > 0 ? `Fetched ${exportProgress.fetched.toLocaleString("en-US")} of ${exportProgress.total.toLocaleString("en-US")} logs\u2026` : "Fetching logs\u2026",
13141
+ notice: exportNotice
12676
13142
  }
12677
13143
  ),
12678
13144
  showBuyCreditsModal && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(