@carlonicora/nextjs-jsonapi 2.1.2 → 2.2.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-FYO6TV4X.js → BlockNoteEditor-5TMQ5YCD.js} +9 -9
- package/dist/{BlockNoteEditor-FYO6TV4X.js.map → BlockNoteEditor-5TMQ5YCD.js.map} +1 -1
- package/dist/{BlockNoteEditor-DYQ5BLQI.mjs → BlockNoteEditor-HAUE4EPR.mjs} +2 -2
- package/dist/billing/index.js +310 -310
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-TEJDX7X7.js → chunk-HPKCZMQX.js} +96 -31
- package/dist/chunk-HPKCZMQX.js.map +1 -0
- package/dist/{chunk-B3ELVSTL.mjs → chunk-PGW2OP3L.mjs} +139 -74
- package/dist/chunk-PGW2OP3L.mjs.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.d.mts +12 -0
- package/dist/components/index.d.ts +12 -0
- package/dist/components/index.js +2 -2
- package/dist/components/index.mjs +1 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/features/help/index.js +31 -31
- package/dist/features/help/index.mjs +1 -1
- package/dist/features/tokenusage/index.js +56 -56
- package/dist/features/tokenusage/index.mjs +1 -1
- package/package.json +1 -1
- package/src/components/navigations/Header.tsx +5 -2
- package/src/components/navigations/RecentPagesNavigator.tsx +64 -33
- package/src/components/navigations/__tests__/RecentPagesNavigator.spec.tsx +166 -0
- package/src/hooks/__tests__/usePageTracker.spec.tsx +296 -0
- package/src/hooks/usePageTracker.ts +108 -19
- package/dist/chunk-B3ELVSTL.mjs.map +0 -1
- package/dist/chunk-TEJDX7X7.js.map +0 -1
- /package/dist/{BlockNoteEditor-DYQ5BLQI.mjs.map → BlockNoteEditor-HAUE4EPR.mjs.map} +0 -0
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
var
|
|
29
|
+
var _chunkHPKCZMQXjs = require('../../chunk-HPKCZMQX.js');
|
|
30
30
|
require('../../chunk-JG7MCYCW.js');
|
|
31
31
|
|
|
32
32
|
|
|
@@ -64,7 +64,7 @@ function TokenUsageAdminFilterBar({ granularity, companyId, metric, companies, o
|
|
|
64
64
|
};
|
|
65
65
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
66
66
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
67
|
-
|
|
67
|
+
_chunkHPKCZMQXjs.DateRangeSelector,
|
|
68
68
|
{
|
|
69
69
|
onDateChange: (range) => {
|
|
70
70
|
if (!_optionalChain([range, 'optionalAccess', _ => _.from]) || !_optionalChain([range, 'optionalAccess', _2 => _2.to])) return;
|
|
@@ -99,16 +99,16 @@ function TokenUsageAdminFilterBar({ granularity, companyId, metric, companies, o
|
|
|
99
99
|
}
|
|
100
100
|
),
|
|
101
101
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
102
|
-
|
|
102
|
+
_chunkHPKCZMQXjs.Select,
|
|
103
103
|
{
|
|
104
104
|
items: companyItems,
|
|
105
105
|
value: _nullishCoalesce(companyId, () => ( ALL_COMPANIES)),
|
|
106
106
|
onValueChange: (value) => onChange({ companyId: !value || value === ALL_COMPANIES ? void 0 : value }),
|
|
107
107
|
children: [
|
|
108
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
109
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
110
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
111
|
-
companies.map((company) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
108
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.SelectTrigger, { className: "w-56", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.SelectValue, { placeholder: t("token_usage.admin.all_companies") }) }),
|
|
109
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkHPKCZMQXjs.SelectContent, { children: [
|
|
110
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.SelectItem, { value: ALL_COMPANIES, children: t("token_usage.admin.all_companies") }),
|
|
111
|
+
companies.map((company) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.SelectItem, { value: company.id, children: company.label }, company.id))
|
|
112
112
|
] })
|
|
113
113
|
]
|
|
114
114
|
}
|
|
@@ -131,7 +131,7 @@ function Segmented({
|
|
|
131
131
|
children: options.map((option) => {
|
|
132
132
|
const selected = option.value === value;
|
|
133
133
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
134
|
-
|
|
134
|
+
_chunkHPKCZMQXjs.Button,
|
|
135
135
|
{
|
|
136
136
|
type: "button",
|
|
137
137
|
size: "sm",
|
|
@@ -212,7 +212,7 @@ var TokenUsageAdminProvider = /* @__PURE__ */ _chunk7QVYU63Ejs.__name.call(void
|
|
|
212
212
|
topN = DEFAULT_TOP_N
|
|
213
213
|
}) => {
|
|
214
214
|
const t = _nextintl.useTranslations.call(void 0, );
|
|
215
|
-
const generateUrl =
|
|
215
|
+
const generateUrl = _chunkHPKCZMQXjs.usePageUrlGenerator.call(void 0, );
|
|
216
216
|
const [filters, setFilterState] = _react.useState.call(void 0, () => {
|
|
217
217
|
const range = defaultRange();
|
|
218
218
|
return {
|
|
@@ -330,7 +330,7 @@ var TokenUsageAdminProvider = /* @__PURE__ */ _chunk7QVYU63Ejs.__name.call(void
|
|
|
330
330
|
error
|
|
331
331
|
]
|
|
332
332
|
);
|
|
333
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
333
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.SharedProvider, { value: { breadcrumbs: breadcrumb(), title: title() }, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, TokenUsageAdminContext.Provider, { value: contextValue, children }) });
|
|
334
334
|
}, "TokenUsageAdminProvider");
|
|
335
335
|
var useTokenUsageAdmin = /* @__PURE__ */ _chunk7QVYU63Ejs.__name.call(void 0, () => {
|
|
336
336
|
const ctx = _react.useContext.call(void 0, TokenUsageAdminContext);
|
|
@@ -458,7 +458,7 @@ function LeadTile({
|
|
|
458
458
|
delta,
|
|
459
459
|
previousLabel
|
|
460
460
|
}) {
|
|
461
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
461
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.Card, { "data-testid": testId, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkHPKCZMQXjs.CardContent, { className: "grid gap-1", children: [
|
|
462
462
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-muted-foreground text-xs", children: label }),
|
|
463
463
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-primary text-xl font-semibold tabular-nums", children: value }),
|
|
464
464
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { className: "flex items-center gap-1", children: [
|
|
@@ -474,7 +474,7 @@ function SupportingTile({
|
|
|
474
474
|
value,
|
|
475
475
|
delta
|
|
476
476
|
}) {
|
|
477
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
477
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.Card, { "data-testid": testId, size: "sm", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkHPKCZMQXjs.CardContent, { className: "grid gap-1", children: [
|
|
478
478
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-muted-foreground text-xs", children: label }),
|
|
479
479
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { className: "flex items-center gap-2", children: [
|
|
480
480
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-sm font-medium tabular-nums", children: value }),
|
|
@@ -566,8 +566,8 @@ function TokenUsageBreakdownTable({ rows, metric }) {
|
|
|
566
566
|
// The package's <Table> wraps itself in an `overflow-x-clip` container, which
|
|
567
567
|
// would swallow the overflow before this scroller ever saw it — hence the
|
|
568
568
|
// child override. Wide metric tables must scroll, never widen the page.
|
|
569
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "overflow-x-auto [&_[data-slot=table-container]]:overflow-x-visible", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
570
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
569
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "overflow-x-auto [&_[data-slot=table-container]]:overflow-x-visible", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkHPKCZMQXjs.Table, { children: [
|
|
570
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableHeader, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableRow, { children: columns.map((column) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableHead, { className: _chunk6YIZX26Tjs.cn.call(void 0, column.numeric && "text-right"), children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
571
571
|
"button",
|
|
572
572
|
{
|
|
573
573
|
type: "button",
|
|
@@ -582,20 +582,20 @@ function TokenUsageBreakdownTable({ rows, metric }) {
|
|
|
582
582
|
]
|
|
583
583
|
}
|
|
584
584
|
) }, column.key)) }) }),
|
|
585
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
585
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableBody, { children: sortedRows.map((row) => {
|
|
586
586
|
const value = metricValue(row, metric);
|
|
587
587
|
const share = total > 0 ? value / total * 100 : 0;
|
|
588
|
-
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
589
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
590
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
591
|
-
showActiveUsers && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
592
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
593
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
594
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
595
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
596
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
597
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
598
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
588
|
+
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkHPKCZMQXjs.TableRow, { "data-testid": `breakdown-row-${row.id}`, children: [
|
|
589
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableCell, { className: "text-xs", children: row.id === "other" ? t("token_usage.admin.other") : row.label }),
|
|
590
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableCell, { className: "text-muted-foreground text-xs", children: _nullishCoalesce(row.sublabel, () => ( "")) }),
|
|
591
|
+
showActiveUsers && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableCell, { className: "text-right text-xs tabular-nums", children: row.activeUsers === void 0 ? "" : formatDecimal(row.activeUsers, 0) }),
|
|
592
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableCell, { className: "text-right text-xs tabular-nums", children: formatDecimal(row.calls, 0) }),
|
|
593
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableCell, { className: "text-right text-xs tabular-nums", children: formatDecimal(row.tokensIn, 0) }),
|
|
594
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableCell, { className: "text-right text-xs tabular-nums", children: formatDecimal(row.tokensOut, 0) }),
|
|
595
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableCell, { className: "text-right text-xs tabular-nums", children: formatPercent(cacheHitPercentage(row.cached, row.tokensIn)) }),
|
|
596
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableCell, { className: "text-right text-xs tabular-nums", children: formatMetricValue(row.cost, "cost") }),
|
|
597
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableCell, { className: "text-right text-xs tabular-nums", children: formatMetricValue(row.credits, "credits") }),
|
|
598
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.TableCell, { className: "text-right text-xs tabular-nums", children: formatPercent(share) })
|
|
599
599
|
] }, row.id);
|
|
600
600
|
}) })
|
|
601
601
|
] }) })
|
|
@@ -789,7 +789,7 @@ function TokenUsageTimelineChart({ rows, metric, stackBy, className }) {
|
|
|
789
789
|
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className, children: [
|
|
790
790
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "sr-only", "data-testid": "timeline-data", children: JSON.stringify(chartData) }),
|
|
791
791
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "sr-only", "data-testid": "timeline-series", children: JSON.stringify(seriesKeys) }),
|
|
792
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
792
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.ChartContainer, { config: chartConfig, className: "aspect-auto h-72 w-full", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _recharts.BarChart, { accessibilityLayer: true, data: chartData, margin: { top: 8, right: 8, bottom: 0, left: 0 }, children: [
|
|
793
793
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _recharts.CartesianGrid, { vertical: false }),
|
|
794
794
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
795
795
|
_recharts.XAxis,
|
|
@@ -813,17 +813,17 @@ function TokenUsageTimelineChart({ rows, metric, stackBy, className }) {
|
|
|
813
813
|
}
|
|
814
814
|
),
|
|
815
815
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
816
|
-
|
|
816
|
+
_chunkHPKCZMQXjs.ChartTooltip,
|
|
817
817
|
{
|
|
818
818
|
content: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
819
|
-
|
|
819
|
+
_chunkHPKCZMQXjs.ChartTooltipContent,
|
|
820
820
|
{
|
|
821
821
|
labelFormatter: (value) => formatBucket.format(/* @__PURE__ */ new Date(`${String(value)}T00:00:00.000Z`))
|
|
822
822
|
}
|
|
823
823
|
)
|
|
824
824
|
}
|
|
825
825
|
),
|
|
826
|
-
seriesKeys.length >= 2 ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
826
|
+
seriesKeys.length >= 2 ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.ChartLegend, { content: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.ChartLegendContent, {}) }) : null,
|
|
827
827
|
seriesKeys.map((series, index) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
828
828
|
_recharts.Bar,
|
|
829
829
|
{
|
|
@@ -869,19 +869,19 @@ function TokenUsageAdminContainer() {
|
|
|
869
869
|
[t]
|
|
870
870
|
);
|
|
871
871
|
if (error) {
|
|
872
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
872
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.RoundPageContainer, { fullWidth: true, forceHeader: true, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "p-4", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.Card, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardContent, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-destructive text-xs/relaxed", children: error }) }) }) }) });
|
|
873
873
|
}
|
|
874
|
-
if (isLoading) return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
874
|
+
if (isLoading) return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.RoundPageContainer, { fullWidth: true, forceHeader: true });
|
|
875
875
|
const emptyLabel = t("token_usage.admin.no_data");
|
|
876
|
-
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
876
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.RoundPageContainer, { fullWidth: true, forceHeader: true, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex w-full flex-col gap-4 p-4", children: [
|
|
877
877
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, TokenUsageAdminTiles, { summary, metric: filters.metric, singleCustomerMode }),
|
|
878
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
879
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
880
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
881
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
878
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkHPKCZMQXjs.Card, { children: [
|
|
879
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkHPKCZMQXjs.CardHeader, { children: [
|
|
880
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardTitle, { children: t("token_usage.admin.usage_over_time") }),
|
|
881
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardAction, { children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-2", children: [
|
|
882
882
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-muted-foreground text-xs", children: t("token_usage.admin.stack_by") }),
|
|
883
883
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
884
|
-
|
|
884
|
+
_chunkHPKCZMQXjs.Select,
|
|
885
885
|
{
|
|
886
886
|
items: stackByItems,
|
|
887
887
|
value: filters.stackBy,
|
|
@@ -889,29 +889,29 @@ function TokenUsageAdminContainer() {
|
|
|
889
889
|
if (value) setFilters({ stackBy: value });
|
|
890
890
|
},
|
|
891
891
|
children: [
|
|
892
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
893
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
892
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.SelectTrigger, { size: "sm", className: "w-36", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.SelectValue, {}) }),
|
|
893
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.SelectContent, { children: STACK_BY_VALUES.map((value) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.SelectItem, { value, children: stackByItems[value] }, value)) })
|
|
894
894
|
]
|
|
895
895
|
}
|
|
896
896
|
)
|
|
897
897
|
] }) })
|
|
898
898
|
] }),
|
|
899
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
899
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardContent, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, TokenUsageTimelineChart, { rows: timeline, metric: filters.metric, stackBy: filters.stackBy }) })
|
|
900
900
|
] }),
|
|
901
901
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "grid gap-4 md:grid-cols-2", children: [
|
|
902
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
903
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
904
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
902
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkHPKCZMQXjs.Card, { children: [
|
|
903
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardHeader, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardTitle, { children: t("token_usage.admin.by_company") }) }),
|
|
904
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardContent, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, TokenUsageRankedBar, { rows: byCompany, metric: filters.metric, emptyLabel }) })
|
|
905
905
|
] }),
|
|
906
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
907
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
908
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
906
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkHPKCZMQXjs.Card, { children: [
|
|
907
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardHeader, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardTitle, { children: t("token_usage.admin.by_user") }) }),
|
|
908
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardContent, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, TokenUsageRankedBar, { rows: byUser, metric: filters.metric, emptyLabel }) })
|
|
909
909
|
] })
|
|
910
910
|
] }),
|
|
911
911
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: _chunk6YIZX26Tjs.cn.call(void 0, "grid gap-4", !singleCustomerMode && "md:grid-cols-2"), children: [
|
|
912
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
913
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
914
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
912
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkHPKCZMQXjs.Card, { children: [
|
|
913
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardHeader, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardTitle, { children: t("token_usage.admin.customer_by_operation") }) }),
|
|
914
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardContent, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
915
915
|
TokenUsageRankedBar,
|
|
916
916
|
{
|
|
917
917
|
rows: byCustomerOperation,
|
|
@@ -921,9 +921,9 @@ function TokenUsageAdminContainer() {
|
|
|
921
921
|
}
|
|
922
922
|
) })
|
|
923
923
|
] }),
|
|
924
|
-
!singleCustomerMode && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
925
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
926
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
924
|
+
!singleCustomerMode && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkHPKCZMQXjs.Card, { children: [
|
|
925
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardHeader, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardTitle, { children: t("token_usage.admin.platform_by_operation") }) }),
|
|
926
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardContent, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
927
927
|
TokenUsageRankedBar,
|
|
928
928
|
{
|
|
929
929
|
rows: byOperation,
|
|
@@ -934,9 +934,9 @@ function TokenUsageAdminContainer() {
|
|
|
934
934
|
) })
|
|
935
935
|
] })
|
|
936
936
|
] }),
|
|
937
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
938
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
939
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
937
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _chunkHPKCZMQXjs.Card, { children: [
|
|
938
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardHeader, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardTitle, { children: t("token_usage.admin.detail") }) }),
|
|
939
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkHPKCZMQXjs.CardContent, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, TokenUsageBreakdownTable, { rows: byCompany, metric: filters.metric }) })
|
|
940
940
|
] })
|
|
941
941
|
] }) });
|
|
942
942
|
}
|
package/package.json
CHANGED
|
@@ -51,8 +51,11 @@ export function Header({ children, mobileChildren, leftContent, logo, className
|
|
|
51
51
|
</div>
|
|
52
52
|
)
|
|
53
53
|
: children && (
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// No fixed width: the widget slot sizes to its content and the
|
|
55
|
+
// breadcrumb (w-full, above) takes the rest. A `w-64` cap here
|
|
56
|
+
// clipped consumers whose widget set outgrew 256px.
|
|
57
|
+
<div className="flex shrink-0 flex-row items-center justify-end gap-x-4 whitespace-nowrap">
|
|
58
|
+
{children}
|
|
56
59
|
</div>
|
|
57
60
|
)}
|
|
58
61
|
</div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useAtomValue } from "jotai";
|
|
4
4
|
import { HistoryIcon } from "lucide-react";
|
|
5
5
|
import { useTranslations } from "next-intl";
|
|
6
|
+
import { usePathname } from "next/navigation";
|
|
6
7
|
import { recentPagesAtom } from "../../atoms";
|
|
7
8
|
import {
|
|
8
9
|
DropdownMenu,
|
|
@@ -13,52 +14,82 @@ import {
|
|
|
13
14
|
DropdownMenuSeparator,
|
|
14
15
|
DropdownMenuTrigger,
|
|
15
16
|
Link,
|
|
17
|
+
SidebarMenuButton,
|
|
18
|
+
SidebarMenuItem,
|
|
16
19
|
useSidebar,
|
|
17
20
|
} from "../../shadcnui";
|
|
18
21
|
|
|
22
|
+
/**
|
|
23
|
+
* A complete sidebar row. It renders its own SidebarMenuItem and owns its
|
|
24
|
+
* trigger; consumers drop it into a SidebarMenu and add nothing around it.
|
|
25
|
+
*
|
|
26
|
+
* The trigger IS the SidebarMenuButton. Wrapping this component in a
|
|
27
|
+
* consumer-owned SidebarMenuButton instead — which four apps used to do —
|
|
28
|
+
* leaves the real trigger as an inline element inside a button that
|
|
29
|
+
* `group-data-[collapsible=icon]:size-8! p-2! overflow-hidden` clamps to a
|
|
30
|
+
* 16px content box once the rail collapses. The trigger shrinks to zero width
|
|
31
|
+
* behind the consumer's own icon, and the icon the user clicks belongs to a
|
|
32
|
+
* button with no handler.
|
|
33
|
+
*/
|
|
19
34
|
export function RecentPagesNavigator() {
|
|
20
35
|
const recentPages = useAtomValue(recentPagesAtom);
|
|
21
36
|
const t = useTranslations();
|
|
22
|
-
const {
|
|
37
|
+
const { isMobile } = useSidebar();
|
|
38
|
+
const pathname = usePathname();
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
// The page the user is standing on is not somewhere to navigate back to, so
|
|
41
|
+
// it is hidden while they are there. Filtering happens here rather than in
|
|
42
|
+
// usePageTracker so the page stays recorded and reappears at the top the
|
|
43
|
+
// moment they leave. A sub-path counts as being on the entity: the
|
|
44
|
+
// /campaigns/:id entry is hidden while the user is at /campaigns/:id/canvas.
|
|
45
|
+
//
|
|
46
|
+
// usePathname is typed non-nullable but is not trusted to be — usePageTracker
|
|
47
|
+
// guards it the same way. With no path known, hide nothing rather than
|
|
48
|
+
// throwing inside a component that renders on every page.
|
|
49
|
+
const isCurrentPage = (url: string) => !!pathname && (pathname === url || pathname.startsWith(`${url}/`));
|
|
50
|
+
|
|
51
|
+
const pages = recentPages.filter((page) => !isCurrentPage(page.url));
|
|
52
|
+
|
|
53
|
+
if (pages.length === 0) return null;
|
|
27
54
|
|
|
28
55
|
return (
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
|
|
56
|
+
<SidebarMenuItem>
|
|
57
|
+
<DropdownMenu>
|
|
58
|
+
{/* No collapsed/expanded branch: SidebarMenuButton's own `tooltip` prop
|
|
59
|
+
renders exactly when the rail is collapsed on desktop, and the label
|
|
60
|
+
is clipped by the button's overflow-hidden like every other row. */}
|
|
61
|
+
<DropdownMenuTrigger
|
|
62
|
+
data-testid="sidebar-recent-pages"
|
|
63
|
+
render={
|
|
64
|
+
<SidebarMenuButton
|
|
65
|
+
className="text-muted-foreground hover:bg-muted/50 cursor-pointer"
|
|
66
|
+
tooltip={t(`common.recent_pages`)}
|
|
67
|
+
/>
|
|
68
|
+
}
|
|
69
|
+
>
|
|
70
|
+
<HistoryIcon />
|
|
71
|
+
<span>{t(`common.recent_pages`)}</span>
|
|
72
|
+
</DropdownMenuTrigger>
|
|
73
|
+
<DropdownMenuContent side={isMobile ? "bottom" : "right"} align="start" className="w-96">
|
|
74
|
+
<DropdownMenuGroup>
|
|
75
|
+
<DropdownMenuLabel>{t(`common.recent_pages`)}</DropdownMenuLabel>
|
|
76
|
+
<DropdownMenuSeparator />
|
|
77
|
+
{pages.map((page, index) => (
|
|
78
|
+
<DropdownMenuItem
|
|
79
|
+
key={`${page.url}-${index}`}
|
|
80
|
+
render={<Link href={page.url} className="flex items-center gap-2" />}
|
|
81
|
+
>
|
|
51
82
|
<div className="flex flex-col">
|
|
52
|
-
<div className="truncate
|
|
83
|
+
<div className="truncate">{page.title}</div>
|
|
53
84
|
<div className="text-muted-foreground text-xs font-normal">
|
|
54
85
|
{t(`entities.${page.moduleType}`, { count: 1 })}
|
|
55
86
|
</div>
|
|
56
87
|
</div>
|
|
57
|
-
</
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
</
|
|
61
|
-
</
|
|
62
|
-
</
|
|
88
|
+
</DropdownMenuItem>
|
|
89
|
+
))}
|
|
90
|
+
</DropdownMenuGroup>
|
|
91
|
+
</DropdownMenuContent>
|
|
92
|
+
</DropdownMenu>
|
|
93
|
+
</SidebarMenuItem>
|
|
63
94
|
);
|
|
64
95
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { createStore, Provider } from "jotai";
|
|
4
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { recentPagesAtom, type RecentPage } from "../../../atoms";
|
|
6
|
+
import { configureI18n } from "../../../i18n";
|
|
7
|
+
import { SidebarMenu, SidebarProvider } from "../../../shadcnui";
|
|
8
|
+
import { RecentPagesNavigator } from "../RecentPagesNavigator";
|
|
9
|
+
|
|
10
|
+
// usePathname is globally mocked to "/" in vitest.setup.ts. Override it here so
|
|
11
|
+
// the current-page filter can be exercised. A spec-level vi.mock takes
|
|
12
|
+
// precedence over the setup-file mock for this module.
|
|
13
|
+
const pathnameRef: { current: string | null } = { current: "/" };
|
|
14
|
+
vi.mock("next/navigation", () => ({
|
|
15
|
+
usePathname: () => pathnameRef.current,
|
|
16
|
+
useRouter: () => ({
|
|
17
|
+
push: vi.fn(),
|
|
18
|
+
replace: vi.fn(),
|
|
19
|
+
prefetch: vi.fn(),
|
|
20
|
+
back: vi.fn(),
|
|
21
|
+
forward: vi.fn(),
|
|
22
|
+
refresh: vi.fn(),
|
|
23
|
+
}),
|
|
24
|
+
useSearchParams: () => new URLSearchParams(),
|
|
25
|
+
useParams: () => ({}),
|
|
26
|
+
redirect: vi.fn(),
|
|
27
|
+
notFound: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
pathnameRef.current = "/";
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
// The package Link resolves its inner component at runtime and throws if i18n
|
|
36
|
+
// was never configured (src/i18n/config.ts).
|
|
37
|
+
configureI18n({
|
|
38
|
+
useRouter: () => ({
|
|
39
|
+
push: vi.fn(),
|
|
40
|
+
replace: vi.fn(),
|
|
41
|
+
back: vi.fn(),
|
|
42
|
+
forward: vi.fn(),
|
|
43
|
+
refresh: vi.fn(),
|
|
44
|
+
prefetch: vi.fn(),
|
|
45
|
+
}),
|
|
46
|
+
useTranslations: () => (key: string) => key,
|
|
47
|
+
usePathname: () => "/",
|
|
48
|
+
Link: ({ href, children, ...rest }: { href: string; children: React.ReactNode; [key: string]: any }) => (
|
|
49
|
+
<a href={href} {...rest}>
|
|
50
|
+
{children}
|
|
51
|
+
</a>
|
|
52
|
+
),
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const PAGES: RecentPage[] = [
|
|
57
|
+
{ url: "/npcs/npc-1", title: "Charlie", moduleType: "npcs", timestamp: 2 },
|
|
58
|
+
{ url: "/campaigns/camp-1", title: "Venezia Obscura", moduleType: "campaigns", timestamp: 1 },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
function renderNavigator({
|
|
62
|
+
pages = PAGES,
|
|
63
|
+
open = true,
|
|
64
|
+
pathname = "/",
|
|
65
|
+
}: { pages?: RecentPage[]; open?: boolean; pathname?: string | null } = {}) {
|
|
66
|
+
pathnameRef.current = pathname;
|
|
67
|
+
const store = createStore();
|
|
68
|
+
store.set(recentPagesAtom, pages);
|
|
69
|
+
return render(
|
|
70
|
+
<Provider store={store}>
|
|
71
|
+
<SidebarProvider open={open}>
|
|
72
|
+
<SidebarMenu>
|
|
73
|
+
<RecentPagesNavigator />
|
|
74
|
+
</SidebarMenu>
|
|
75
|
+
</SidebarProvider>
|
|
76
|
+
</Provider>,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("RecentPagesNavigator", () => {
|
|
81
|
+
it("renders nothing when no pages have been recorded", () => {
|
|
82
|
+
renderNavigator({ pages: [] });
|
|
83
|
+
expect(screen.queryByTestId("sidebar-recent-pages")).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("renders exactly one trigger button", () => {
|
|
87
|
+
// Regression: four apps wrapped this component in their own
|
|
88
|
+
// SidebarMenuButton and rendered a second HistoryIcon beside it. The
|
|
89
|
+
// wrapper had no handler, so the visible icon was dead in the collapsed
|
|
90
|
+
// rail. Exactly one button, and it must be the trigger.
|
|
91
|
+
renderNavigator();
|
|
92
|
+
const triggers = screen.getAllByTestId("sidebar-recent-pages");
|
|
93
|
+
expect(triggers).toHaveLength(1);
|
|
94
|
+
expect(triggers[0].tagName).toBe("BUTTON");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("opens the menu when the trigger is clicked in the collapsed rail", async () => {
|
|
98
|
+
renderNavigator({ open: false });
|
|
99
|
+
await userEvent.click(screen.getByTestId("sidebar-recent-pages"));
|
|
100
|
+
expect(await screen.findByText("Charlie")).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("opens the menu when the trigger is clicked while expanded", async () => {
|
|
104
|
+
renderNavigator({ open: true });
|
|
105
|
+
await userEvent.click(screen.getByTestId("sidebar-recent-pages"));
|
|
106
|
+
expect(await screen.findByText("Venezia Obscura")).toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("shows the entity name over its translated singular type", async () => {
|
|
110
|
+
renderNavigator();
|
|
111
|
+
await userEvent.click(screen.getByTestId("sidebar-recent-pages"));
|
|
112
|
+
expect(await screen.findByText("Charlie")).toBeInTheDocument();
|
|
113
|
+
// The i18n mock echoes the key back, so the second line is the raw key.
|
|
114
|
+
expect(screen.getByText("entities.npcs")).toBeInTheDocument();
|
|
115
|
+
expect(screen.getByText("entities.campaigns")).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("makes each entry an anchor pointing at the recorded url", async () => {
|
|
119
|
+
renderNavigator();
|
|
120
|
+
await userEvent.click(screen.getByTestId("sidebar-recent-pages"));
|
|
121
|
+
const link = (await screen.findByText("Charlie")).closest("a");
|
|
122
|
+
expect(link).not.toBeNull();
|
|
123
|
+
expect(link).toHaveAttribute("href", "/npcs/npc-1");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("hides the entry for the page the user is currently on", async () => {
|
|
127
|
+
renderNavigator({ pathname: "/npcs/npc-1" });
|
|
128
|
+
await userEvent.click(screen.getByTestId("sidebar-recent-pages"));
|
|
129
|
+
expect(await screen.findByText("Venezia Obscura")).toBeInTheDocument();
|
|
130
|
+
expect(screen.queryByText("Charlie")).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("hides the entry for the entity whose sub-page the user is on", async () => {
|
|
134
|
+
renderNavigator({ pathname: "/campaigns/camp-1/canvas" });
|
|
135
|
+
await userEvent.click(screen.getByTestId("sidebar-recent-pages"));
|
|
136
|
+
expect(await screen.findByText("Charlie")).toBeInTheDocument();
|
|
137
|
+
expect(screen.queryByText("Venezia Obscura")).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("does not hide an entry whose url is merely a string prefix of the current path", async () => {
|
|
141
|
+
// "/npcs/npc-1" must not be hidden while on "/npcs/npc-10".
|
|
142
|
+
renderNavigator({
|
|
143
|
+
pages: [{ url: "/npcs/npc-1", title: "Charlie", moduleType: "npcs", timestamp: 1 }],
|
|
144
|
+
pathname: "/npcs/npc-10",
|
|
145
|
+
});
|
|
146
|
+
await userEvent.click(screen.getByTestId("sidebar-recent-pages"));
|
|
147
|
+
expect(await screen.findByText("Charlie")).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("renders nothing when the only recorded page is the one being viewed", () => {
|
|
151
|
+
renderNavigator({
|
|
152
|
+
pages: [{ url: "/npcs/npc-1", title: "Charlie", moduleType: "npcs", timestamp: 1 }],
|
|
153
|
+
pathname: "/npcs/npc-1",
|
|
154
|
+
});
|
|
155
|
+
expect(screen.queryByTestId("sidebar-recent-pages")).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("hides nothing when the pathname is unavailable", async () => {
|
|
159
|
+
// usePathname is typed non-nullable but can return null in practice; the
|
|
160
|
+
// sidebar must degrade rather than throw.
|
|
161
|
+
renderNavigator({ pathname: null as unknown as string });
|
|
162
|
+
await userEvent.click(screen.getByTestId("sidebar-recent-pages"));
|
|
163
|
+
expect(await screen.findByText("Charlie")).toBeInTheDocument();
|
|
164
|
+
expect(screen.getByText("Venezia Obscura")).toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
});
|