@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.mjs CHANGED
@@ -7676,6 +7676,7 @@ import {
7676
7676
  useEffect as useEffect18,
7677
7677
  useCallback as useCallback8,
7678
7678
  useMemo as useMemo4,
7679
+ useReducer,
7679
7680
  useRef as useRef10
7680
7681
  } from "react";
7681
7682
  import {
@@ -7826,7 +7827,10 @@ function formatDateRange(date) {
7826
7827
  function ExportDateModal({
7827
7828
  title,
7828
7829
  onClose,
7829
- onExport
7830
+ onExport,
7831
+ busy = false,
7832
+ busyText,
7833
+ notice = null
7830
7834
  }) {
7831
7835
  useOverlayOpen(true, onClose);
7832
7836
  const now = /* @__PURE__ */ new Date();
@@ -7913,6 +7917,7 @@ function ExportDateModal({
7913
7917
  [rangeStart, rangeEnd, isFuture]
7914
7918
  );
7915
7919
  const handleExport = () => {
7920
+ if (busy) return;
7916
7921
  if (rangeStart && rangeEnd) onExport(rangeStart, rangeEnd);
7917
7922
  };
7918
7923
  const getCellVisuals = (cell, idx) => {
@@ -8134,24 +8139,42 @@ function ExportDateModal({
8134
8139
  borderTop: "1px solid var(--alias-border-subtle)"
8135
8140
  },
8136
8141
  children: [
8137
- /* @__PURE__ */ jsx21(
8138
- "p",
8139
- {
8140
- className: "flex-1 min-w-0 truncate",
8141
- style: {
8142
- fontFamily: "var(--font-family-font-family-body)",
8143
- fontSize: "var(--font-size-font-size-md)",
8144
- fontWeight: 400,
8145
- lineHeight: "var(--line-height-line-height-md)",
8146
- color: "var(--alias-text---icons-primary)"
8147
- },
8148
- children: rangeStart && rangeEnd ? `${formatDateRange(rangeStart)} - ${formatDateRange(rangeEnd)}` : rangeStart ? `${formatDateRange(rangeStart)} - Select end date` : "Select date range"
8149
- }
8150
- ),
8142
+ /* @__PURE__ */ jsxs18("div", { className: "flex-1 min-w-0 flex flex-col", children: [
8143
+ /* @__PURE__ */ jsx21(
8144
+ "p",
8145
+ {
8146
+ className: "min-w-0 truncate",
8147
+ style: {
8148
+ fontFamily: "var(--font-family-font-family-body)",
8149
+ fontSize: "var(--font-size-font-size-md)",
8150
+ fontWeight: 400,
8151
+ lineHeight: "var(--line-height-line-height-md)",
8152
+ color: "var(--alias-text---icons-primary)"
8153
+ },
8154
+ "aria-live": "polite",
8155
+ children: busy && busyText ? busyText : rangeStart && rangeEnd ? `${formatDateRange(rangeStart)} - ${formatDateRange(rangeEnd)}` : rangeStart ? `${formatDateRange(rangeStart)} - Select end date` : "Select date range"
8156
+ }
8157
+ ),
8158
+ notice && !busy && /* @__PURE__ */ jsx21(
8159
+ "p",
8160
+ {
8161
+ role: "alert",
8162
+ style: {
8163
+ fontFamily: "var(--font-family-font-family-body)",
8164
+ fontSize: "var(--font-size-font-size-sm)",
8165
+ fontWeight: 400,
8166
+ lineHeight: "var(--line-height-line-height-sm)",
8167
+ color: "var(--alias-text---icons-error)"
8168
+ },
8169
+ children: notice
8170
+ }
8171
+ )
8172
+ ] }),
8151
8173
  /* @__PURE__ */ jsx21(
8152
8174
  "div",
8153
8175
  {
8154
- className: `flex items-center justify-center shrink-0 ${rangeStart && rangeEnd ? "cursor-pointer" : "opacity-50 cursor-not-allowed"}`,
8176
+ className: `flex items-center justify-center shrink-0 ${rangeStart && rangeEnd && !busy ? "cursor-pointer" : "opacity-50 cursor-not-allowed"}`,
8177
+ "aria-disabled": busy || !(rangeStart && rangeEnd),
8155
8178
  style: {
8156
8179
  backgroundColor: "var(--alias-text---icons-primary)",
8157
8180
  borderRadius: "var(--radius-xl)",
@@ -8169,7 +8192,7 @@ function ExportDateModal({
8169
8192
  lineHeight: "var(--line-height-line-height-sm)",
8170
8193
  color: "var(--alias-text---icons-inverse)"
8171
8194
  },
8172
- children: "Export"
8195
+ children: busy ? "Exporting\u2026" : "Export"
8173
8196
  }
8174
8197
  )
8175
8198
  }
@@ -10588,6 +10611,196 @@ async function loadCurrentPlan(fetchCurrentPlan) {
10588
10611
  return { subscription, entitlement, paid: hasPaidFeatures({ entitlement, subscription }) };
10589
10612
  }
10590
10613
 
10614
+ // src/usage-logs.ts
10615
+ var USAGE_LOGS_PAGE_SIZE = 50;
10616
+ var USAGE_LOGS_EXPORT_BATCH_SIZE = 100;
10617
+ var USAGE_LOGS_EXPORT_MAX_ROWS = 2e4;
10618
+ var USAGE_LOGS_EXPORT_EXTRA_REQUESTS = 10;
10619
+ function rowsOf(res) {
10620
+ return Array.isArray(res?.data) ? res.data : [];
10621
+ }
10622
+ function sliceUsageLogsPage(all, page, pageSize = USAGE_LOGS_PAGE_SIZE) {
10623
+ const total = all.length;
10624
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
10625
+ const safePage = Math.min(Math.max(1, Math.floor(page) || 1), totalPages);
10626
+ return {
10627
+ rows: all.slice((safePage - 1) * pageSize, safePage * pageSize),
10628
+ page: safePage,
10629
+ totalPages,
10630
+ total,
10631
+ unpaged: all
10632
+ };
10633
+ }
10634
+ function usageLogsPage(res, page, pageSize = USAGE_LOGS_PAGE_SIZE) {
10635
+ const data = rowsOf(res);
10636
+ const pagination = res?.pagination;
10637
+ if (!pagination || !Number.isFinite(pagination.total)) {
10638
+ return sliceUsageLogsPage(data, page, pageSize);
10639
+ }
10640
+ const total = Math.max(0, pagination.total);
10641
+ const limit = pagination.limit > 0 ? pagination.limit : pageSize;
10642
+ const totalPages = Math.max(
10643
+ 1,
10644
+ pagination.totalPages > 0 ? pagination.totalPages : Math.ceil(total / limit)
10645
+ );
10646
+ return {
10647
+ rows: data.slice(0, pageSize),
10648
+ page: pagination.page > 0 ? pagination.page : page,
10649
+ totalPages,
10650
+ total,
10651
+ unpaged: null
10652
+ };
10653
+ }
10654
+ var EMPTY_USAGE_LOGS_PAGE = {
10655
+ rows: [],
10656
+ page: 1,
10657
+ totalPages: 1,
10658
+ total: 0,
10659
+ unpaged: null
10660
+ };
10661
+ var INITIAL_USAGE_LOGS_STATE = {
10662
+ logs: EMPTY_USAGE_LOGS_PAGE,
10663
+ requestedPage: 1,
10664
+ loading: false,
10665
+ error: null
10666
+ };
10667
+ function usageLogsReducer(state, action) {
10668
+ switch (action.type) {
10669
+ case "request":
10670
+ return { ...state, requestedPage: action.page, loading: true, error: null };
10671
+ case "loaded":
10672
+ return {
10673
+ logs: action.logs,
10674
+ requestedPage: action.logs.page,
10675
+ loading: false,
10676
+ error: null
10677
+ };
10678
+ case "failed":
10679
+ return { ...state, loading: false, error: action.error };
10680
+ case "turn": {
10681
+ if (!state.logs.unpaged || state.error) return state;
10682
+ const logs = sliceUsageLogsPage(state.logs.unpaged, action.page);
10683
+ return { ...state, logs, requestedPage: logs.page };
10684
+ }
10685
+ case "reset":
10686
+ return INITIAL_USAGE_LOGS_STATE;
10687
+ }
10688
+ }
10689
+ function usageLogsSpentByOthers(logs, workspaceId) {
10690
+ return (logs.unpaged ?? logs.rows).some(
10691
+ (log) => usedByWorkspaceKey(log, workspaceId) !== null
10692
+ );
10693
+ }
10694
+ function createUsedByNamesLoader(save) {
10695
+ let savedFor = null;
10696
+ let inFlight = null;
10697
+ return {
10698
+ async load(workspaceId, fetchCreditPool) {
10699
+ if (savedFor === workspaceId || inFlight?.workspaceId === workspaceId) {
10700
+ return;
10701
+ }
10702
+ const lookup = { workspaceId };
10703
+ inFlight = lookup;
10704
+ try {
10705
+ const pool = await fetchCreditPool();
10706
+ if (inFlight !== lookup) return;
10707
+ if (save(workspaceId, usedByNamesFrom(pool))) savedFor = workspaceId;
10708
+ } catch {
10709
+ } finally {
10710
+ if (inFlight === lookup) inFlight = null;
10711
+ }
10712
+ },
10713
+ reset() {
10714
+ savedFor = null;
10715
+ inFlight = null;
10716
+ }
10717
+ };
10718
+ }
10719
+ function usageLogsExportFileName(start, end) {
10720
+ return `usage_logs_${toLocalDateInput(start)}_${toLocalDateInput(end)}.csv`;
10721
+ }
10722
+ function usageLogsDateRange(start, end) {
10723
+ const from = new Date(start);
10724
+ from.setHours(0, 0, 0, 0);
10725
+ const to = new Date(end);
10726
+ to.setHours(23, 59, 59, 999);
10727
+ return { startDate: from.toISOString(), endDate: to.toISOString() };
10728
+ }
10729
+ async function fetchUsageLogsInRange(fetchUsageLogs, range, {
10730
+ batchSize = USAGE_LOGS_EXPORT_BATCH_SIZE,
10731
+ maxRows = USAGE_LOGS_EXPORT_MAX_ROWS,
10732
+ maxRequests,
10733
+ onProgress,
10734
+ signal
10735
+ } = {}) {
10736
+ const first = await fetchUsageLogs({ ...range, page: 1, limit: batchSize });
10737
+ if (signal?.aborted) return { status: "cancelled" };
10738
+ if (!first?.pagination || !Number.isFinite(first.pagination.total)) {
10739
+ const from = Date.parse(range.startDate);
10740
+ const to = Date.parse(range.endDate);
10741
+ const rows2 = rowsOf(first).filter((row) => {
10742
+ const at = Date.parse(row.createdAt);
10743
+ return at >= from && at <= to;
10744
+ });
10745
+ if (rows2.length > maxRows) return { status: "too-large", total: rows2.length };
10746
+ onProgress?.(rows2.length, rows2.length);
10747
+ return { status: "ok", rows: rows2, total: rows2.length };
10748
+ }
10749
+ let total = Math.max(0, first.pagination.total);
10750
+ if (total > maxRows) return { status: "too-large", total };
10751
+ const limit = first.pagination.limit > 0 ? first.pagination.limit : batchSize;
10752
+ const requestCap = maxRequests ?? Math.ceil(maxRows / limit) + USAGE_LOGS_EXPORT_EXTRA_REQUESTS;
10753
+ const seen = /* @__PURE__ */ new Set();
10754
+ const rows = [];
10755
+ const add = (batch) => {
10756
+ for (const row of batch) {
10757
+ if (rows.length >= maxRows) return;
10758
+ if (seen.has(row._id)) continue;
10759
+ seen.add(row._id);
10760
+ rows.push(row);
10761
+ }
10762
+ };
10763
+ const hasNextAfter = (pagination, page) => typeof pagination?.hasNextPage === "boolean" ? pagination.hasNextPage : page * limit < total;
10764
+ add(rowsOf(first));
10765
+ onProgress?.(rows.length, total);
10766
+ let hasNext = hasNextAfter(first.pagination, 1);
10767
+ let requests = 1;
10768
+ for (let page = 2; hasNext && rows.length < total; page++) {
10769
+ if (requests >= requestCap) {
10770
+ return { status: "incomplete", fetched: rows.length, total };
10771
+ }
10772
+ if (signal?.aborted) return { status: "cancelled" };
10773
+ const res = await fetchUsageLogs({ ...range, page, limit });
10774
+ requests++;
10775
+ if (signal?.aborted) return { status: "cancelled" };
10776
+ if (res?.pagination && Number.isFinite(res.pagination.total)) {
10777
+ total = Math.max(0, res.pagination.total);
10778
+ if (total > maxRows) return { status: "too-large", total };
10779
+ }
10780
+ const batch = rowsOf(res);
10781
+ if (batch.length === 0) break;
10782
+ add(batch);
10783
+ onProgress?.(rows.length, total);
10784
+ hasNext = hasNextAfter(res?.pagination, page);
10785
+ }
10786
+ return { status: "ok", rows, total };
10787
+ }
10788
+ function usageLogsCsvRows(logs, userMap, formatDate5) {
10789
+ return [
10790
+ ["Date", "Type", "Credits", "Service", "User ID"],
10791
+ ...logs.map((log) => [
10792
+ formatDate5(log.createdAt),
10793
+ log.type,
10794
+ log.amount.toLocaleString("en-US"),
10795
+ log.service?.serviceGroupName ?? log.meta?.info ?? "",
10796
+ log.triggeredByUserId != null ? userMap[log.triggeredByUserId] ?? log.triggeredByUserId : ""
10797
+ ])
10798
+ ];
10799
+ }
10800
+ function toCsv(rows) {
10801
+ return rows.map((r) => r.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")).join("\n");
10802
+ }
10803
+
10591
10804
  // src/billing-content.tsx
10592
10805
  import { Fragment as Fragment11, jsx as jsx29, jsxs as jsxs26 } from "react/jsx-runtime";
10593
10806
  var DEFAULT_TOPUP_SERVICE = "catalogix";
@@ -11473,6 +11686,8 @@ function UsageLogsTable({
11473
11686
  logs,
11474
11687
  userMap,
11475
11688
  loading,
11689
+ error = null,
11690
+ onRetry,
11476
11691
  currentWorkspaceId = null,
11477
11692
  usedByNames = {}
11478
11693
  }) {
@@ -11503,6 +11718,50 @@ function UsageLogsTable({
11503
11718
  }
11504
11719
  );
11505
11720
  }
11721
+ if (error) {
11722
+ return /* @__PURE__ */ jsxs26(
11723
+ "div",
11724
+ {
11725
+ role: "alert",
11726
+ className: "flex flex-col items-center justify-center w-full",
11727
+ style: {
11728
+ backgroundColor: "var(--alias-surface-base)",
11729
+ borderRadius: "var(--radius-3xl)",
11730
+ border: "1px solid var(--alias-border-subtle)",
11731
+ padding: "var(--spacing-6xl)",
11732
+ gap: "var(--spacing-md)"
11733
+ },
11734
+ children: [
11735
+ /* @__PURE__ */ jsxs26(
11736
+ "span",
11737
+ {
11738
+ style: {
11739
+ fontFamily: "var(--font-family-font-family-body)",
11740
+ fontSize: "var(--font-size-font-size-sm)",
11741
+ fontWeight: 400,
11742
+ lineHeight: "var(--line-height-line-height-sm)",
11743
+ color: "var(--alias-text---icons-muted)",
11744
+ textAlign: "center"
11745
+ },
11746
+ children: [
11747
+ "Couldn't load usage logs. ",
11748
+ error
11749
+ ]
11750
+ }
11751
+ ),
11752
+ onRetry && /* @__PURE__ */ jsx29(
11753
+ ScButton14,
11754
+ {
11755
+ text: "Try again",
11756
+ variant: "tertiary",
11757
+ size: "sm",
11758
+ onClick: onRetry
11759
+ }
11760
+ )
11761
+ ]
11762
+ }
11763
+ );
11764
+ }
11506
11765
  return /* @__PURE__ */ jsxs26(
11507
11766
  "div",
11508
11767
  {
@@ -11678,6 +11937,71 @@ function UsageLogsTable({
11678
11937
  }
11679
11938
  );
11680
11939
  }
11940
+ function UsageLogsPager({
11941
+ page,
11942
+ totalPages,
11943
+ total,
11944
+ loading = false,
11945
+ onPageChange
11946
+ }) {
11947
+ if (total <= 0) return null;
11948
+ const canPrevious = !loading && page > 1;
11949
+ const canNext = !loading && page < totalPages;
11950
+ return /* @__PURE__ */ jsxs26(
11951
+ "div",
11952
+ {
11953
+ className: "flex items-center justify-between shrink-0 w-full",
11954
+ style: { gap: "var(--spacing-md)" },
11955
+ children: [
11956
+ /* @__PURE__ */ jsxs26(
11957
+ "span",
11958
+ {
11959
+ style: {
11960
+ color: "var(--alias-text---icons-muted)",
11961
+ fontFamily: "var(--font-family-font-family-body)",
11962
+ fontSize: "var(--font-size-font-size-sm)",
11963
+ lineHeight: "var(--line-height-line-height-sm)"
11964
+ },
11965
+ children: [
11966
+ "Page ",
11967
+ page.toLocaleString("en-US"),
11968
+ " of",
11969
+ " ",
11970
+ totalPages.toLocaleString("en-US"),
11971
+ " \xB7",
11972
+ " ",
11973
+ total.toLocaleString("en-US"),
11974
+ " ",
11975
+ total === 1 ? "log" : "logs"
11976
+ ]
11977
+ }
11978
+ ),
11979
+ totalPages > 1 && /* @__PURE__ */ jsxs26("div", { className: "flex items-center", style: { gap: "var(--spacing-md)" }, children: [
11980
+ /* @__PURE__ */ jsx29(
11981
+ ScButton14,
11982
+ {
11983
+ text: "Previous",
11984
+ variant: "tertiary",
11985
+ size: "sm",
11986
+ state: canPrevious ? "default" : "disabled",
11987
+ onClick: canPrevious ? () => onPageChange(page - 1) : void 0
11988
+ }
11989
+ ),
11990
+ /* @__PURE__ */ jsx29(
11991
+ ScButton14,
11992
+ {
11993
+ text: "Next",
11994
+ variant: "tertiary",
11995
+ size: "sm",
11996
+ state: canNext ? "default" : "disabled",
11997
+ onClick: canNext ? () => onPageChange(page + 1) : void 0
11998
+ }
11999
+ )
12000
+ ] })
12001
+ ]
12002
+ }
12003
+ );
12004
+ }
11681
12005
  function BackToBilling({ onClick }) {
11682
12006
  return /* @__PURE__ */ jsxs26(
11683
12007
  "div",
@@ -11757,6 +12081,9 @@ function BillingContent({
11757
12081
  else setShowSharedCreditsInternal(next);
11758
12082
  };
11759
12083
  const [showExportModal, setShowExportModal] = useState21(false);
12084
+ const [exportProgress, setExportProgress] = useState21(null);
12085
+ const [exportNotice, setExportNotice] = useState21(null);
12086
+ const exportAbortRef = useRef10(null);
11760
12087
  const [showBuyCreditsModal, setShowBuyCreditsModal] = useState21(false);
11761
12088
  const [subscription, setSubscription] = useState21(null);
11762
12089
  const [entitlement, setEntitlement] = useState21(
@@ -11768,7 +12095,24 @@ function BillingContent({
11768
12095
  const [products, setProducts] = useState21([]);
11769
12096
  const [invoices, setInvoices] = useState21([]);
11770
12097
  const [totalCredits, setTotalCredits] = useState21(null);
11771
- const [usageLogs, setUsageLogs] = useState21([]);
12098
+ const [usageLogsState, dispatchUsageLogs] = useReducer(
12099
+ usageLogsReducer,
12100
+ INITIAL_USAGE_LOGS_STATE
12101
+ );
12102
+ const {
12103
+ logs: usageLogs,
12104
+ loading: loadingUsageLogs,
12105
+ error: usageLogsError
12106
+ } = usageLogsState;
12107
+ const usageLogsRequestRef = useRef10(0);
12108
+ const workspaceIdRef = useRef10(workspaceId);
12109
+ workspaceIdRef.current = workspaceId;
12110
+ const loadUsedByNamesRef = useRef10(null);
12111
+ loadUsedByNamesRef.current ??= createUsedByNamesLoader((forWorkspace, names) => {
12112
+ if (forWorkspace !== workspaceIdRef.current) return false;
12113
+ setUsedByNames(names);
12114
+ return true;
12115
+ });
11772
12116
  const [userMap, setUserMap] = useState21({});
11773
12117
  const [customPlanCredits, setCustomPlanCredits] = useState21(
11774
12118
  null
@@ -11852,8 +12196,68 @@ function BillingContent({
11852
12196
  } catch {
11853
12197
  }
11854
12198
  }, [api]);
12199
+ const ensureUsedByNames = useCallback8(
12200
+ (logs) => {
12201
+ const fetchCreditPool = api.fetchCreditPool;
12202
+ if (!fetchCreditPool || !workspaceId || !usageLogsSpentByOthers(logs, workspaceId)) {
12203
+ return;
12204
+ }
12205
+ void loadUsedByNamesRef.current?.load(
12206
+ workspaceId,
12207
+ () => fetchCreditPool.call(api)
12208
+ );
12209
+ },
12210
+ [api, workspaceId]
12211
+ );
12212
+ const loadUsageLogsPage = useCallback8(
12213
+ async (page) => {
12214
+ const request = ++usageLogsRequestRef.current;
12215
+ if (!api.fetchUsageLogs) {
12216
+ dispatchUsageLogs({ type: "loaded", logs: EMPTY_USAGE_LOGS_PAGE });
12217
+ return;
12218
+ }
12219
+ dispatchUsageLogs({ type: "request", page });
12220
+ try {
12221
+ const res = await api.fetchUsageLogs({
12222
+ page,
12223
+ limit: USAGE_LOGS_PAGE_SIZE
12224
+ });
12225
+ if (request !== usageLogsRequestRef.current) return;
12226
+ const next = usageLogsPage(res, page);
12227
+ dispatchUsageLogs({ type: "loaded", logs: next });
12228
+ ensureUsedByNames(next);
12229
+ } catch (err) {
12230
+ if (request !== usageLogsRequestRef.current) return;
12231
+ dispatchUsageLogs({
12232
+ type: "failed",
12233
+ error: errorMessage(err, { method: "GET" })
12234
+ });
12235
+ }
12236
+ },
12237
+ [api, ensureUsedByNames]
12238
+ );
12239
+ const goToUsageLogsPage = useCallback8(
12240
+ (page) => {
12241
+ if (usageLogs.unpaged && !usageLogsError) {
12242
+ dispatchUsageLogs({ type: "turn", page });
12243
+ ensureUsedByNames(usageLogs);
12244
+ return;
12245
+ }
12246
+ void loadUsageLogsPage(page);
12247
+ },
12248
+ [usageLogs, usageLogsError, loadUsageLogsPage, ensureUsedByNames]
12249
+ );
12250
+ useEffect18(() => {
12251
+ if (!loadingUsageLogs && !usageLogsError && usageLogs.unpaged === null && usageLogs.rows.length === 0 && usageLogs.total > 0 && usageLogs.page > usageLogs.totalPages) {
12252
+ void loadUsageLogsPage(usageLogs.totalPages);
12253
+ }
12254
+ }, [usageLogs, loadingUsageLogs, usageLogsError, loadUsageLogsPage]);
11855
12255
  const fetchTableData = useCallback8(
11856
12256
  async (tab) => {
12257
+ if (tab === "usage") {
12258
+ await loadUsageLogsPage(1);
12259
+ return;
12260
+ }
11857
12261
  setLoadingTable(true);
11858
12262
  try {
11859
12263
  if (tab === "invoice") {
@@ -11865,25 +12269,14 @@ function BillingContent({
11865
12269
  (item) => item.expiresAt && item.expiresAt.split("T")[0] > today
11866
12270
  ).reduce((total, item) => total + item.amount, 0)
11867
12271
  );
11868
- } else if (api.fetchUsageLogs) {
11869
- const res = await api.fetchUsageLogs();
11870
- const logs = res.data?.length ? res.data : [];
11871
- setUsageLogs(logs);
11872
- if (api.fetchCreditPool && logs.some((log) => usedByWorkspaceKey(log, workspaceId))) {
11873
- const pool = await api.fetchCreditPool().catch(() => null);
11874
- setUsedByNames(usedByNamesFrom(pool));
11875
- }
11876
- } else {
11877
- setUsageLogs([]);
11878
12272
  }
11879
12273
  } catch {
11880
- if (tab === "invoice") setInvoices([]);
11881
- else setUsageLogs([]);
12274
+ setInvoices([]);
11882
12275
  } finally {
11883
12276
  setLoadingTable(false);
11884
12277
  }
11885
12278
  },
11886
- [api, workspaceId]
12279
+ [api, loadUsageLogsPage]
11887
12280
  );
11888
12281
  const handlePlanAction = useCallback8(
11889
12282
  async ({ productId, priceId, planName: planName2 }) => {
@@ -11990,6 +12383,10 @@ function BillingContent({
11990
12383
  setProducts([]);
11991
12384
  setTotalCredits(null);
11992
12385
  setCustomPlanCredits(null);
12386
+ usageLogsRequestRef.current++;
12387
+ dispatchUsageLogs({ type: "reset" });
12388
+ loadUsedByNamesRef.current?.reset();
12389
+ setUsedByNames({});
11993
12390
  }, [workspaceId]);
11994
12391
  useEffect18(() => {
11995
12392
  setInitialFetchTriggered(true);
@@ -12092,35 +12489,74 @@ function BillingContent({
12092
12489
  toast.success(`Opened ${successCount} invoice PDF(s).`);
12093
12490
  }
12094
12491
  } else {
12095
- const filtered = usageLogs.filter((log) => {
12096
- const d = new Date(log.createdAt);
12097
- return d >= startDate && d <= end;
12492
+ await exportUsageLogs(startDate, endDate);
12493
+ }
12494
+ };
12495
+ const closeExportModal = () => {
12496
+ exportAbortRef.current?.abort();
12497
+ exportAbortRef.current = null;
12498
+ setExportProgress(null);
12499
+ setExportNotice(null);
12500
+ setShowExportModal(false);
12501
+ };
12502
+ useEffect18(() => () => exportAbortRef.current?.abort(), []);
12503
+ const exportUsageLogs = async (startDate, endDate) => {
12504
+ if (!api.fetchUsageLogs) {
12505
+ toast.info?.("Usage logs are not available in this workspace.");
12506
+ closeExportModal();
12507
+ return;
12508
+ }
12509
+ exportAbortRef.current?.abort();
12510
+ const controller = new AbortController();
12511
+ exportAbortRef.current = controller;
12512
+ setExportNotice(null);
12513
+ setExportProgress({ fetched: 0, total: 0 });
12514
+ const range = usageLogsDateRange(startDate, endDate);
12515
+ try {
12516
+ const result = await fetchUsageLogsInRange(api.fetchUsageLogs, range, {
12517
+ signal: controller.signal,
12518
+ onProgress: (fetched, total) => {
12519
+ if (!controller.signal.aborted) setExportProgress({ fetched, total });
12520
+ }
12098
12521
  });
12099
- if (filtered.length === 0) {
12522
+ if (result.status === "cancelled" || controller.signal.aborted) return;
12523
+ if (result.status === "too-large") {
12524
+ setExportNotice(
12525
+ `${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.`
12526
+ );
12527
+ return;
12528
+ }
12529
+ if (result.status === "incomplete") {
12530
+ setExportNotice(
12531
+ "Usage logs kept changing while exporting, so some were missed. Try again."
12532
+ );
12533
+ return;
12534
+ }
12535
+ if (result.rows.length === 0) {
12100
12536
  toast.info?.("No usage logs found in the selected date range.");
12101
- setShowExportModal(false);
12537
+ closeExportModal();
12102
12538
  return;
12103
12539
  }
12104
- const headers = ["Date", "Type", "Credits", "Service", "User ID"];
12105
- const rows = filtered.map((log) => [
12106
- formatDate3(log.createdAt),
12107
- log.type,
12108
- log.amount.toLocaleString("en-US"),
12109
- log.service?.serviceGroupName ?? log.meta?.info ?? "",
12110
- log.triggeredByUserId != null ? userMap[log.triggeredByUserId] ?? log.triggeredByUserId : ""
12111
- ]);
12112
12540
  downloadCsv(
12113
- [headers, ...rows],
12114
- `usage_logs_${fmtFilename(startDate)}_${fmtFilename(end)}.csv`
12541
+ usageLogsCsvRows(result.rows, userMap, formatDate3),
12542
+ usageLogsExportFileName(startDate, endDate)
12115
12543
  );
12116
- toast.success(`Exported ${filtered.length} log(s).`);
12117
- setShowExportModal(false);
12544
+ toast.success(`Exported ${result.rows.length.toLocaleString("en-US")} log(s).`);
12545
+ closeExportModal();
12546
+ } catch (err) {
12547
+ if (controller.signal.aborted) return;
12548
+ setExportNotice(
12549
+ `Couldn't export usage logs. ${errorMessage(err, { method: "GET" })}`
12550
+ );
12551
+ } finally {
12552
+ if (exportAbortRef.current === controller) {
12553
+ exportAbortRef.current = null;
12554
+ setExportProgress(null);
12555
+ }
12118
12556
  }
12119
12557
  };
12120
12558
  function downloadCsv(rows, filename) {
12121
- const csv = rows.map(
12122
- (r) => r.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")
12123
- ).join("\n");
12559
+ const csv = toCsv(rows);
12124
12560
  const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
12125
12561
  const url = URL.createObjectURL(blob);
12126
12562
  const a = document.createElement("a");
@@ -12131,9 +12567,6 @@ function BillingContent({
12131
12567
  document.body.removeChild(a);
12132
12568
  URL.revokeObjectURL(url);
12133
12569
  }
12134
- function fmtFilename(d) {
12135
- return d.toISOString().slice(0, 10);
12136
- }
12137
12570
  async function handleBuyCredits(credits2, price) {
12138
12571
  if (!workspaceId) {
12139
12572
  toast.error("Missing workspace details for credit top-up");
@@ -12586,22 +13019,39 @@ function BillingContent({
12586
13019
  loading: loadingTable,
12587
13020
  workspaceCountry: workspace.current?.country
12588
13021
  }
12589
- ) : /* @__PURE__ */ jsx29(
12590
- UsageLogsTable,
12591
- {
12592
- logs: usageLogs,
12593
- userMap,
12594
- loading: loadingTable,
12595
- currentWorkspaceId: workspaceId,
12596
- usedByNames
12597
- }
12598
- ),
13022
+ ) : /* @__PURE__ */ jsxs26(Fragment11, { children: [
13023
+ /* @__PURE__ */ jsx29(
13024
+ UsageLogsTable,
13025
+ {
13026
+ logs: usageLogs.rows,
13027
+ userMap,
13028
+ loading: loadingUsageLogs,
13029
+ error: usageLogsError,
13030
+ onRetry: () => void loadUsageLogsPage(usageLogsState.requestedPage),
13031
+ currentWorkspaceId: workspaceId,
13032
+ usedByNames
13033
+ }
13034
+ ),
13035
+ !usageLogsError && /* @__PURE__ */ jsx29(
13036
+ UsageLogsPager,
13037
+ {
13038
+ page: usageLogs.page,
13039
+ totalPages: usageLogs.totalPages,
13040
+ total: usageLogs.total,
13041
+ loading: loadingUsageLogs,
13042
+ onPageChange: goToUsageLogsPage
13043
+ }
13044
+ )
13045
+ ] }),
12599
13046
  showExportModal && /* @__PURE__ */ jsx29(
12600
13047
  ExportDateModal,
12601
13048
  {
12602
13049
  title: activeTab === "invoice" ? "Export invoices" : "Export log",
12603
- onClose: () => setShowExportModal(false),
12604
- onExport: handleExport
13050
+ onClose: closeExportModal,
13051
+ onExport: handleExport,
13052
+ busy: exportProgress !== null,
13053
+ busyText: exportProgress && exportProgress.total > 0 ? `Fetched ${exportProgress.fetched.toLocaleString("en-US")} of ${exportProgress.total.toLocaleString("en-US")} logs\u2026` : "Fetching logs\u2026",
13054
+ notice: exportNotice
12605
13055
  }
12606
13056
  ),
12607
13057
  showBuyCreditsModal && /* @__PURE__ */ jsx29(
@@ -16972,7 +17422,9 @@ export {
16972
17422
  CreditPoolMemberModal,
16973
17423
  DEFAULT_PERMISSIONS,
16974
17424
  DEFAULT_USER_LANGUAGE,
17425
+ EMPTY_USAGE_LOGS_PAGE,
16975
17426
  ExportDateModal,
17427
+ INITIAL_USAGE_LOGS_STATE,
16976
17428
  InviteUpdateModal,
16977
17429
  MemberCreditLimitModal,
16978
17430
  MemberListFrame,
@@ -17005,7 +17457,12 @@ export {
17005
17457
  TeamsContent,
17006
17458
  TokensContent,
17007
17459
  URL_TO_TAB,
17460
+ USAGE_LOGS_EXPORT_BATCH_SIZE,
17461
+ USAGE_LOGS_EXPORT_EXTRA_REQUESTS,
17462
+ USAGE_LOGS_EXPORT_MAX_ROWS,
17463
+ USAGE_LOGS_PAGE_SIZE,
17008
17464
  USER_LANGUAGES,
17465
+ UsageLogsPager,
17009
17466
  WORKSPACE_LIST_PAGE_SIZE,
17010
17467
  WorkspaceGeneralTab,
17011
17468
  WorkspaceListTab,
@@ -17017,6 +17474,7 @@ export {
17017
17474
  cleanCreditLimitInput,
17018
17475
  cleanPoolValueInput,
17019
17476
  countLabel,
17477
+ createUsedByNamesLoader,
17020
17478
  creditCycleUnit,
17021
17479
  creditsMeter,
17022
17480
  deriveWorkspaceInvitePermission,
@@ -17026,6 +17484,7 @@ export {
17026
17484
  entitlementFromBody,
17027
17485
  entitlementFromFailure,
17028
17486
  expiryChoiceFor,
17487
+ fetchUsageLogsInRange,
17029
17488
  findBillingError,
17030
17489
  findPoolError,
17031
17490
  formatCreditLimit,
@@ -17060,8 +17519,16 @@ export {
17060
17519
  settingsReturnTarget,
17061
17520
  sharedPoolBannerText,
17062
17521
  sharedPoolEndsText,
17522
+ sliceUsageLogsPage,
17063
17523
  sponsoredCreditsText,
17524
+ toCsv,
17064
17525
  toLocalDateInput,
17526
+ usageLogsCsvRows,
17527
+ usageLogsDateRange,
17528
+ usageLogsExportFileName,
17529
+ usageLogsPage,
17530
+ usageLogsReducer,
17531
+ usageLogsSpentByOthers,
17065
17532
  useBillingEntitlement,
17066
17533
  useSettingsAdapters,
17067
17534
  useWorkspaceInvitePermission,