@carlonicora/nextjs-jsonapi 1.141.3 → 1.143.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/AbstractService-B0T7h8fX.d.mts +124 -0
- package/dist/AbstractService-Cew0PD0L.d.ts +124 -0
- package/dist/{AssistantMessageInterface-DH1QwtC_.d.mts → AssistantMessageInterface-Ce8FcllX.d.mts} +1 -1
- package/dist/{AssistantMessageInterface-giWvsOrX.d.ts → AssistantMessageInterface-Dh0BpbP2.d.ts} +1 -1
- package/dist/{AuthComponent-DdxCFgUZ.d.mts → AuthComponent-Cik96ElA.d.mts} +1 -1
- package/dist/{AuthComponent-Cd7lcYif.d.ts → AuthComponent-DTTKu3VK.d.ts} +1 -1
- package/dist/{BlockNoteEditor-JTKEBS2W.mjs → BlockNoteEditor-34QM4UR5.mjs} +4 -4
- package/dist/{BlockNoteEditor-4OXY6BKF.js → BlockNoteEditor-LMKWUOJG.js} +19 -19
- package/dist/{BlockNoteEditor-4OXY6BKF.js.map → BlockNoteEditor-LMKWUOJG.js.map} +1 -1
- package/dist/{auth.interface-yivTb46w.d.ts → auth.interface-COmBjh89.d.ts} +1 -1
- package/dist/{auth.interface-r3XKYOrQ.d.mts → auth.interface-CkmZqat3.d.mts} +1 -1
- package/dist/billing/index.js +357 -357
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-5GVY2M5X.mjs → chunk-BIL2WQEV.mjs} +264 -18
- package/dist/chunk-BIL2WQEV.mjs.map +1 -0
- package/dist/{chunk-AGKZCRG2.js → chunk-DS4IBFWA.js} +269 -23
- package/dist/chunk-DS4IBFWA.js.map +1 -0
- package/dist/{chunk-4TGGHNWL.js → chunk-F6PZZ4KI.js} +752 -752
- package/dist/chunk-F6PZZ4KI.js.map +1 -0
- package/dist/{chunk-KXTTYXJ6.mjs → chunk-HN5ED7OF.mjs} +2 -2
- package/dist/{chunk-K76QSQTC.mjs → chunk-UGSOBBZH.mjs} +28 -28
- package/dist/chunk-UGSOBBZH.mjs.map +1 -0
- package/dist/{chunk-SE2YSAER.js → chunk-UXJAS6C5.js} +7 -7
- package/dist/{chunk-SE2YSAER.js.map → chunk-UXJAS6C5.js.map} +1 -1
- package/dist/client/index.d.mts +5 -5
- package/dist/client/index.d.ts +5 -5
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +6 -6
- package/dist/components/index.d.ts +6 -6
- package/dist/components/index.js +4 -4
- package/dist/components/index.mjs +3 -3
- package/dist/{config-BrkU_Ynz.d.ts → config-Cd5xTEfZ.d.ts} +1 -1
- package/dist/{config-snd2ToEB.d.mts → config-YyhFHxkU.d.mts} +1 -1
- package/dist/contexts/index.d.mts +4 -4
- package/dist/contexts/index.d.ts +4 -4
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +24 -60
- package/dist/core/index.d.ts +24 -60
- package/dist/core/index.js +8 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +7 -1
- package/dist/features/help/index.d.mts +2 -2
- package/dist/features/help/index.d.ts +2 -2
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.css +29 -0
- package/dist/features/tokenusage/index.css.map +1 -0
- package/dist/features/tokenusage/index.d.mts +388 -0
- package/dist/features/tokenusage/index.d.ts +388 -0
- package/dist/features/tokenusage/index.js +1012 -0
- package/dist/features/tokenusage/index.js.map +1 -0
- package/dist/features/tokenusage/index.mjs +1012 -0
- package/dist/features/tokenusage/index.mjs.map +1 -0
- package/dist/index.d.mts +11 -9
- package/dist/index.d.ts +11 -9
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8 -2
- package/dist/{notification.interface-LKFmZndR.d.ts → notification.interface--27exGde.d.ts} +6 -6
- package/dist/{notification.interface-B3kNUTZs.d.mts → notification.interface-CG1vqoxg.d.mts} +6 -6
- package/dist/{s3.service-BrmeMjtk.d.ts → s3.service-CpwA3sl_.d.ts} +4 -125
- package/dist/{s3.service-CXKV_OT-.d.mts → s3.service-Dt6MfuHr.d.mts} +4 -125
- package/dist/server/index.d.mts +5 -4
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/tokenusage-admin.module-5wJ_tZTj.d.ts +61 -0
- package/dist/tokenusage-admin.module-DQVT4O4j.d.mts +61 -0
- package/dist/{types-CQSjy7et.d.mts → types-Bq_UA8Lg.d.mts} +1 -1
- package/dist/{types-DHOxe8rc.d.ts → types-bsm8Fp55.d.ts} +1 -1
- package/dist/{usePageUrlGenerator-tjq2mlDV.d.ts → usePageUrlGenerator-J_ECv-oQ.d.ts} +1 -1
- package/dist/{usePageUrlGenerator-uOnyJ6j2.d.mts → usePageUrlGenerator-uKBjAKqC.d.mts} +1 -1
- package/dist/{useSocket-BsbaGPqE.d.ts → useSocket-DgGAQW6h.d.ts} +1 -1
- package/dist/{useSocket-BSjn1Rlb.d.mts → useSocket-M3YY0aTS.d.mts} +1 -1
- package/package.json +6 -1
- package/src/core/index.ts +5 -0
- package/src/core/registry/ModuleRegistry.ts +4 -0
- package/src/features/company/components/details/TokenStatusIndicator.tsx +9 -9
- package/src/features/company/data/company.interface.ts +6 -6
- package/src/features/company/data/company.ts +17 -17
- package/src/features/tokenusage/components/TokenUsageAdminContainer.tsx +189 -0
- package/src/features/tokenusage/components/TokenUsageAdminFilterBar.tsx +138 -0
- package/src/features/tokenusage/components/TokenUsageAdminTiles.tsx +177 -0
- package/src/features/tokenusage/components/TokenUsageBreakdownTable.tsx +160 -0
- package/src/features/tokenusage/components/TokenUsageRankedBar.tsx +99 -0
- package/src/features/tokenusage/components/TokenUsageTimelineChart.tsx +225 -0
- package/src/features/tokenusage/components/__tests__/TokenUsageAdminTiles.spec.tsx +42 -0
- package/src/features/tokenusage/components/__tests__/TokenUsageRankedBar.spec.tsx +41 -0
- package/src/features/tokenusage/components/__tests__/TokenUsageTimelineChart.spec.tsx +47 -0
- package/src/features/tokenusage/contexts/TokenUsageAdminContext.tsx +252 -0
- package/src/features/tokenusage/contexts/__tests__/TokenUsageAdminContext.spec.tsx +80 -0
- package/src/features/tokenusage/data/TokenUsageAdminService.ts +54 -0
- package/src/features/tokenusage/data/__tests__/tokenusage-admin-models.spec.ts +147 -0
- package/src/features/tokenusage/data/index.ts +8 -0
- package/src/features/tokenusage/data/tokenusage-admin-breakdown.interface.ts +18 -0
- package/src/features/tokenusage/data/tokenusage-admin-breakdown.ts +115 -0
- package/src/features/tokenusage/data/tokenusage-admin-summary.interface.ts +14 -0
- package/src/features/tokenusage/data/tokenusage-admin-summary.ts +90 -0
- package/src/features/tokenusage/data/tokenusage-admin-timeline.interface.ts +13 -0
- package/src/features/tokenusage/data/tokenusage-admin-timeline.ts +94 -0
- package/src/features/tokenusage/data/tokenusage-admin.types.ts +56 -0
- package/src/features/tokenusage/i18n-keys.ts +65 -0
- package/src/features/tokenusage/index.ts +30 -0
- package/src/features/tokenusage/lib/metrics.ts +69 -0
- package/src/features/tokenusage/lib/operation-label.ts +41 -0
- package/src/features/tokenusage/lib/palette.ts +197 -0
- package/src/features/tokenusage/tokenusage-admin.module.ts +22 -0
- package/src/features/user/contexts/CurrentUserContext.tsx +18 -18
- package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +21 -21
- package/dist/chunk-4TGGHNWL.js.map +0 -1
- package/dist/chunk-5GVY2M5X.mjs.map +0 -1
- package/dist/chunk-AGKZCRG2.js.map +0 -1
- package/dist/chunk-K76QSQTC.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-JTKEBS2W.mjs.map → BlockNoteEditor-34QM4UR5.mjs.map} +0 -0
- /package/dist/{chunk-KXTTYXJ6.mjs.map → chunk-HN5ED7OF.mjs.map} +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
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 { TokenUsageAdminFilterBar } from "../components/TokenUsageAdminFilterBar";
|
|
9
|
+
import type { TokenUsageAdminBreakdownInterface } from "../data/tokenusage-admin-breakdown.interface";
|
|
10
|
+
import type { TokenUsageAdminSummaryInterface } from "../data/tokenusage-admin-summary.interface";
|
|
11
|
+
import type { TokenUsageAdminTimelineInterface } from "../data/tokenusage-admin-timeline.interface";
|
|
12
|
+
import type { Granularity, Metric, StackBy } from "../data/tokenusage-admin.types";
|
|
13
|
+
import { TokenUsageAdminService } from "../data/TokenUsageAdminService";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Route the breadcrumb links back to. The page itself lives in the consuming
|
|
17
|
+
* app (a360ai mounts it at `/administration/token-usage`); the constant is here
|
|
18
|
+
* because the breadcrumb is built by this provider, not by the app.
|
|
19
|
+
*/
|
|
20
|
+
const TOKEN_USAGE_ADMIN_PAGE_URL = "/administration/token-usage";
|
|
21
|
+
|
|
22
|
+
/** Rows kept per ranked panel before the repository folds the tail into "other". */
|
|
23
|
+
const DEFAULT_TOP_N = 10;
|
|
24
|
+
|
|
25
|
+
export type TokenUsageAdminFilterState = {
|
|
26
|
+
/** ISO 8601 instant. */
|
|
27
|
+
from: string;
|
|
28
|
+
/** ISO 8601 instant. */
|
|
29
|
+
to: string;
|
|
30
|
+
granularity: Granularity;
|
|
31
|
+
stackBy: StackBy;
|
|
32
|
+
companyId?: string;
|
|
33
|
+
metric: Metric;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface TokenUsageAdminContextType {
|
|
37
|
+
summary: TokenUsageAdminSummaryInterface[];
|
|
38
|
+
timeline: TokenUsageAdminTimelineInterface[];
|
|
39
|
+
byCompany: TokenUsageAdminBreakdownInterface[];
|
|
40
|
+
byUser: TokenUsageAdminBreakdownInterface[];
|
|
41
|
+
/** Platform-side spend split by operation. Always empty in single-customer mode. */
|
|
42
|
+
/** Platform spend by operation — empty in single-customer mode. */
|
|
43
|
+
byOperation: TokenUsageAdminBreakdownInterface[];
|
|
44
|
+
/** Customer spend by operation — the customer-side mirror of byOperation. */
|
|
45
|
+
byCustomerOperation: TokenUsageAdminBreakdownInterface[];
|
|
46
|
+
companies: { id: string; label: string }[];
|
|
47
|
+
filters: TokenUsageAdminFilterState;
|
|
48
|
+
/** Merges a partial patch into the current filters; every key is optional. */
|
|
49
|
+
setFilters: (next: Partial<TokenUsageAdminFilterState>) => void;
|
|
50
|
+
/** `true` once a company filter is applied — the platform panels are meaningless then. */
|
|
51
|
+
singleCustomerMode: boolean;
|
|
52
|
+
isLoading: boolean;
|
|
53
|
+
error: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const TokenUsageAdminContext = createContext<TokenUsageAdminContextType | undefined>(undefined);
|
|
57
|
+
|
|
58
|
+
/** Current calendar month to now, which is the window the page opens on. */
|
|
59
|
+
function defaultRange(): { from: string; to: string } {
|
|
60
|
+
const now = new Date();
|
|
61
|
+
const start = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
|
|
62
|
+
return { from: start.toISOString(), to: now.toISOString() };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type TokenUsageAdminProviderProps = {
|
|
66
|
+
children: ReactNode;
|
|
67
|
+
/** Pre-select a company, which puts the page in single-customer mode from the first render. */
|
|
68
|
+
initialCompanyId?: string;
|
|
69
|
+
/** ISO 8601 instant. Defaults to the start of the current month. */
|
|
70
|
+
initialFrom?: string;
|
|
71
|
+
/** ISO 8601 instant. Defaults to now. */
|
|
72
|
+
initialTo?: string;
|
|
73
|
+
/** Rows per ranked panel. Defaults to 10. */
|
|
74
|
+
topN?: number;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Owns every filter the administrative token-usage page reads, fetches the five
|
|
79
|
+
* panels behind it, and publishes the filter bar into the page title bar.
|
|
80
|
+
*
|
|
81
|
+
* The filter bar is rendered into `title.functions` here — NOT in the container —
|
|
82
|
+
* because `RoundPageContainer`'s title bar reads `title.functions` from
|
|
83
|
+
* `SharedContext`, and a descendant cannot inject nodes into an ancestor's
|
|
84
|
+
* provider value. That is why the filter state lives at this level.
|
|
85
|
+
*/
|
|
86
|
+
export const TokenUsageAdminProvider = ({
|
|
87
|
+
children,
|
|
88
|
+
initialCompanyId,
|
|
89
|
+
initialFrom,
|
|
90
|
+
initialTo,
|
|
91
|
+
topN = DEFAULT_TOP_N,
|
|
92
|
+
}: TokenUsageAdminProviderProps) => {
|
|
93
|
+
const t = useTranslations();
|
|
94
|
+
const generateUrl = usePageUrlGenerator();
|
|
95
|
+
|
|
96
|
+
const [filters, setFilterState] = useState<TokenUsageAdminFilterState>(() => {
|
|
97
|
+
const range = defaultRange();
|
|
98
|
+
return {
|
|
99
|
+
from: initialFrom ?? range.from,
|
|
100
|
+
to: initialTo ?? range.to,
|
|
101
|
+
granularity: "day",
|
|
102
|
+
stackBy: "scope",
|
|
103
|
+
companyId: initialCompanyId,
|
|
104
|
+
metric: "cost",
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const [summary, setSummary] = useState<TokenUsageAdminSummaryInterface[]>([]);
|
|
109
|
+
const [timeline, setTimeline] = useState<TokenUsageAdminTimelineInterface[]>([]);
|
|
110
|
+
const [byCompany, setByCompany] = useState<TokenUsageAdminBreakdownInterface[]>([]);
|
|
111
|
+
const [byUser, setByUser] = useState<TokenUsageAdminBreakdownInterface[]>([]);
|
|
112
|
+
const [byOperation, setByOperation] = useState<TokenUsageAdminBreakdownInterface[]>([]);
|
|
113
|
+
const [byCustomerOperation, setByCustomerOperation] = useState<TokenUsageAdminBreakdownInterface[]>([]);
|
|
114
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
115
|
+
const [error, setError] = useState<string | null>(null);
|
|
116
|
+
|
|
117
|
+
const { from, to, granularity, stackBy, companyId, metric } = filters;
|
|
118
|
+
const singleCustomerMode = Boolean(companyId);
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
// `cancelled` is the out-of-order guard: a filter change fires a new request
|
|
122
|
+
// while the previous one is still in flight, and without this flag the slower
|
|
123
|
+
// (older) response would land last and overwrite the fresher state.
|
|
124
|
+
let cancelled = false;
|
|
125
|
+
|
|
126
|
+
setIsLoading(true);
|
|
127
|
+
setError(null);
|
|
128
|
+
|
|
129
|
+
const base = { from, to, companyId };
|
|
130
|
+
|
|
131
|
+
Promise.all([
|
|
132
|
+
TokenUsageAdminService.getSummary(base),
|
|
133
|
+
TokenUsageAdminService.getTimeline({ ...base, granularity, stackBy }),
|
|
134
|
+
TokenUsageAdminService.getBreakdown({ ...base, dimension: "company", scope: "customer", limit: topN }),
|
|
135
|
+
TokenUsageAdminService.getBreakdown({ ...base, dimension: "user", scope: "customer", limit: topN }),
|
|
136
|
+
// Platform spend is, by definition, the usage with no owning company, so
|
|
137
|
+
// filtering it by one is meaningless — the call is skipped entirely rather
|
|
138
|
+
// than issued and discarded.
|
|
139
|
+
companyId
|
|
140
|
+
? Promise.resolve<TokenUsageAdminBreakdownInterface[]>([])
|
|
141
|
+
: TokenUsageAdminService.getBreakdown({ ...base, dimension: "operation", scope: "platform", limit: topN }),
|
|
142
|
+
// Customer spend by operation — unlike the platform panel this one IS
|
|
143
|
+
// meaningful under a company filter, so it is always requested.
|
|
144
|
+
TokenUsageAdminService.getBreakdown({ ...base, dimension: "operation", scope: "customer", limit: topN }),
|
|
145
|
+
])
|
|
146
|
+
.then(([nextSummary, nextTimeline, nextByCompany, nextByUser, nextByOperation, nextByCustomerOperation]) => {
|
|
147
|
+
if (cancelled) return;
|
|
148
|
+
setSummary(nextSummary ?? []);
|
|
149
|
+
setTimeline(nextTimeline ?? []);
|
|
150
|
+
setByCompany(nextByCompany ?? []);
|
|
151
|
+
setByUser(nextByUser ?? []);
|
|
152
|
+
setByOperation(nextByOperation ?? []);
|
|
153
|
+
setByCustomerOperation(nextByCustomerOperation ?? []);
|
|
154
|
+
})
|
|
155
|
+
.catch((err) => {
|
|
156
|
+
if (cancelled) return;
|
|
157
|
+
console.error("Failed to load administrative token usage:", err);
|
|
158
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
159
|
+
})
|
|
160
|
+
.finally(() => {
|
|
161
|
+
if (cancelled) return;
|
|
162
|
+
setIsLoading(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return () => {
|
|
166
|
+
cancelled = true;
|
|
167
|
+
};
|
|
168
|
+
}, [from, to, granularity, stackBy, companyId, topN]);
|
|
169
|
+
|
|
170
|
+
const setFilters = useCallback((next: Partial<TokenUsageAdminFilterState>) => {
|
|
171
|
+
setFilterState((prev) => ({ ...prev, ...next }));
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Derived from the company breakdown, per the page design. Note the
|
|
176
|
+
* consequence: once a company filter is applied the breakdown returns that
|
|
177
|
+
* company alone, so the selector narrows to it — clearing the filter restores
|
|
178
|
+
* the full list.
|
|
179
|
+
*/
|
|
180
|
+
const companies = useMemo(
|
|
181
|
+
() => byCompany.filter((row) => row.id !== "other").map((row) => ({ id: row.id, label: row.label })),
|
|
182
|
+
[byCompany],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const breadcrumb = (): BreadcrumbItemData[] => [
|
|
186
|
+
{
|
|
187
|
+
name: t("token_usage.admin.title"),
|
|
188
|
+
href: generateUrl({ page: TOKEN_USAGE_ADMIN_PAGE_URL }),
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const title = () => ({
|
|
193
|
+
type: t("token_usage.admin.title"),
|
|
194
|
+
functions: (
|
|
195
|
+
<TokenUsageAdminFilterBar
|
|
196
|
+
key="tokenUsageAdminFilterBar"
|
|
197
|
+
from={from}
|
|
198
|
+
to={to}
|
|
199
|
+
granularity={granularity}
|
|
200
|
+
companyId={companyId}
|
|
201
|
+
metric={metric}
|
|
202
|
+
companies={companies}
|
|
203
|
+
onChange={setFilters}
|
|
204
|
+
/>
|
|
205
|
+
),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const contextValue = useMemo<TokenUsageAdminContextType>(
|
|
209
|
+
() => ({
|
|
210
|
+
summary,
|
|
211
|
+
timeline,
|
|
212
|
+
byCompany,
|
|
213
|
+
byUser,
|
|
214
|
+
byOperation,
|
|
215
|
+
byCustomerOperation,
|
|
216
|
+
companies,
|
|
217
|
+
filters,
|
|
218
|
+
setFilters,
|
|
219
|
+
singleCustomerMode,
|
|
220
|
+
isLoading,
|
|
221
|
+
error,
|
|
222
|
+
}),
|
|
223
|
+
[
|
|
224
|
+
summary,
|
|
225
|
+
timeline,
|
|
226
|
+
byCompany,
|
|
227
|
+
byUser,
|
|
228
|
+
byOperation,
|
|
229
|
+
byCustomerOperation,
|
|
230
|
+
companies,
|
|
231
|
+
filters,
|
|
232
|
+
setFilters,
|
|
233
|
+
singleCustomerMode,
|
|
234
|
+
isLoading,
|
|
235
|
+
error,
|
|
236
|
+
],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<SharedProvider value={{ breadcrumbs: breadcrumb(), title: title() }}>
|
|
241
|
+
<TokenUsageAdminContext.Provider value={contextValue}>{children}</TokenUsageAdminContext.Provider>
|
|
242
|
+
</SharedProvider>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export const useTokenUsageAdmin = (): TokenUsageAdminContextType => {
|
|
247
|
+
const ctx = useContext(TokenUsageAdminContext);
|
|
248
|
+
if (!ctx) {
|
|
249
|
+
throw new Error("useTokenUsageAdmin() called outside <TokenUsageAdminProvider>.");
|
|
250
|
+
}
|
|
251
|
+
return ctx;
|
|
252
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { TokenUsageAdminService } from "../../data/TokenUsageAdminService";
|
|
4
|
+
import { TokenUsageAdminProvider, useTokenUsageAdmin } from "../TokenUsageAdminContext";
|
|
5
|
+
|
|
6
|
+
vi.mock("../../data/TokenUsageAdminService", () => ({
|
|
7
|
+
TokenUsageAdminService: {
|
|
8
|
+
getSummary: vi.fn(async () => []),
|
|
9
|
+
getTimeline: vi.fn(async () => []),
|
|
10
|
+
getBreakdown: vi.fn(async () => []),
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
function Probe() {
|
|
15
|
+
const ctx = useTokenUsageAdmin();
|
|
16
|
+
return <span data-testid="mode">{String(ctx.singleCustomerMode)}</span>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("TokenUsageAdminContext", () => {
|
|
20
|
+
// The mocked service is module-level, so its call counts survive across tests.
|
|
21
|
+
// Without this reset the per-test `toHaveBeenCalledTimes` assertions would
|
|
22
|
+
// measure the whole file rather than one render.
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("throws when used outside its provider", () => {
|
|
28
|
+
expect(() => render(<Probe />)).toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("requests every panel on mount and reports not-single-customer by default", async () => {
|
|
32
|
+
render(
|
|
33
|
+
<TokenUsageAdminProvider>
|
|
34
|
+
<Probe />
|
|
35
|
+
</TokenUsageAdminProvider>,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
await waitFor(() => expect(TokenUsageAdminService.getSummary).toHaveBeenCalled());
|
|
39
|
+
|
|
40
|
+
// Four ranked panels: company, user, customer-by-operation, platform-by-operation.
|
|
41
|
+
// Asserted by dimension+scope rather than by count alone, so a wrong pairing
|
|
42
|
+
// fails here instead of silently rendering the wrong data.
|
|
43
|
+
const calls = (TokenUsageAdminService.getBreakdown as ReturnType<typeof vi.fn>).mock.calls.map(([args]) => [
|
|
44
|
+
args.dimension,
|
|
45
|
+
args.scope,
|
|
46
|
+
]);
|
|
47
|
+
expect(calls).toEqual(
|
|
48
|
+
expect.arrayContaining([
|
|
49
|
+
["company", "customer"],
|
|
50
|
+
["user", "customer"],
|
|
51
|
+
["operation", "customer"],
|
|
52
|
+
["operation", "platform"],
|
|
53
|
+
]),
|
|
54
|
+
);
|
|
55
|
+
expect(calls).toHaveLength(4);
|
|
56
|
+
expect(screen.getByTestId("mode")).toHaveTextContent("false");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("skips only the PLATFORM operation breakdown once a company filter is applied", async () => {
|
|
60
|
+
render(
|
|
61
|
+
<TokenUsageAdminProvider initialCompanyId="c1">
|
|
62
|
+
<Probe />
|
|
63
|
+
</TokenUsageAdminProvider>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
await waitFor(() => expect(TokenUsageAdminService.getSummary).toHaveBeenCalled());
|
|
67
|
+
|
|
68
|
+
const calls = (TokenUsageAdminService.getBreakdown as ReturnType<typeof vi.fn>).mock.calls.map(([args]) => [
|
|
69
|
+
args.dimension,
|
|
70
|
+
args.scope,
|
|
71
|
+
]);
|
|
72
|
+
// Platform spend has no owning company, so filtering it is meaningless and
|
|
73
|
+
// the call is skipped. The CUSTOMER operation panel is still meaningful and
|
|
74
|
+
// must survive — that distinction is the point of this test.
|
|
75
|
+
expect(calls).toContainEqual(["operation", "customer"]);
|
|
76
|
+
expect(calls).not.toContainEqual(["operation", "platform"]);
|
|
77
|
+
expect(calls).toHaveLength(3);
|
|
78
|
+
expect(screen.getByTestId("mode")).toHaveTextContent("true");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
|
|
2
|
+
import { TokenUsageAdminBreakdownInterface } from "./tokenusage-admin-breakdown.interface";
|
|
3
|
+
import { TokenUsageAdminSummaryInterface } from "./tokenusage-admin-summary.interface";
|
|
4
|
+
import { TokenUsageAdminTimelineInterface } from "./tokenusage-admin-timeline.interface";
|
|
5
|
+
import { Dimension, Granularity, Scope, StackBy, TokenUsageAdminFilters } from "./tokenusage-admin.types";
|
|
6
|
+
|
|
7
|
+
function withFilters(endpoint: EndpointCreator, filters: TokenUsageAdminFilters): EndpointCreator {
|
|
8
|
+
endpoint.addAdditionalParam("from", filters.from);
|
|
9
|
+
endpoint.addAdditionalParam("to", filters.to);
|
|
10
|
+
if (filters.companyId) endpoint.addAdditionalParam("companyId", filters.companyId);
|
|
11
|
+
return endpoint;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class TokenUsageAdminService extends AbstractService {
|
|
15
|
+
/** Six rows: {customer, platform, total} × {current, previous}. */
|
|
16
|
+
static async getSummary(filters: TokenUsageAdminFilters): Promise<TokenUsageAdminSummaryInterface[]> {
|
|
17
|
+
const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.TokenUsageAdminSummary }), filters);
|
|
18
|
+
|
|
19
|
+
return this.callApi<TokenUsageAdminSummaryInterface[]>({
|
|
20
|
+
type: Modules.TokenUsageAdminSummary,
|
|
21
|
+
method: HttpMethod.GET,
|
|
22
|
+
endpoint: endpoint.generate(),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static async getTimeline(
|
|
27
|
+
params: TokenUsageAdminFilters & { granularity: Granularity; stackBy: StackBy },
|
|
28
|
+
): Promise<TokenUsageAdminTimelineInterface[]> {
|
|
29
|
+
const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.TokenUsageAdminTimeline }), params);
|
|
30
|
+
endpoint.addAdditionalParam("granularity", params.granularity);
|
|
31
|
+
endpoint.addAdditionalParam("stackBy", params.stackBy);
|
|
32
|
+
|
|
33
|
+
return this.callApi<TokenUsageAdminTimelineInterface[]>({
|
|
34
|
+
type: Modules.TokenUsageAdminTimeline,
|
|
35
|
+
method: HttpMethod.GET,
|
|
36
|
+
endpoint: endpoint.generate(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static async getBreakdown(
|
|
41
|
+
params: TokenUsageAdminFilters & { dimension: Dimension; scope: Scope; limit?: number },
|
|
42
|
+
): Promise<TokenUsageAdminBreakdownInterface[]> {
|
|
43
|
+
const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.TokenUsageAdminBreakdown }), params);
|
|
44
|
+
endpoint.addAdditionalParam("dimension", params.dimension);
|
|
45
|
+
endpoint.addAdditionalParam("scope", params.scope);
|
|
46
|
+
if (params.limit !== undefined) endpoint.addAdditionalParam("limit", String(params.limit));
|
|
47
|
+
|
|
48
|
+
return this.callApi<TokenUsageAdminBreakdownInterface[]>({
|
|
49
|
+
type: Modules.TokenUsageAdminBreakdown,
|
|
50
|
+
method: HttpMethod.GET,
|
|
51
|
+
endpoint: endpoint.generate(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { ModuleFactory, ModuleWithPermissions } from "../../../../permissions/types";
|
|
3
|
+
import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
|
|
4
|
+
import {
|
|
5
|
+
TokenUsageAdminBreakdownModule,
|
|
6
|
+
TokenUsageAdminSummaryModule,
|
|
7
|
+
TokenUsageAdminTimelineModule,
|
|
8
|
+
} from "../../tokenusage-admin.module";
|
|
9
|
+
import { TokenUsageAdminBreakdown } from "../tokenusage-admin-breakdown";
|
|
10
|
+
import { TokenUsageAdminSummary } from "../tokenusage-admin-summary";
|
|
11
|
+
import { TokenUsageAdminTimeline } from "../tokenusage-admin-timeline";
|
|
12
|
+
|
|
13
|
+
// createJsonApi reads Modules.X.name (resolved lazily via ModuleRegistry), so
|
|
14
|
+
// the modules must be registered before a model serialises — mirrors the
|
|
15
|
+
// HowTo.spec / AssistantMessage.spec setup convention in this package.
|
|
16
|
+
const moduleFactory: ModuleFactory = (params) =>
|
|
17
|
+
({
|
|
18
|
+
pageUrl: params.pageUrl,
|
|
19
|
+
name: params.name,
|
|
20
|
+
model: params.model,
|
|
21
|
+
}) as ModuleWithPermissions;
|
|
22
|
+
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
ModuleRegistry.register("TokenUsageAdminSummary", TokenUsageAdminSummaryModule(moduleFactory));
|
|
25
|
+
ModuleRegistry.register("TokenUsageAdminTimeline", TokenUsageAdminTimelineModule(moduleFactory));
|
|
26
|
+
ModuleRegistry.register("TokenUsageAdminBreakdown", TokenUsageAdminBreakdownModule(moduleFactory));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const hydrate = (attributes: Record<string, unknown>) =>
|
|
30
|
+
({ jsonApi: { id: "x", type: "t", attributes }, included: [] }) as any;
|
|
31
|
+
|
|
32
|
+
describe("token usage admin models", () => {
|
|
33
|
+
it("rehydrates every summary attribute", () => {
|
|
34
|
+
const m = new TokenUsageAdminSummary().rehydrate(
|
|
35
|
+
hydrate({
|
|
36
|
+
scope: "customer",
|
|
37
|
+
window: "current",
|
|
38
|
+
cost: 9.46,
|
|
39
|
+
credits: 3394,
|
|
40
|
+
tokensIn: 49244143,
|
|
41
|
+
tokensOut: 12,
|
|
42
|
+
cached: 75752,
|
|
43
|
+
calls: 2956,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
expect(m.scope).toBe("customer");
|
|
47
|
+
expect(m.window).toBe("current");
|
|
48
|
+
expect(m.cost).toBe(9.46);
|
|
49
|
+
expect(m.credits).toBe(3394);
|
|
50
|
+
expect(m.tokensIn).toBe(49244143);
|
|
51
|
+
expect(m.tokensOut).toBe(12);
|
|
52
|
+
expect(m.cached).toBe(75752);
|
|
53
|
+
expect(m.calls).toBe(2956);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("parses the timeline bucket into a Date, not the wire string", () => {
|
|
57
|
+
const m = new TokenUsageAdminTimeline().rehydrate(
|
|
58
|
+
hydrate({
|
|
59
|
+
bucket: "2026-08-01",
|
|
60
|
+
series: "customer",
|
|
61
|
+
cost: 1,
|
|
62
|
+
credits: 1,
|
|
63
|
+
tokensIn: 1,
|
|
64
|
+
tokensOut: 1,
|
|
65
|
+
cached: 0,
|
|
66
|
+
calls: 1,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
expect(m.bucket).toBeInstanceOf(Date);
|
|
70
|
+
expect(m.bucket.toISOString().slice(0, 10)).toBe("2026-08-01");
|
|
71
|
+
expect(m.series).toBe("customer");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("leaves company-only breakdown fields undefined on other dimensions", () => {
|
|
75
|
+
const m = new TokenUsageAdminBreakdown().rehydrate(
|
|
76
|
+
hydrate({
|
|
77
|
+
label: "m.rossi",
|
|
78
|
+
sublabel: "Studio Rossi",
|
|
79
|
+
cost: 3.74,
|
|
80
|
+
credits: 900,
|
|
81
|
+
tokensIn: 1,
|
|
82
|
+
tokensOut: 1,
|
|
83
|
+
cached: 0,
|
|
84
|
+
calls: 4,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
expect(m.label).toBe("m.rossi");
|
|
88
|
+
expect(m.sublabel).toBe("Studio Rossi");
|
|
89
|
+
expect(m.activeUsers).toBeUndefined();
|
|
90
|
+
expect(m.monthlyCredits).toBeUndefined();
|
|
91
|
+
expect(m.availableMonthlyCredits).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("serialises a summary back to a JSON:API resource object", () => {
|
|
95
|
+
const doc = new TokenUsageAdminSummary().createJsonApi({
|
|
96
|
+
id: "customer|current",
|
|
97
|
+
scope: "customer",
|
|
98
|
+
window: "current",
|
|
99
|
+
cost: 9.46,
|
|
100
|
+
credits: 3394,
|
|
101
|
+
tokensIn: 1,
|
|
102
|
+
tokensOut: 2,
|
|
103
|
+
cached: 3,
|
|
104
|
+
calls: 4,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(doc.data.type).toBe("tokenusages/administration/summary");
|
|
108
|
+
expect(doc.data.id).toBe("customer|current");
|
|
109
|
+
expect(doc.data.attributes).toMatchObject({ scope: "customer", window: "current", cost: 9.46, calls: 4 });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("emits the timeline bucket as YYYY-MM-DD via formatLocalDate, never a raw Date", () => {
|
|
113
|
+
const doc = new TokenUsageAdminTimeline().createJsonApi({
|
|
114
|
+
id: "2026-08-01|customer",
|
|
115
|
+
bucket: new Date(2026, 7, 1),
|
|
116
|
+
series: "customer",
|
|
117
|
+
cost: 1,
|
|
118
|
+
credits: 1,
|
|
119
|
+
tokensIn: 1,
|
|
120
|
+
tokensOut: 1,
|
|
121
|
+
cached: 0,
|
|
122
|
+
calls: 1,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(doc.data.attributes.bucket).toBe("2026-08-01");
|
|
126
|
+
expect(typeof doc.data.attributes.bucket).toBe("string");
|
|
127
|
+
// A raw Date would survive JSON.stringify as an ISO instant and UTC-shift.
|
|
128
|
+
expect(JSON.parse(JSON.stringify(doc)).data.attributes.bucket).toBe("2026-08-01");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("omits absent optional breakdown attributes rather than emitting undefined", () => {
|
|
132
|
+
const doc = new TokenUsageAdminBreakdown().createJsonApi({
|
|
133
|
+
id: "u1",
|
|
134
|
+
label: "m.rossi",
|
|
135
|
+
cost: 1,
|
|
136
|
+
credits: 1,
|
|
137
|
+
tokensIn: 1,
|
|
138
|
+
tokensOut: 1,
|
|
139
|
+
cached: 0,
|
|
140
|
+
calls: 1,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(doc.data.attributes.label).toBe("m.rossi");
|
|
144
|
+
expect("sublabel" in doc.data.attributes).toBe(false);
|
|
145
|
+
expect("activeUsers" in doc.data.attributes).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./tokenusage-admin.types";
|
|
2
|
+
export { TokenUsageAdminSummary } from "./tokenusage-admin-summary";
|
|
3
|
+
export type { TokenUsageAdminSummaryInterface } from "./tokenusage-admin-summary.interface";
|
|
4
|
+
export { TokenUsageAdminTimeline } from "./tokenusage-admin-timeline";
|
|
5
|
+
export type { TokenUsageAdminTimelineInterface } from "./tokenusage-admin-timeline.interface";
|
|
6
|
+
export { TokenUsageAdminBreakdown } from "./tokenusage-admin-breakdown";
|
|
7
|
+
export type { TokenUsageAdminBreakdownInterface } from "./tokenusage-admin-breakdown.interface";
|
|
8
|
+
export { TokenUsageAdminService } from "./TokenUsageAdminService";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
export interface TokenUsageAdminBreakdownInterface 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
|
+
/** Company dimension only. */
|
|
13
|
+
get activeUsers(): number | undefined;
|
|
14
|
+
/** Company dimension only. */
|
|
15
|
+
get monthlyCredits(): number | undefined;
|
|
16
|
+
/** Company dimension only. */
|
|
17
|
+
get availableMonthlyCredits(): number | undefined;
|
|
18
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { TokenUsageAdminBreakdownInterface } from "./tokenusage-admin-breakdown.interface";
|
|
3
|
+
import { TokenUsageAdminBreakdownInput } from "./tokenusage-admin.types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One ranked row of a breakdown. The same shape serves all three dimensions —
|
|
7
|
+
* company, user and operation — which is why the three "by-X" views collapse
|
|
8
|
+
* into a single `breakdown?dimension=` call.
|
|
9
|
+
*
|
|
10
|
+
* `activeUsers` / `monthlyCredits` / `availableMonthlyCredits` are populated only
|
|
11
|
+
* for `dimension=company`; they are absent on user and operation rows.
|
|
12
|
+
*/
|
|
13
|
+
export class TokenUsageAdminBreakdown extends AbstractApiData implements TokenUsageAdminBreakdownInterface {
|
|
14
|
+
private _label: string = "";
|
|
15
|
+
private _sublabel?: 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
|
+
private _activeUsers?: number;
|
|
23
|
+
private _monthlyCredits?: number;
|
|
24
|
+
private _availableMonthlyCredits?: number;
|
|
25
|
+
|
|
26
|
+
get label(): string {
|
|
27
|
+
return this._label;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get sublabel(): string | undefined {
|
|
31
|
+
return this._sublabel;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get cost(): number {
|
|
35
|
+
return this._cost;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get credits(): number {
|
|
39
|
+
return this._credits;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get tokensIn(): number {
|
|
43
|
+
return this._tokensIn;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get tokensOut(): number {
|
|
47
|
+
return this._tokensOut;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get cached(): number {
|
|
51
|
+
return this._cached;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get calls(): number {
|
|
55
|
+
return this._calls;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get activeUsers(): number | undefined {
|
|
59
|
+
return this._activeUsers;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get monthlyCredits(): number | undefined {
|
|
63
|
+
return this._monthlyCredits;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get availableMonthlyCredits(): number | undefined {
|
|
67
|
+
return this._availableMonthlyCredits;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
71
|
+
super.rehydrate(data);
|
|
72
|
+
|
|
73
|
+
const attrs = data.jsonApi.attributes;
|
|
74
|
+
this._label = attrs.label ?? "";
|
|
75
|
+
this._sublabel = attrs.sublabel ?? undefined;
|
|
76
|
+
this._cost = attrs.cost ?? 0;
|
|
77
|
+
this._credits = attrs.credits ?? 0;
|
|
78
|
+
this._tokensIn = attrs.tokensIn ?? 0;
|
|
79
|
+
this._tokensOut = attrs.tokensOut ?? 0;
|
|
80
|
+
this._cached = attrs.cached ?? 0;
|
|
81
|
+
this._calls = attrs.calls ?? 0;
|
|
82
|
+
this._activeUsers = attrs.activeUsers ?? undefined;
|
|
83
|
+
this._monthlyCredits = attrs.monthlyCredits ?? undefined;
|
|
84
|
+
this._availableMonthlyCredits = attrs.availableMonthlyCredits ?? undefined;
|
|
85
|
+
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
createJsonApi(data: TokenUsageAdminBreakdownInput) {
|
|
90
|
+
const response: any = {
|
|
91
|
+
data: {
|
|
92
|
+
type: Modules.TokenUsageAdminBreakdown.name,
|
|
93
|
+
id: data.id,
|
|
94
|
+
attributes: {},
|
|
95
|
+
relationships: {},
|
|
96
|
+
},
|
|
97
|
+
included: [],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (data.label !== undefined) response.data.attributes.label = data.label;
|
|
101
|
+
if (data.sublabel !== undefined) response.data.attributes.sublabel = data.sublabel;
|
|
102
|
+
if (data.cost !== undefined) response.data.attributes.cost = data.cost;
|
|
103
|
+
if (data.credits !== undefined) response.data.attributes.credits = data.credits;
|
|
104
|
+
if (data.tokensIn !== undefined) response.data.attributes.tokensIn = data.tokensIn;
|
|
105
|
+
if (data.tokensOut !== undefined) response.data.attributes.tokensOut = data.tokensOut;
|
|
106
|
+
if (data.cached !== undefined) response.data.attributes.cached = data.cached;
|
|
107
|
+
if (data.calls !== undefined) response.data.attributes.calls = data.calls;
|
|
108
|
+
if (data.activeUsers !== undefined) response.data.attributes.activeUsers = data.activeUsers;
|
|
109
|
+
if (data.monthlyCredits !== undefined) response.data.attributes.monthlyCredits = data.monthlyCredits;
|
|
110
|
+
if (data.availableMonthlyCredits !== undefined)
|
|
111
|
+
response.data.attributes.availableMonthlyCredits = data.availableMonthlyCredits;
|
|
112
|
+
|
|
113
|
+
return response;
|
|
114
|
+
}
|
|
115
|
+
}
|