@oh-my-pi/omp-stats 16.0.4 → 16.0.6
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/CHANGELOG.md +15 -0
- package/build.ts +11 -0
- package/dist/client/index.css +1 -1
- package/dist/client/index.html +11 -0
- package/dist/client/index.js +108 -108
- package/dist/client/styles.css +1070 -631
- package/dist/types/client/api.d.ts +19 -10
- package/dist/types/client/app/AppLayout.d.ts +16 -0
- package/dist/types/client/app/NavRail.d.ts +7 -0
- package/dist/types/client/app/RangeControl.d.ts +7 -0
- package/dist/types/client/app/SyncButton.d.ts +14 -0
- package/dist/types/client/app/ThemeToggle.d.ts +1 -0
- package/dist/types/client/app/TopBar.d.ts +15 -0
- package/dist/types/client/app/routes.d.ts +12 -0
- package/dist/types/client/components/chart-shared.d.ts +26 -40
- package/dist/types/client/components/models-table-shared.d.ts +20 -40
- package/dist/types/client/data/charts.d.ts +1 -0
- package/dist/types/client/data/formatters.d.ts +7 -0
- package/dist/types/client/data/useHashRoute.d.ts +8 -0
- package/dist/types/client/data/useResource.d.ts +13 -0
- package/dist/types/client/data/view-models.d.ts +37 -0
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/routes/BehaviorRoute.d.ts +7 -0
- package/dist/types/client/routes/CostsRoute.d.ts +7 -0
- package/dist/types/client/routes/ErrorsRoute.d.ts +8 -0
- package/dist/types/client/routes/ModelsRoute.d.ts +7 -0
- package/dist/types/client/routes/OverviewRoute.d.ts +8 -0
- package/dist/types/client/routes/ProjectsRoute.d.ts +7 -0
- package/dist/types/client/routes/RequestsRoute.d.ts +8 -0
- package/dist/types/client/routes/index.d.ts +7 -0
- package/dist/types/client/ui/AsyncBoundary.d.ts +12 -0
- package/dist/types/client/ui/DataTable.d.ts +17 -0
- package/dist/types/client/ui/EmptyState.d.ts +7 -0
- package/dist/types/client/ui/ErrorState.d.ts +6 -0
- package/dist/types/client/ui/JsonBlock.d.ts +7 -0
- package/dist/types/client/ui/MetricCluster.d.ts +5 -0
- package/dist/types/client/ui/Panel.d.ts +7 -0
- package/dist/types/client/ui/RequestDrawer.d.ts +5 -0
- package/dist/types/client/ui/SegmentedControl.d.ts +12 -0
- package/dist/types/client/ui/Skeleton.d.ts +8 -0
- package/dist/types/client/ui/StatusPill.d.ts +7 -0
- package/dist/types/client/ui/index.d.ts +11 -0
- package/dist/types/client/useSystemTheme.d.ts +9 -0
- package/package.json +4 -4
- package/src/aggregator.ts +4 -3
- package/src/client/App.tsx +89 -207
- package/src/client/api.ts +55 -37
- package/src/client/app/AppLayout.tsx +93 -0
- package/src/client/app/NavRail.tsx +44 -0
- package/src/client/app/RangeControl.tsx +39 -0
- package/src/client/app/SyncButton.tsx +75 -0
- package/src/client/app/ThemeToggle.tsx +37 -0
- package/src/client/app/TopBar.tsx +73 -0
- package/src/client/app/routes.ts +50 -0
- package/src/client/components/chart-shared.tsx +28 -91
- package/src/client/components/models-table-shared.tsx +9 -29
- package/src/client/components/range-meta.ts +3 -2
- package/src/client/data/charts.ts +14 -0
- package/src/client/data/formatters.ts +38 -0
- package/src/client/data/useHashRoute.ts +85 -0
- package/src/client/data/useResource.ts +154 -0
- package/src/client/data/view-models.ts +178 -0
- package/src/client/index.tsx +4 -0
- package/src/client/routes/BehaviorRoute.tsx +623 -0
- package/src/client/routes/CostsRoute.tsx +234 -0
- package/src/client/routes/ErrorsRoute.tsx +118 -0
- package/src/client/routes/ModelsRoute.tsx +430 -0
- package/src/client/routes/OverviewRoute.tsx +332 -0
- package/src/client/routes/ProjectsRoute.tsx +163 -0
- package/src/client/routes/RequestsRoute.tsx +123 -0
- package/src/client/routes/index.ts +7 -0
- package/src/client/styles.css +1242 -225
- package/src/client/ui/AsyncBoundary.tsx +54 -0
- package/src/client/ui/DataTable.tsx +122 -0
- package/src/client/ui/EmptyState.tsx +16 -0
- package/src/client/ui/ErrorState.tsx +25 -0
- package/src/client/ui/JsonBlock.tsx +75 -0
- package/src/client/ui/MetricCluster.tsx +67 -0
- package/src/client/ui/Panel.tsx +24 -0
- package/src/client/ui/RequestDrawer.tsx +208 -0
- package/src/client/ui/SegmentedControl.tsx +36 -0
- package/src/client/ui/Skeleton.tsx +17 -0
- package/src/client/ui/StatusPill.tsx +15 -0
- package/src/client/ui/index.ts +11 -0
- package/src/client/useSystemTheme.ts +73 -17
- package/dist/types/client/components/BehaviorChart.d.ts +0 -6
- package/dist/types/client/components/BehaviorModelsTable.d.ts +0 -7
- package/dist/types/client/components/BehaviorSummary.d.ts +0 -7
- package/dist/types/client/components/ChartsContainer.d.ts +0 -7
- package/dist/types/client/components/CostChart.d.ts +0 -6
- package/dist/types/client/components/CostSummary.d.ts +0 -6
- package/dist/types/client/components/Header.d.ts +0 -12
- package/dist/types/client/components/ModelsTable.d.ts +0 -8
- package/dist/types/client/components/RequestDetail.d.ts +0 -6
- package/dist/types/client/components/RequestList.d.ts +0 -8
- package/dist/types/client/components/StatsGrid.d.ts +0 -6
- package/src/client/components/BehaviorChart.tsx +0 -189
- package/src/client/components/BehaviorModelsTable.tsx +0 -342
- package/src/client/components/BehaviorSummary.tsx +0 -95
- package/src/client/components/ChartsContainer.tsx +0 -221
- package/src/client/components/CostChart.tsx +0 -171
- package/src/client/components/CostSummary.tsx +0 -53
- package/src/client/components/Header.tsx +0 -72
- package/src/client/components/ModelsTable.tsx +0 -265
- package/src/client/components/RequestDetail.tsx +0 -172
- package/src/client/components/RequestList.tsx +0 -73
- package/src/client/components/StatsGrid.tsx +0 -135
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { Plugin } from "chart.js";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { Bar, Line } from "react-chartjs-2";
|
|
4
|
+
import { getCostDashboardStats } from "../api";
|
|
5
|
+
import {
|
|
6
|
+
barDatasetStyle,
|
|
7
|
+
buildAggregateTimeSeries,
|
|
8
|
+
buildSharedPlugins,
|
|
9
|
+
buildSharedScales,
|
|
10
|
+
buildTopNByModelSeries,
|
|
11
|
+
CHART_THEMES,
|
|
12
|
+
lineDatasetStyle,
|
|
13
|
+
MODEL_COLORS,
|
|
14
|
+
styleDatasets,
|
|
15
|
+
} from "../components/chart-shared";
|
|
16
|
+
import { formatCost } from "../data/formatters";
|
|
17
|
+
import { useResource } from "../data/useResource";
|
|
18
|
+
import { buildCostSummary } from "../data/view-models";
|
|
19
|
+
import type { CostTimeSeriesPoint, TimeRange } from "../types";
|
|
20
|
+
import { AsyncBoundary, Panel, SegmentedControl } from "../ui";
|
|
21
|
+
import { useSystemTheme } from "../useSystemTheme";
|
|
22
|
+
|
|
23
|
+
export interface CostsRouteProps {
|
|
24
|
+
active: boolean;
|
|
25
|
+
range: TimeRange;
|
|
26
|
+
refreshTrigger: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function CostsRoute({ active, range, refreshTrigger }: CostsRouteProps) {
|
|
30
|
+
const {
|
|
31
|
+
data: costStats,
|
|
32
|
+
error,
|
|
33
|
+
loading,
|
|
34
|
+
} = useResource(["costs", range, refreshTrigger], signal => getCostDashboardStats(range, signal), {
|
|
35
|
+
pollMs: 30000,
|
|
36
|
+
enabled: active,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="stats-route-container space-y-6">
|
|
41
|
+
<AsyncBoundary loading={loading} error={error} data={costStats}>
|
|
42
|
+
{costStats && (
|
|
43
|
+
<>
|
|
44
|
+
<CostOverviewPanel costSeries={costStats.costSeries} />
|
|
45
|
+
<CostTrendPanel costSeries={costStats.costSeries} />
|
|
46
|
+
</>
|
|
47
|
+
)}
|
|
48
|
+
</AsyncBoundary>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function CostOverviewPanel({ costSeries }: { costSeries: CostTimeSeriesPoint[] }) {
|
|
54
|
+
const summary = useMemo(() => buildCostSummary(costSeries), [costSeries]);
|
|
55
|
+
|
|
56
|
+
const cards = [
|
|
57
|
+
{ label: "Total Cost", value: formatCost(summary.totalCost) },
|
|
58
|
+
{ label: "Average / Day", value: formatCost(summary.avgDailyCost) },
|
|
59
|
+
{
|
|
60
|
+
label: "Top Model",
|
|
61
|
+
value: summary.topModelName || "—",
|
|
62
|
+
sub: summary.topModelName ? formatCost(summary.topModelCost) : undefined,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
68
|
+
{cards.map(card => (
|
|
69
|
+
<Panel key={card.label} className="stats-cost-overview-card py-4 px-5">
|
|
70
|
+
<p className="text-xs stats-text-muted mb-1 font-medium uppercase tracking-wider">{card.label}</p>
|
|
71
|
+
<p className="text-2xl font-bold stats-text-primary truncate" title={card.value}>
|
|
72
|
+
{card.value}
|
|
73
|
+
</p>
|
|
74
|
+
{card.sub && <p className="text-xs stats-text-muted mt-1 font-medium">Total spent: {card.sub}</p>}
|
|
75
|
+
</Panel>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const BAR_LABEL_COLORS = {
|
|
82
|
+
dark: "rgba(248, 250, 252, 0.7)",
|
|
83
|
+
light: "rgba(15, 23, 42, 0.6)",
|
|
84
|
+
} as const;
|
|
85
|
+
|
|
86
|
+
// Inline Chart.js plugin to draw cost value above bars
|
|
87
|
+
function makeBarLabelPlugin(color: string): Plugin<"bar"> {
|
|
88
|
+
return {
|
|
89
|
+
id: "costBarLabels",
|
|
90
|
+
afterDatasetsDraw(chart) {
|
|
91
|
+
const { ctx } = chart;
|
|
92
|
+
const dataset = chart.data.datasets[0];
|
|
93
|
+
if (!dataset) return;
|
|
94
|
+
const meta = chart.getDatasetMeta(0);
|
|
95
|
+
ctx.save();
|
|
96
|
+
ctx.font = "11px system-ui, sans-serif";
|
|
97
|
+
ctx.fillStyle = color;
|
|
98
|
+
ctx.textAlign = "center";
|
|
99
|
+
ctx.textBaseline = "bottom";
|
|
100
|
+
for (const bar of meta.data) {
|
|
101
|
+
// Accessing Chart.js internal parsed coordinates via unknown cast
|
|
102
|
+
const value = (bar as unknown as { $context: { parsed: { y: number } } }).$context.parsed.y;
|
|
103
|
+
if (!value) continue;
|
|
104
|
+
const label = `$${Math.round(value)}`;
|
|
105
|
+
// Accessing internal getProps for positioning via unknown cast
|
|
106
|
+
const { x, y } = bar.getProps(["x", "y"], true) as {
|
|
107
|
+
x: number;
|
|
108
|
+
y: number;
|
|
109
|
+
};
|
|
110
|
+
ctx.fillText(label, x, y - 3);
|
|
111
|
+
}
|
|
112
|
+
ctx.restore();
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function CostTrendPanel({ costSeries }: { costSeries: CostTimeSeriesPoint[] }) {
|
|
118
|
+
const [byModel, setByModel] = useState(false);
|
|
119
|
+
const theme = useSystemTheme();
|
|
120
|
+
const chartTheme = CHART_THEMES[theme];
|
|
121
|
+
|
|
122
|
+
const chartData = useMemo(() => {
|
|
123
|
+
if (byModel) {
|
|
124
|
+
return buildTopNByModelSeries<CostTimeSeriesPoint, { total: number }>(costSeries, {
|
|
125
|
+
rankWeight: point => point.cost,
|
|
126
|
+
initBucket: () => ({ total: 0 }),
|
|
127
|
+
accumulate: (bucket, point) => {
|
|
128
|
+
bucket.total += point.cost;
|
|
129
|
+
},
|
|
130
|
+
bucketToValue: bucket => bucket.total,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return buildAggregateTimeSeries<CostTimeSeriesPoint, { total: number }>(costSeries, "Cost", {
|
|
134
|
+
initBucket: () => ({ total: 0 }),
|
|
135
|
+
accumulate: (bucket, point) => {
|
|
136
|
+
bucket.total += point.cost;
|
|
137
|
+
},
|
|
138
|
+
bucketToValue: bucket => bucket.total,
|
|
139
|
+
});
|
|
140
|
+
}, [costSeries, byModel]);
|
|
141
|
+
|
|
142
|
+
const sharedPlugins = useMemo(() => {
|
|
143
|
+
return buildSharedPlugins({
|
|
144
|
+
chartTheme,
|
|
145
|
+
showLegend: byModel,
|
|
146
|
+
defaultLabel: "Cost",
|
|
147
|
+
formatValue: v => `$${v.toFixed(2)}`,
|
|
148
|
+
footer: items => {
|
|
149
|
+
if (!byModel || items.length < 2) return undefined;
|
|
150
|
+
const total = items.reduce((sum, item) => sum + (item.parsed.y ?? 0), 0);
|
|
151
|
+
return `Total: $${total.toFixed(2)}`;
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}, [chartTheme, byModel]);
|
|
155
|
+
|
|
156
|
+
const { sharedScaleBase, yScale } = useMemo(() => {
|
|
157
|
+
return buildSharedScales({
|
|
158
|
+
chartTheme,
|
|
159
|
+
formatY: v => `$${Math.round(v)}`,
|
|
160
|
+
});
|
|
161
|
+
}, [chartTheme]);
|
|
162
|
+
|
|
163
|
+
const barLabelPlugin = useMemo(() => {
|
|
164
|
+
return makeBarLabelPlugin(BAR_LABEL_COLORS[theme]);
|
|
165
|
+
}, [theme]);
|
|
166
|
+
|
|
167
|
+
const lineData = useMemo(() => {
|
|
168
|
+
if (!byModel) return null;
|
|
169
|
+
return {
|
|
170
|
+
labels: chartData.labels,
|
|
171
|
+
datasets: styleDatasets(chartData, i => lineDatasetStyle(MODEL_COLORS[i % MODEL_COLORS.length])),
|
|
172
|
+
};
|
|
173
|
+
}, [chartData, byModel]);
|
|
174
|
+
|
|
175
|
+
const lineOptions = useMemo(() => {
|
|
176
|
+
return {
|
|
177
|
+
responsive: true,
|
|
178
|
+
maintainAspectRatio: false,
|
|
179
|
+
interaction: { mode: "index" as const, intersect: false },
|
|
180
|
+
plugins: sharedPlugins,
|
|
181
|
+
scales: { x: sharedScaleBase, y: yScale },
|
|
182
|
+
};
|
|
183
|
+
}, [sharedPlugins, sharedScaleBase, yScale]);
|
|
184
|
+
|
|
185
|
+
const barData = useMemo(() => {
|
|
186
|
+
if (byModel) return null;
|
|
187
|
+
return {
|
|
188
|
+
labels: chartData.labels,
|
|
189
|
+
datasets: styleDatasets(chartData, i => barDatasetStyle(MODEL_COLORS[i % MODEL_COLORS.length])),
|
|
190
|
+
};
|
|
191
|
+
}, [chartData, byModel]);
|
|
192
|
+
|
|
193
|
+
const barOptions = useMemo(() => {
|
|
194
|
+
return {
|
|
195
|
+
responsive: true,
|
|
196
|
+
maintainAspectRatio: false,
|
|
197
|
+
interaction: { mode: "index" as const, intersect: false },
|
|
198
|
+
plugins: {
|
|
199
|
+
...sharedPlugins,
|
|
200
|
+
costBarLabels: {},
|
|
201
|
+
},
|
|
202
|
+
scales: {
|
|
203
|
+
x: { ...sharedScaleBase, stacked: true },
|
|
204
|
+
y: { ...yScale, stacked: true },
|
|
205
|
+
},
|
|
206
|
+
layout: { padding: { top: 24 } },
|
|
207
|
+
};
|
|
208
|
+
}, [sharedPlugins, sharedScaleBase, yScale]);
|
|
209
|
+
|
|
210
|
+
const toggleOptions = [
|
|
211
|
+
{ value: false, label: "All Models" },
|
|
212
|
+
{ value: true, label: "By Model" },
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<Panel
|
|
217
|
+
title="Daily Cost"
|
|
218
|
+
subtitle="API spending over time"
|
|
219
|
+
actions={<SegmentedControl options={toggleOptions} value={byModel} onChange={setByModel} />}
|
|
220
|
+
>
|
|
221
|
+
<div className="h-[300px]">
|
|
222
|
+
{chartData.labels.length === 0 ? (
|
|
223
|
+
<div className="h-full flex items-center justify-center text-stats-muted text-sm">
|
|
224
|
+
No cost data available
|
|
225
|
+
</div>
|
|
226
|
+
) : byModel && lineData ? (
|
|
227
|
+
<Line data={lineData} options={lineOptions} />
|
|
228
|
+
) : barData ? (
|
|
229
|
+
<Bar data={barData} options={barOptions} plugins={[barLabelPlugin]} />
|
|
230
|
+
) : null}
|
|
231
|
+
</div>
|
|
232
|
+
</Panel>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { getRecentErrors } from "../api";
|
|
3
|
+
import { formatCost, formatInteger, formatRelativeTime } from "../data/formatters";
|
|
4
|
+
import { useResource } from "../data/useResource";
|
|
5
|
+
import type { MessageStats, TimeRange } from "../types";
|
|
6
|
+
import { AsyncBoundary, DataTable, Panel, StatusPill } from "../ui";
|
|
7
|
+
|
|
8
|
+
export interface ErrorsRouteProps {
|
|
9
|
+
active: boolean;
|
|
10
|
+
range: TimeRange;
|
|
11
|
+
refreshTrigger: number;
|
|
12
|
+
onRequestClick: (id: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ErrorsRoute({ active, refreshTrigger, onRequestClick }: ErrorsRouteProps) {
|
|
16
|
+
const {
|
|
17
|
+
data: recentErrors,
|
|
18
|
+
error,
|
|
19
|
+
loading,
|
|
20
|
+
} = useResource(["recent-errors-dense", refreshTrigger], signal => getRecentErrors(50, signal), {
|
|
21
|
+
pollMs: 30000,
|
|
22
|
+
enabled: active,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const columns = useMemo(
|
|
26
|
+
() => [
|
|
27
|
+
{
|
|
28
|
+
key: "model",
|
|
29
|
+
header: "Model",
|
|
30
|
+
render: (item: MessageStats) => (
|
|
31
|
+
<div>
|
|
32
|
+
<div className="stats-font-medium stats-text-primary">{item.model}</div>
|
|
33
|
+
<div className="stats-text-xs stats-text-muted">{item.provider}</div>
|
|
34
|
+
</div>
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "timestamp",
|
|
39
|
+
header: "Time",
|
|
40
|
+
render: (item: MessageStats) => formatRelativeTime(item.timestamp),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "errorMessage",
|
|
44
|
+
header: "Error Message",
|
|
45
|
+
render: (item: MessageStats) => (
|
|
46
|
+
<div
|
|
47
|
+
className="stats-text-xs stats-text-danger stats-truncate stats-max-w-md stats-font-mono"
|
|
48
|
+
title={item.errorMessage || ""}
|
|
49
|
+
>
|
|
50
|
+
{item.errorMessage || "Unknown error"}
|
|
51
|
+
</div>
|
|
52
|
+
),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "tokens",
|
|
56
|
+
header: "Tokens",
|
|
57
|
+
numeric: true,
|
|
58
|
+
render: (item: MessageStats) => formatInteger(item.usage.totalTokens),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "cost",
|
|
62
|
+
header: "Cost",
|
|
63
|
+
numeric: true,
|
|
64
|
+
render: (item: MessageStats) => formatCost(item.usage.cost.total, 4),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
[],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const renderMobileCard = (item: MessageStats, onClick?: () => void) => (
|
|
71
|
+
<div className="stats-mobile-card stats-border-danger" onClick={onClick}>
|
|
72
|
+
<div className="stats-mobile-card-header">
|
|
73
|
+
<div>
|
|
74
|
+
<div className="stats-font-semibold stats-text-primary">{item.model}</div>
|
|
75
|
+
<div className="stats-text-xs stats-text-muted">{item.provider}</div>
|
|
76
|
+
</div>
|
|
77
|
+
<StatusPill variant="danger">Failed</StatusPill>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="stats-mobile-card-grid">
|
|
80
|
+
<div>
|
|
81
|
+
<div className="stats-mobile-card-label">Time</div>
|
|
82
|
+
<div className="stats-mobile-card-value">{formatRelativeTime(item.timestamp)}</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div>
|
|
85
|
+
<div className="stats-mobile-card-label">Cost</div>
|
|
86
|
+
<div className="stats-mobile-card-value">{formatCost(item.usage.cost.total, 4)}</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div>
|
|
89
|
+
<div className="stats-mobile-card-label">Tokens</div>
|
|
90
|
+
<div className="stats-mobile-card-value">{formatInteger(item.usage.totalTokens)}</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
{item.errorMessage && <div className="stats-mobile-card-error mt-2 stats-font-mono">{item.errorMessage}</div>}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="stats-route-container">
|
|
99
|
+
<Panel title="Recent Errors" subtitle="Up to 50 most recent failed requests in the stats database">
|
|
100
|
+
<AsyncBoundary
|
|
101
|
+
loading={loading}
|
|
102
|
+
error={error}
|
|
103
|
+
data={recentErrors}
|
|
104
|
+
emptyText="No recent failures in the local stats database"
|
|
105
|
+
>
|
|
106
|
+
<DataTable
|
|
107
|
+
columns={columns}
|
|
108
|
+
data={recentErrors || []}
|
|
109
|
+
keyExtractor={item => item.id || `${item.sessionFile}-${item.entryId}`}
|
|
110
|
+
onRowClick={item => item.id && onRequestClick(item.id)}
|
|
111
|
+
renderMobileCard={renderMobileCard}
|
|
112
|
+
emptyText="No recent failures in the local stats database"
|
|
113
|
+
/>
|
|
114
|
+
</AsyncBoundary>
|
|
115
|
+
</Panel>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|