@carlonicora/nextjs-jsonapi 2.4.0 → 3.0.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/{BlockNoteEditor-7WGCH2KB.js → BlockNoteEditor-CDTU6DRN.js} +19 -19
- package/dist/{BlockNoteEditor-7WGCH2KB.js.map → BlockNoteEditor-CDTU6DRN.js.map} +1 -1
- package/dist/{BlockNoteEditor-GWAV5ESO.mjs → BlockNoteEditor-VVAY73V2.mjs} +4 -4
- package/dist/billing/index.d.mts +127 -46
- package/dist/billing/index.d.ts +127 -46
- package/dist/billing/index.js +1243 -1084
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +983 -824
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-V66AZWPG.js → chunk-53B6NPGA.js} +278 -8
- package/dist/chunk-53B6NPGA.js.map +1 -0
- package/dist/{chunk-M2EGUO2F.mjs → chunk-J54546YC.mjs} +1608 -1419
- package/dist/chunk-J54546YC.mjs.map +1 -0
- package/dist/{chunk-TOKHHZSP.js → chunk-JKGEJGSD.js} +7 -7
- package/dist/{chunk-TOKHHZSP.js.map → chunk-JKGEJGSD.js.map} +1 -1
- package/dist/{chunk-3LVSA7IM.mjs → chunk-PHEFWL6L.mjs} +2 -2
- package/dist/{chunk-LCCRUWWY.mjs → chunk-TWXKAY34.mjs} +273 -3
- package/dist/chunk-TWXKAY34.mjs.map +1 -0
- package/dist/{chunk-GYGMEDVS.js → chunk-UGNLKDI6.js} +1012 -823
- package/dist/chunk-UGNLKDI6.js.map +1 -0
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +16 -1
- package/dist/components/index.d.ts +16 -1
- package/dist/components/index.js +4 -4
- package/dist/components/index.mjs +3 -3
- package/dist/{tokenusage-admin.module-5wJ_tZTj.d.ts → config-BIjrg5wd.d.ts} +39 -1
- package/dist/{tokenusage-admin.module-DQVT4O4j.d.mts → config-BRUjtCd1.d.mts} +39 -1
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +22 -3
- package/dist/core/index.d.ts +22 -3
- package/dist/core/index.js +12 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +11 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.d.mts +413 -14
- package/dist/features/tokenusage/index.d.ts +413 -14
- package/dist/features/tokenusage/index.js +481 -120
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +431 -70
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +12 -2
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +4 -0
- package/src/core/index.ts +10 -0
- package/src/core/registry/ModuleRegistry.ts +4 -0
- package/src/features/administration/components/AdminIndexContainer.tsx +1 -1
- package/src/features/administration/components/__tests__/AdminIndexContainer.spec.tsx +1 -1
- package/src/features/billing/contexts/PriceContext.tsx +202 -0
- package/src/features/billing/contexts/ProductContext.tsx +185 -0
- package/src/features/billing/contexts/__tests__/PriceContext.spec.tsx +89 -0
- package/src/features/billing/contexts/__tests__/ProductContext.spec.tsx +81 -0
- package/src/features/billing/contexts/index.ts +2 -1
- package/src/features/billing/i18n-keys.ts +88 -0
- package/src/features/billing/stripe-price/components/containers/PriceContainer.tsx +21 -0
- package/src/features/billing/stripe-price/components/containers/index.ts +1 -0
- package/src/features/billing/stripe-price/components/details/PriceDetails.tsx +131 -0
- package/src/features/billing/stripe-price/components/details/index.ts +1 -0
- package/src/features/billing/stripe-price/components/forms/PriceArchiver.tsx +55 -0
- package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +307 -374
- package/src/features/billing/stripe-price/components/forms/index.ts +3 -1
- package/src/features/billing/stripe-price/components/index.ts +2 -0
- package/src/features/billing/stripe-price/components/lists/PricesList.tsx +49 -267
- package/src/features/billing/stripe-price/data/index.ts +1 -0
- package/src/features/billing/stripe-price/data/stripe-price.fields.ts +8 -0
- package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +119 -0
- package/src/features/billing/stripe-price/stripe-price.module.ts +5 -1
- package/src/features/billing/stripe-product/components/containers/ProductContainer.tsx +45 -0
- package/src/features/billing/stripe-product/components/containers/ProductsListContainer.tsx +27 -0
- package/src/features/billing/stripe-product/components/containers/index.ts +2 -2
- package/src/features/billing/stripe-product/components/details/ProductDetails.tsx +45 -0
- package/src/features/billing/stripe-product/components/details/index.ts +1 -0
- package/src/features/billing/stripe-product/components/forms/ProductArchiver.tsx +55 -0
- package/src/features/billing/stripe-product/components/forms/ProductEditor.tsx +92 -81
- package/src/features/billing/stripe-product/components/forms/index.ts +3 -1
- package/src/features/billing/stripe-product/components/index.ts +1 -0
- package/src/features/billing/stripe-product/components/lists/ProductsList.tsx +45 -187
- package/src/features/billing/stripe-product/data/index.ts +1 -0
- package/src/features/billing/stripe-product/data/stripe-product.fields.ts +6 -0
- package/src/features/billing/stripe-product/hooks/useStripeProductTableStructure.tsx +80 -0
- package/src/features/billing/stripe-product/stripe-product.module.ts +5 -1
- package/src/features/tokenusage/components/TokenUsageAdminContainer.tsx +20 -4
- package/src/features/tokenusage/components/TokenUsageAdminTiles.tsx +31 -18
- package/src/features/tokenusage/components/TokenUsageBreakdownTable.tsx +13 -13
- package/src/features/tokenusage/components/TokenUsageRankedBar.tsx +24 -7
- package/src/features/tokenusage/components/TokenUsageReportContainer.tsx +102 -0
- package/src/features/tokenusage/components/TokenUsageReportFilterBar.tsx +34 -0
- package/src/features/tokenusage/components/TokenUsageReportTiles.tsx +161 -0
- package/src/features/tokenusage/components/TokenUsageTimelineChart.tsx +15 -13
- package/src/features/tokenusage/components/__tests__/TokenUsageAdminTiles.spec.tsx +4 -2
- package/src/features/tokenusage/components/__tests__/TokenUsageRankedBar.spec.tsx +3 -1
- package/src/features/tokenusage/contexts/TokenUsageAdminContext.tsx +6 -3
- package/src/features/tokenusage/contexts/TokenUsageReportContext.tsx +199 -0
- package/src/features/tokenusage/data/TokenUsageReportService.ts +65 -0
- package/src/features/tokenusage/data/index.ts +9 -0
- package/src/features/tokenusage/data/tokenusage-report-breakdown.interface.ts +12 -0
- package/src/features/tokenusage/data/tokenusage-report-breakdown.ts +94 -0
- package/src/features/tokenusage/data/tokenusage-report-summary.interface.ts +12 -0
- package/src/features/tokenusage/data/tokenusage-report-summary.ts +87 -0
- package/src/features/tokenusage/data/tokenusage-report-timeline.interface.ts +13 -0
- package/src/features/tokenusage/data/tokenusage-report-timeline.ts +94 -0
- package/src/features/tokenusage/data/tokenusage-report.types.ts +51 -0
- package/src/features/tokenusage/i18n-keys.ts +28 -0
- package/src/features/tokenusage/index.ts +18 -0
- package/src/features/tokenusage/lib/config.ts +22 -0
- package/src/features/tokenusage/lib/formatters.ts +100 -0
- package/src/features/tokenusage/lib/metrics.ts +8 -26
- package/src/features/tokenusage/lib/palette.ts +41 -14
- package/src/features/tokenusage/tokenusage-admin.module.ts +4 -0
- package/src/features/tokenusage/tokenusage.modules.ts +40 -0
- package/src/shadcnui/ui/chart.tsx +30 -3
- package/dist/chunk-GYGMEDVS.js.map +0 -1
- package/dist/chunk-LCCRUWWY.mjs.map +0 -1
- package/dist/chunk-M2EGUO2F.mjs.map +0 -1
- package/dist/chunk-V66AZWPG.js.map +0 -1
- package/src/features/billing/contexts/BillingContext.tsx +0 -95
- package/src/features/billing/stripe-product/components/containers/ProductsAdminContainer.tsx +0 -98
- package/src/features/billing/stripe-product/components/containers/ProductsAdminPageContainer.tsx +0 -27
- /package/dist/{BlockNoteEditor-GWAV5ESO.mjs.map → BlockNoteEditor-VVAY73V2.mjs.map} +0 -0
- /package/dist/{chunk-3LVSA7IM.mjs.map → chunk-PHEFWL6L.mjs.map} +0 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../../../shadcnui";
|
|
15
15
|
import { TokenUsageAdminTimelineInterface } from "../data/tokenusage-admin-timeline.interface";
|
|
16
16
|
import { Metric, StackBy } from "../data/tokenusage-admin.types";
|
|
17
|
+
import { useUsageFormatters } from "../lib/formatters";
|
|
17
18
|
import { operationLabel } from "../lib/operation-label";
|
|
18
19
|
import { CATEGORICAL_CEILING, ChartMode, OTHER_COLOR, seriesColor } from "../lib/palette";
|
|
19
20
|
|
|
@@ -71,13 +72,6 @@ const inferGranularity = (buckets: string[]): "day" | "week" | "month" => {
|
|
|
71
72
|
return "day";
|
|
72
73
|
};
|
|
73
74
|
|
|
74
|
-
const bucketFormatter = (granularity: "day" | "week" | "month"): Intl.DateTimeFormat =>
|
|
75
|
-
granularity === "month"
|
|
76
|
-
? new Intl.DateTimeFormat("it-IT", { month: "short", year: "numeric", timeZone: "UTC" })
|
|
77
|
-
: new Intl.DateTimeFormat("it-IT", { day: "numeric", month: "short", timeZone: "UTC" });
|
|
78
|
-
|
|
79
|
-
const compactNumber = new Intl.NumberFormat("it-IT", { notation: "compact", maximumFractionDigits: 1 });
|
|
80
|
-
|
|
81
75
|
/**
|
|
82
76
|
* Usage over time as a stacked bar chart.
|
|
83
77
|
*
|
|
@@ -93,6 +87,7 @@ const compactNumber = new Intl.NumberFormat("it-IT", { notation: "compact", maxi
|
|
|
93
87
|
*/
|
|
94
88
|
export function TokenUsageTimelineChart({ rows, metric, stackBy, className }: TokenUsageTimelineChartProps) {
|
|
95
89
|
const t = useTranslations();
|
|
90
|
+
const { compact, bucketDate, metricValue: formatValue } = useUsageFormatters();
|
|
96
91
|
const { resolvedTheme } = useTheme();
|
|
97
92
|
const mode: ChartMode = resolvedTheme === "dark" ? "dark" : "light";
|
|
98
93
|
|
|
@@ -134,7 +129,7 @@ export function TokenUsageTimelineChart({ rows, metric, stackBy, className }: To
|
|
|
134
129
|
const seriesLabel = (series: string): string => {
|
|
135
130
|
if (series === OTHER_SERIES) {
|
|
136
131
|
const key = "token_usage.series.other";
|
|
137
|
-
return t.has(key) ? t(key) : "
|
|
132
|
+
return t.has(key) ? t(key) : "Other";
|
|
138
133
|
}
|
|
139
134
|
// Stacking by company makes the series a company NAME, which is data, not
|
|
140
135
|
// vocabulary — it is never looked up. Scope and type keys are vocabulary the
|
|
@@ -156,13 +151,12 @@ export function TokenUsageTimelineChart({ rows, metric, stackBy, className }: To
|
|
|
156
151
|
const key = "token_usage.timeline.empty";
|
|
157
152
|
return (
|
|
158
153
|
<p className={className ? `text-muted-foreground text-sm ${className}` : "text-muted-foreground text-sm"}>
|
|
159
|
-
{t.has(key) ? t(key) : "
|
|
154
|
+
{t.has(key) ? t(key) : "No data in the selected period"}
|
|
160
155
|
</p>
|
|
161
156
|
);
|
|
162
157
|
}
|
|
163
158
|
|
|
164
159
|
const granularity = inferGranularity(chartData.map((entry) => entry.bucket));
|
|
165
|
-
const formatBucket = bucketFormatter(granularity);
|
|
166
160
|
const seriesColorFor = (series: string, index: number) =>
|
|
167
161
|
series === OTHER_SERIES ? OTHER_COLOR : seriesColor(index, mode);
|
|
168
162
|
|
|
@@ -186,19 +180,27 @@ export function TokenUsageTimelineChart({ rows, metric, stackBy, className }: To
|
|
|
186
180
|
axisLine={false}
|
|
187
181
|
tickMargin={8}
|
|
188
182
|
minTickGap={16}
|
|
189
|
-
tickFormatter={(value: string) =>
|
|
183
|
+
tickFormatter={(value: string) => bucketDate(value, granularity)}
|
|
190
184
|
/>
|
|
191
185
|
<YAxis
|
|
192
186
|
tickLine={false}
|
|
193
187
|
axisLine={false}
|
|
194
188
|
tickMargin={8}
|
|
195
189
|
width={48}
|
|
196
|
-
tickFormatter={(value: number) =>
|
|
190
|
+
tickFormatter={(value: number) => compact(value)}
|
|
197
191
|
/>
|
|
198
192
|
<ChartTooltip
|
|
199
193
|
content={
|
|
200
194
|
<ChartTooltipContent
|
|
201
|
-
labelFormatter={(value) =>
|
|
195
|
+
labelFormatter={(value) => bucketDate(String(value), granularity)}
|
|
196
|
+
// Without this the tooltip falls back to toLocaleString() and
|
|
197
|
+
// prints the raw stored precision (credits carry 4 decimals),
|
|
198
|
+
// which disagrees with every other number on the page.
|
|
199
|
+
valueFormatter={(value) => formatValue(Number(value), metric)}
|
|
200
|
+
// Every series appears in every bucket's payload, so a day with
|
|
201
|
+
// one active operation would otherwise list the whole vocabulary
|
|
202
|
+
// as zeros and bury the one number that matters.
|
|
203
|
+
hideZeroValues
|
|
202
204
|
/>
|
|
203
205
|
}
|
|
204
206
|
/>
|
|
@@ -18,8 +18,10 @@ describe("TokenUsageAdminTiles", () => {
|
|
|
18
18
|
it("shows both cost centres with their deltas against the previous window", () => {
|
|
19
19
|
render(<TokenUsageAdminTiles summary={summary} metric="cost" singleCustomerMode={false} />);
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// The suite mocks useLocale() as "en", and formatting now honours the
|
|
22
|
+
// locale instead of a hard-coded it-IT, so the separator is a point.
|
|
23
|
+
expect(screen.getByTestId("tile-customer")).toHaveTextContent("9.46");
|
|
24
|
+
expect(screen.getByTestId("tile-platform")).toHaveTextContent("9.47");
|
|
23
25
|
// customer 9.46 vs 8.00 => +18%
|
|
24
26
|
expect(screen.getByTestId("tile-customer-delta")).toHaveTextContent("18");
|
|
25
27
|
// platform 9.47 vs 10.00 => -5%
|
|
@@ -15,7 +15,9 @@ describe("TokenUsageRankedBar", () => {
|
|
|
15
15
|
/>,
|
|
16
16
|
);
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// The suite mocks useLocale() as "en", and formatting now honours the
|
|
19
|
+
// locale instead of a hard-coded it-IT, so the separator is a point.
|
|
20
|
+
expect(screen.getByTestId("ranked-value-a")).toHaveTextContent("6.00");
|
|
19
21
|
expect(screen.getByTestId("ranked-share-a")).toHaveTextContent("60");
|
|
20
22
|
expect(screen.getByTestId("ranked-share-b")).toHaveTextContent("40");
|
|
21
23
|
});
|
|
@@ -13,8 +13,8 @@ import type { Granularity, Metric, StackBy } from "../data/tokenusage-admin.type
|
|
|
13
13
|
import { TokenUsageAdminService } from "../data/TokenUsageAdminService";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Default route the breadcrumb links back to. A host app that mounts the page
|
|
17
|
+
* elsewhere overrides it with the `pageUrl` prop — the constant stays here
|
|
18
18
|
* because the breadcrumb is built by this provider, not by the app.
|
|
19
19
|
*/
|
|
20
20
|
const TOKEN_USAGE_ADMIN_PAGE_URL = "/administration/token-usage";
|
|
@@ -72,6 +72,8 @@ type TokenUsageAdminProviderProps = {
|
|
|
72
72
|
initialTo?: string;
|
|
73
73
|
/** Rows per ranked panel. Defaults to 10. */
|
|
74
74
|
topN?: number;
|
|
75
|
+
/** Route the breadcrumb links back to. */
|
|
76
|
+
pageUrl?: string;
|
|
75
77
|
};
|
|
76
78
|
|
|
77
79
|
/**
|
|
@@ -89,6 +91,7 @@ export const TokenUsageAdminProvider = ({
|
|
|
89
91
|
initialFrom,
|
|
90
92
|
initialTo,
|
|
91
93
|
topN = DEFAULT_TOP_N,
|
|
94
|
+
pageUrl = TOKEN_USAGE_ADMIN_PAGE_URL,
|
|
92
95
|
}: TokenUsageAdminProviderProps) => {
|
|
93
96
|
const t = useTranslations();
|
|
94
97
|
const generateUrl = usePageUrlGenerator();
|
|
@@ -185,7 +188,7 @@ export const TokenUsageAdminProvider = ({
|
|
|
185
188
|
const breadcrumb = (): BreadcrumbItemData[] => [
|
|
186
189
|
{
|
|
187
190
|
name: t("token_usage.admin.title"),
|
|
188
|
-
href: generateUrl({ page:
|
|
191
|
+
href: generateUrl({ page: pageUrl }),
|
|
189
192
|
},
|
|
190
193
|
];
|
|
191
194
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { SharedProvider } from "../../../contexts";
|
|
6
|
+
import { usePageUrlGenerator } from "../../../hooks";
|
|
7
|
+
import { BreadcrumbItemData } from "../../../interfaces";
|
|
8
|
+
import { TokenUsageReportFilterBar } from "../components/TokenUsageReportFilterBar";
|
|
9
|
+
import type { TokenUsageReportBreakdownInterface } from "../data/tokenusage-report-breakdown.interface";
|
|
10
|
+
import type { TokenUsageReportSummaryInterface } from "../data/tokenusage-report-summary.interface";
|
|
11
|
+
import type { TokenUsageReportTimelineInterface } from "../data/tokenusage-report-timeline.interface";
|
|
12
|
+
import type { ReportMetric } from "../data/tokenusage-report.types";
|
|
13
|
+
import { TokenUsageReportService } from "../data/TokenUsageReportService";
|
|
14
|
+
|
|
15
|
+
/** Default page the breadcrumb links back to; overridable per host app. */
|
|
16
|
+
const TOKEN_USAGE_REPORT_PAGE_URL = "/tokenusage";
|
|
17
|
+
|
|
18
|
+
/** Rows kept per ranked panel before the backend folds the tail into "other". */
|
|
19
|
+
const DEFAULT_TOP_N = 10;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The unit this surface reports in, always. A game master is billed in credits,
|
|
23
|
+
* so credits are the only number that means anything to them: cost would leak
|
|
24
|
+
* platform margin and a raw token count is an implementation detail nobody is
|
|
25
|
+
* charged for. Pinned rather than exposed as a filter — there is no metric
|
|
26
|
+
* selector on the page.
|
|
27
|
+
*/
|
|
28
|
+
const REPORT_METRIC: ReportMetric = "credits";
|
|
29
|
+
|
|
30
|
+
export type TokenUsageReportFilterState = {
|
|
31
|
+
/** ISO 8601 instant. */
|
|
32
|
+
from: string;
|
|
33
|
+
/** ISO 8601 instant. */
|
|
34
|
+
to: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface TokenUsageReportContextType {
|
|
38
|
+
summary: TokenUsageReportSummaryInterface[];
|
|
39
|
+
timeline: TokenUsageReportTimelineInterface[];
|
|
40
|
+
byOperation: TokenUsageReportBreakdownInterface[];
|
|
41
|
+
/** Empty unless the host app declared a targetLabel. */
|
|
42
|
+
byTarget: TokenUsageReportBreakdownInterface[];
|
|
43
|
+
/** i18n key for the target panel's title; undefined when the host set no targetLabel. */
|
|
44
|
+
targetPanelTitleKey?: string;
|
|
45
|
+
filters: TokenUsageReportFilterState;
|
|
46
|
+
/** Merges a partial patch into the current filters; every key is optional. */
|
|
47
|
+
setFilters: (next: Partial<TokenUsageReportFilterState>) => void;
|
|
48
|
+
isLoading: boolean;
|
|
49
|
+
error: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const TokenUsageReportContext = createContext<TokenUsageReportContextType | undefined>(undefined);
|
|
53
|
+
|
|
54
|
+
/** Current calendar month to now, which is the window the page opens on. */
|
|
55
|
+
function defaultRange(): { from: string; to: string } {
|
|
56
|
+
const now = new Date();
|
|
57
|
+
const start = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
|
|
58
|
+
return { from: start.toISOString(), to: now.toISOString() };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type TokenUsageReportProviderProps = {
|
|
62
|
+
children: ReactNode;
|
|
63
|
+
/**
|
|
64
|
+
* The Neo4j label the "by target" panel groups by, e.g. "Campaign". The set of
|
|
65
|
+
* things usage can be attributed to is application-specific, so the package
|
|
66
|
+
* cannot pick one — omit it and the panel is skipped rather than rendered
|
|
67
|
+
* empty, and no request is issued.
|
|
68
|
+
*/
|
|
69
|
+
targetLabel?: string;
|
|
70
|
+
/** i18n key for the target panel's title. Required when targetLabel is set. */
|
|
71
|
+
targetPanelTitleKey?: string;
|
|
72
|
+
/** ISO 8601 instant. Defaults to the start of the current month. */
|
|
73
|
+
initialFrom?: string;
|
|
74
|
+
/** ISO 8601 instant. Defaults to now. */
|
|
75
|
+
initialTo?: string;
|
|
76
|
+
/** Rows per ranked panel. Defaults to 10. */
|
|
77
|
+
topN?: number;
|
|
78
|
+
/** Route the breadcrumb links back to. */
|
|
79
|
+
pageUrl?: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Owns the date range of the self-service token-usage page, fetches the three
|
|
84
|
+
* panels behind it, and publishes the filter bar into the page title bar.
|
|
85
|
+
*
|
|
86
|
+
* The filter bar is rendered into `title.functions` here — NOT in the container —
|
|
87
|
+
* because `RoundPageContainer`'s title bar reads `title.functions` from
|
|
88
|
+
* `SharedContext`, and a descendant cannot inject nodes into an ancestor's
|
|
89
|
+
* provider value. That is why the filter state lives at this level.
|
|
90
|
+
*/
|
|
91
|
+
export const TokenUsageReportProvider = ({
|
|
92
|
+
children,
|
|
93
|
+
targetLabel,
|
|
94
|
+
targetPanelTitleKey,
|
|
95
|
+
initialFrom,
|
|
96
|
+
initialTo,
|
|
97
|
+
topN = DEFAULT_TOP_N,
|
|
98
|
+
pageUrl = TOKEN_USAGE_REPORT_PAGE_URL,
|
|
99
|
+
}: TokenUsageReportProviderProps) => {
|
|
100
|
+
const t = useTranslations();
|
|
101
|
+
const generateUrl = usePageUrlGenerator();
|
|
102
|
+
|
|
103
|
+
const [filters, setFilterState] = useState<TokenUsageReportFilterState>(() => {
|
|
104
|
+
const range = defaultRange();
|
|
105
|
+
return { from: initialFrom ?? range.from, to: initialTo ?? range.to };
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const [summary, setSummary] = useState<TokenUsageReportSummaryInterface[]>([]);
|
|
109
|
+
const [timeline, setTimeline] = useState<TokenUsageReportTimelineInterface[]>([]);
|
|
110
|
+
const [byOperation, setByOperation] = useState<TokenUsageReportBreakdownInterface[]>([]);
|
|
111
|
+
const [byTarget, setByTarget] = useState<TokenUsageReportBreakdownInterface[]>([]);
|
|
112
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
113
|
+
const [error, setError] = useState<string | null>(null);
|
|
114
|
+
|
|
115
|
+
const { from, to } = filters;
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
// `cancelled` is the out-of-order guard: a filter change fires a new request
|
|
119
|
+
// while the previous one is still in flight, and without this flag the slower
|
|
120
|
+
// (older) response would land last and overwrite the fresher state.
|
|
121
|
+
let cancelled = false;
|
|
122
|
+
|
|
123
|
+
setIsLoading(true);
|
|
124
|
+
setError(null);
|
|
125
|
+
|
|
126
|
+
const base = { from, to };
|
|
127
|
+
|
|
128
|
+
Promise.all([
|
|
129
|
+
TokenUsageReportService.getSummary(base),
|
|
130
|
+
// granularity is pinned to "day": week/month bucketing buys nothing on the
|
|
131
|
+
// ranges a single tenant browses.
|
|
132
|
+
TokenUsageReportService.getTimeline({ ...base, granularity: "day" }),
|
|
133
|
+
TokenUsageReportService.getBreakdown({ ...base, dimension: "operation", metric: REPORT_METRIC, limit: topN }),
|
|
134
|
+
// No targetLabel means the host app never opted in, so the request is
|
|
135
|
+
// skipped entirely rather than issued and discarded.
|
|
136
|
+
targetLabel
|
|
137
|
+
? TokenUsageReportService.getBreakdown({
|
|
138
|
+
...base,
|
|
139
|
+
dimension: "target",
|
|
140
|
+
targetLabel,
|
|
141
|
+
metric: REPORT_METRIC,
|
|
142
|
+
limit: topN,
|
|
143
|
+
})
|
|
144
|
+
: Promise.resolve<TokenUsageReportBreakdownInterface[]>([]),
|
|
145
|
+
])
|
|
146
|
+
.then(([nextSummary, nextTimeline, nextByOperation, nextByTarget]) => {
|
|
147
|
+
if (cancelled) return;
|
|
148
|
+
setSummary(nextSummary ?? []);
|
|
149
|
+
setTimeline(nextTimeline ?? []);
|
|
150
|
+
setByOperation(nextByOperation ?? []);
|
|
151
|
+
setByTarget(nextByTarget ?? []);
|
|
152
|
+
})
|
|
153
|
+
.catch((err) => {
|
|
154
|
+
if (cancelled) return;
|
|
155
|
+
console.error("Failed to load token usage:", err);
|
|
156
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
157
|
+
})
|
|
158
|
+
.finally(() => {
|
|
159
|
+
if (cancelled) return;
|
|
160
|
+
setIsLoading(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return () => {
|
|
164
|
+
cancelled = true;
|
|
165
|
+
};
|
|
166
|
+
}, [from, to, targetLabel, topN]);
|
|
167
|
+
|
|
168
|
+
const setFilters = useCallback((next: Partial<TokenUsageReportFilterState>) => {
|
|
169
|
+
setFilterState((prev) => ({ ...prev, ...next }));
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
const breadcrumb = (): BreadcrumbItemData[] => [
|
|
173
|
+
{ name: t("token_usage.report.title"), href: generateUrl({ page: pageUrl }) },
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const title = () => ({
|
|
177
|
+
type: t("token_usage.report.title"),
|
|
178
|
+
functions: <TokenUsageReportFilterBar key="tokenUsageReportFilterBar" onChange={setFilters} />,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const contextValue = useMemo<TokenUsageReportContextType>(
|
|
182
|
+
() => ({ summary, timeline, byOperation, byTarget, targetPanelTitleKey, filters, setFilters, isLoading, error }),
|
|
183
|
+
[summary, timeline, byOperation, byTarget, targetPanelTitleKey, filters, setFilters, isLoading, error],
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<SharedProvider value={{ breadcrumbs: breadcrumb(), title: title() }}>
|
|
188
|
+
<TokenUsageReportContext.Provider value={contextValue}>{children}</TokenUsageReportContext.Provider>
|
|
189
|
+
</SharedProvider>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const useTokenUsageReport = (): TokenUsageReportContextType => {
|
|
194
|
+
const ctx = useContext(TokenUsageReportContext);
|
|
195
|
+
if (!ctx) {
|
|
196
|
+
throw new Error("useTokenUsageReport() called outside <TokenUsageReportProvider>.");
|
|
197
|
+
}
|
|
198
|
+
return ctx;
|
|
199
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
|
|
2
|
+
import { TokenUsageReportBreakdownInterface } from "./tokenusage-report-breakdown.interface";
|
|
3
|
+
import { TokenUsageReportSummaryInterface } from "./tokenusage-report-summary.interface";
|
|
4
|
+
import { TokenUsageReportTimelineInterface } from "./tokenusage-report-timeline.interface";
|
|
5
|
+
import { ReportDimension, ReportMetric, TokenUsageReportFilters } from "./tokenusage-report.types";
|
|
6
|
+
|
|
7
|
+
function withFilters(endpoint: EndpointCreator, filters: TokenUsageReportFilters): EndpointCreator {
|
|
8
|
+
endpoint.addAdditionalParam("from", filters.from);
|
|
9
|
+
endpoint.addAdditionalParam("to", filters.to);
|
|
10
|
+
return endpoint;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The self-service token-usage endpoints.
|
|
15
|
+
*
|
|
16
|
+
* No companyId parameter anywhere: the backend scopes every query to the
|
|
17
|
+
* caller's own company through the CLS preamble, so there is nothing for the
|
|
18
|
+
* client to name. No `metric: "cost"` either — the controller rejects it.
|
|
19
|
+
*/
|
|
20
|
+
export class TokenUsageReportService extends AbstractService {
|
|
21
|
+
/** Two rows: window "current" and "previous". */
|
|
22
|
+
static async getSummary(filters: TokenUsageReportFilters): Promise<TokenUsageReportSummaryInterface[]> {
|
|
23
|
+
const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.TokenUsageReportSummary }), filters);
|
|
24
|
+
|
|
25
|
+
return this.callApi<TokenUsageReportSummaryInterface[]>({
|
|
26
|
+
type: Modules.TokenUsageReportSummary,
|
|
27
|
+
method: HttpMethod.GET,
|
|
28
|
+
endpoint: endpoint.generate(),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static async getTimeline(
|
|
33
|
+
params: TokenUsageReportFilters & { granularity: "day" },
|
|
34
|
+
): Promise<TokenUsageReportTimelineInterface[]> {
|
|
35
|
+
const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.TokenUsageReportTimeline }), params);
|
|
36
|
+
endpoint.addAdditionalParam("granularity", params.granularity);
|
|
37
|
+
|
|
38
|
+
return this.callApi<TokenUsageReportTimelineInterface[]>({
|
|
39
|
+
type: Modules.TokenUsageReportTimeline,
|
|
40
|
+
method: HttpMethod.GET,
|
|
41
|
+
endpoint: endpoint.generate(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static async getBreakdown(
|
|
46
|
+
params: TokenUsageReportFilters & {
|
|
47
|
+
dimension: ReportDimension;
|
|
48
|
+
targetLabel?: string;
|
|
49
|
+
metric: ReportMetric;
|
|
50
|
+
limit?: number;
|
|
51
|
+
},
|
|
52
|
+
): Promise<TokenUsageReportBreakdownInterface[]> {
|
|
53
|
+
const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.TokenUsageReportBreakdown }), params);
|
|
54
|
+
endpoint.addAdditionalParam("dimension", params.dimension);
|
|
55
|
+
if (params.targetLabel) endpoint.addAdditionalParam("targetLabel", params.targetLabel);
|
|
56
|
+
endpoint.addAdditionalParam("metric", params.metric);
|
|
57
|
+
if (params.limit !== undefined) endpoint.addAdditionalParam("limit", String(params.limit));
|
|
58
|
+
|
|
59
|
+
return this.callApi<TokenUsageReportBreakdownInterface[]>({
|
|
60
|
+
type: Modules.TokenUsageReportBreakdown,
|
|
61
|
+
method: HttpMethod.GET,
|
|
62
|
+
endpoint: endpoint.generate(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -6,3 +6,12 @@ export type { TokenUsageAdminTimelineInterface } from "./tokenusage-admin-timeli
|
|
|
6
6
|
export { TokenUsageAdminBreakdown } from "./tokenusage-admin-breakdown";
|
|
7
7
|
export type { TokenUsageAdminBreakdownInterface } from "./tokenusage-admin-breakdown.interface";
|
|
8
8
|
export { TokenUsageAdminService } from "./TokenUsageAdminService";
|
|
9
|
+
|
|
10
|
+
export * from "./tokenusage-report.types";
|
|
11
|
+
export { TokenUsageReportSummary } from "./tokenusage-report-summary";
|
|
12
|
+
export type { TokenUsageReportSummaryInterface } from "./tokenusage-report-summary.interface";
|
|
13
|
+
export { TokenUsageReportTimeline } from "./tokenusage-report-timeline";
|
|
14
|
+
export type { TokenUsageReportTimelineInterface } from "./tokenusage-report-timeline.interface";
|
|
15
|
+
export { TokenUsageReportBreakdown } from "./tokenusage-report-breakdown";
|
|
16
|
+
export type { TokenUsageReportBreakdownInterface } from "./tokenusage-report-breakdown.interface";
|
|
17
|
+
export { TokenUsageReportService } from "./TokenUsageReportService";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
export interface TokenUsageReportBreakdownInterface extends ApiDataInterface {
|
|
4
|
+
get label(): string;
|
|
5
|
+
get sublabel(): string | undefined;
|
|
6
|
+
get cost(): number;
|
|
7
|
+
get credits(): number;
|
|
8
|
+
get tokensIn(): number;
|
|
9
|
+
get tokensOut(): number;
|
|
10
|
+
get cached(): number;
|
|
11
|
+
get calls(): number;
|
|
12
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { TokenUsageReportBreakdownInterface } from "./tokenusage-report-breakdown.interface";
|
|
3
|
+
import { TokenUsageReportBreakdownInput } from "./tokenusage-report.types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One ranked row of a self-service breakdown. The same shape serves both
|
|
7
|
+
* dimensions — operation and target — which is why the two "by-X" panels
|
|
8
|
+
* collapse into a single `breakdown?dimension=` call.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately narrower than the administrative row: no `activeUsers`,
|
|
11
|
+
* `monthlyCredits` or `availableMonthlyCredits`, which are company-fleet
|
|
12
|
+
* figures a single company has no business seeing.
|
|
13
|
+
*/
|
|
14
|
+
export class TokenUsageReportBreakdown extends AbstractApiData implements TokenUsageReportBreakdownInterface {
|
|
15
|
+
private _label: string = "";
|
|
16
|
+
private _sublabel?: string;
|
|
17
|
+
private _cost: number = 0;
|
|
18
|
+
private _credits: number = 0;
|
|
19
|
+
private _tokensIn: number = 0;
|
|
20
|
+
private _tokensOut: number = 0;
|
|
21
|
+
private _cached: number = 0;
|
|
22
|
+
private _calls: number = 0;
|
|
23
|
+
|
|
24
|
+
get label(): string {
|
|
25
|
+
return this._label;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get sublabel(): string | undefined {
|
|
29
|
+
return this._sublabel;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get cost(): number {
|
|
33
|
+
return this._cost;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get credits(): number {
|
|
37
|
+
return this._credits;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get tokensIn(): number {
|
|
41
|
+
return this._tokensIn;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get tokensOut(): number {
|
|
45
|
+
return this._tokensOut;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get cached(): number {
|
|
49
|
+
return this._cached;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get calls(): number {
|
|
53
|
+
return this._calls;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
57
|
+
super.rehydrate(data);
|
|
58
|
+
|
|
59
|
+
const attrs = data.jsonApi.attributes;
|
|
60
|
+
this._label = attrs.label ?? "";
|
|
61
|
+
this._sublabel = attrs.sublabel ?? undefined;
|
|
62
|
+
this._cost = attrs.cost ?? 0;
|
|
63
|
+
this._credits = attrs.credits ?? 0;
|
|
64
|
+
this._tokensIn = attrs.tokensIn ?? 0;
|
|
65
|
+
this._tokensOut = attrs.tokensOut ?? 0;
|
|
66
|
+
this._cached = attrs.cached ?? 0;
|
|
67
|
+
this._calls = attrs.calls ?? 0;
|
|
68
|
+
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
createJsonApi(data: TokenUsageReportBreakdownInput) {
|
|
73
|
+
const response: any = {
|
|
74
|
+
data: {
|
|
75
|
+
type: Modules.TokenUsageReportBreakdown.name,
|
|
76
|
+
id: data.id,
|
|
77
|
+
attributes: {},
|
|
78
|
+
relationships: {},
|
|
79
|
+
},
|
|
80
|
+
included: [],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (data.label !== undefined) response.data.attributes.label = data.label;
|
|
84
|
+
if (data.sublabel !== undefined) response.data.attributes.sublabel = data.sublabel;
|
|
85
|
+
if (data.cost !== undefined) response.data.attributes.cost = data.cost;
|
|
86
|
+
if (data.credits !== undefined) response.data.attributes.credits = data.credits;
|
|
87
|
+
if (data.tokensIn !== undefined) response.data.attributes.tokensIn = data.tokensIn;
|
|
88
|
+
if (data.tokensOut !== undefined) response.data.attributes.tokensOut = data.tokensOut;
|
|
89
|
+
if (data.cached !== undefined) response.data.attributes.cached = data.cached;
|
|
90
|
+
if (data.calls !== undefined) response.data.attributes.calls = data.calls;
|
|
91
|
+
|
|
92
|
+
return response;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
export interface TokenUsageReportSummaryInterface extends ApiDataInterface {
|
|
4
|
+
/** "current" or "previous" — the equal-length preceding span. */
|
|
5
|
+
get window(): string;
|
|
6
|
+
get cost(): number;
|
|
7
|
+
get credits(): number;
|
|
8
|
+
get tokensIn(): number;
|
|
9
|
+
get tokensOut(): number;
|
|
10
|
+
get cached(): number;
|
|
11
|
+
get calls(): number;
|
|
12
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { TokenUsageReportSummaryInterface } from "./tokenusage-report-summary.interface";
|
|
3
|
+
import { TokenUsageReportSummaryInput } from "./tokenusage-report.types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One row of the self-service summary: the caller's own company observed over
|
|
7
|
+
* one time window. Two rows are returned per request — "current" and the
|
|
8
|
+
* equal-length "previous" span — so the KPI tiles can render a value and its
|
|
9
|
+
* delta without a second call.
|
|
10
|
+
*
|
|
11
|
+
* There is no `scope` here: a company only ever sees itself, so the
|
|
12
|
+
* customer/platform split that the administrative summary carries is absent.
|
|
13
|
+
*/
|
|
14
|
+
export class TokenUsageReportSummary extends AbstractApiData implements TokenUsageReportSummaryInterface {
|
|
15
|
+
private _window: string = "";
|
|
16
|
+
private _cost: number = 0;
|
|
17
|
+
private _credits: number = 0;
|
|
18
|
+
private _tokensIn: number = 0;
|
|
19
|
+
private _tokensOut: number = 0;
|
|
20
|
+
private _cached: number = 0;
|
|
21
|
+
private _calls: number = 0;
|
|
22
|
+
|
|
23
|
+
get window(): string {
|
|
24
|
+
return this._window;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get cost(): number {
|
|
28
|
+
return this._cost;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get credits(): number {
|
|
32
|
+
return this._credits;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get tokensIn(): number {
|
|
36
|
+
return this._tokensIn;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get tokensOut(): number {
|
|
40
|
+
return this._tokensOut;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get cached(): number {
|
|
44
|
+
return this._cached;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get calls(): number {
|
|
48
|
+
return this._calls;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
52
|
+
super.rehydrate(data);
|
|
53
|
+
|
|
54
|
+
const attrs = data.jsonApi.attributes;
|
|
55
|
+
this._window = attrs.window ?? "";
|
|
56
|
+
this._cost = attrs.cost ?? 0;
|
|
57
|
+
this._credits = attrs.credits ?? 0;
|
|
58
|
+
this._tokensIn = attrs.tokensIn ?? 0;
|
|
59
|
+
this._tokensOut = attrs.tokensOut ?? 0;
|
|
60
|
+
this._cached = attrs.cached ?? 0;
|
|
61
|
+
this._calls = attrs.calls ?? 0;
|
|
62
|
+
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
createJsonApi(data: TokenUsageReportSummaryInput) {
|
|
67
|
+
const response: any = {
|
|
68
|
+
data: {
|
|
69
|
+
type: Modules.TokenUsageReportSummary.name,
|
|
70
|
+
id: data.id,
|
|
71
|
+
attributes: {},
|
|
72
|
+
relationships: {},
|
|
73
|
+
},
|
|
74
|
+
included: [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (data.window !== undefined) response.data.attributes.window = data.window;
|
|
78
|
+
if (data.cost !== undefined) response.data.attributes.cost = data.cost;
|
|
79
|
+
if (data.credits !== undefined) response.data.attributes.credits = data.credits;
|
|
80
|
+
if (data.tokensIn !== undefined) response.data.attributes.tokensIn = data.tokensIn;
|
|
81
|
+
if (data.tokensOut !== undefined) response.data.attributes.tokensOut = data.tokensOut;
|
|
82
|
+
if (data.cached !== undefined) response.data.attributes.cached = data.cached;
|
|
83
|
+
if (data.calls !== undefined) response.data.attributes.calls = data.calls;
|
|
84
|
+
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
export interface TokenUsageReportTimelineInterface extends ApiDataInterface {
|
|
4
|
+
/** Backend declares this `type: "date"`, so it is a Date in memory. */
|
|
5
|
+
get bucket(): Date;
|
|
6
|
+
get series(): string;
|
|
7
|
+
get cost(): number;
|
|
8
|
+
get credits(): number;
|
|
9
|
+
get tokensIn(): number;
|
|
10
|
+
get tokensOut(): number;
|
|
11
|
+
get cached(): number;
|
|
12
|
+
get calls(): number;
|
|
13
|
+
}
|