@checkstack/healthcheck-frontend 0.9.1 → 0.11.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/CHANGELOG.md +72 -0
- package/package.json +9 -9
- package/src/auto-charts/AutoChartGrid.tsx +7 -4
- package/src/auto-charts/SingleRunChartGrid.tsx +359 -0
- package/src/auto-charts/index.ts +1 -0
- package/src/auto-charts/schema-parser.ts +57 -11
- package/src/auto-charts/useStrategySchemas.ts +17 -0
- package/src/components/ExpandedResultView.tsx +17 -120
- package/src/components/HealthCheckSystemOverview.tsx +21 -36
- package/src/pages/HealthCheckHistoryDetailPage.tsx +19 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
# @checkstack/healthcheck-frontend
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 84dd430: ## Single Run Auto-Charts
|
|
8
|
+
|
|
9
|
+
Added `SingleRunChartGrid` component to display auto-generated charts for individual health check runs when viewing run history details.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- Renders charts based on the strategy's `resultSchema` metadata (same as aggregated charts)
|
|
14
|
+
- Supports all chart types: gauge, counter, boolean, text, status
|
|
15
|
+
- Groups fields by collector instance with assertion status display
|
|
16
|
+
- Updated `useStrategySchemas` hook to also return `resultSchema` for single-run visualization
|
|
17
|
+
|
|
18
|
+
### Changes
|
|
19
|
+
|
|
20
|
+
- Simplified `ExpandedResultView` to show only basic run metadata (status, latency, connection)
|
|
21
|
+
- Collector results and detailed data now displayed via `SingleRunChartGrid`
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- c842373: ## Animated Numbers & Availability Stats Live Updates
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
- **AnimatedNumber component** (`@checkstack/ui`): New reusable component that displays numbers with a smooth "rolling" animation when values change. Uses `requestAnimationFrame` with eased interpolation for a polished effect.
|
|
30
|
+
- **useAnimatedNumber hook** (`@checkstack/ui`): Underlying hook for the animation logic, can be used directly for custom implementations.
|
|
31
|
+
- **Live availability updates**: Availability stats (31-day and 365-day) now automatically refresh when new health check runs are received via signals.
|
|
32
|
+
|
|
33
|
+
### Usage
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { AnimatedNumber } from "@checkstack/ui";
|
|
37
|
+
|
|
38
|
+
<AnimatedNumber
|
|
39
|
+
value={99.95}
|
|
40
|
+
suffix="%"
|
|
41
|
+
decimals={2}
|
|
42
|
+
duration={500}
|
|
43
|
+
className="text-2xl font-bold text-green-500"
|
|
44
|
+
/>;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- c842373: ## Fix Counter Chart Multiplier Display
|
|
48
|
+
|
|
49
|
+
Hide redundant "(1×)" multiplier suffix for single-value counters in auto-charts. For aggregated counter values like "Errors", the displayed value itself represents the count, so showing "(1×)" adds no information and is confusing.
|
|
50
|
+
|
|
51
|
+
- Updated dependencies [c842373]
|
|
52
|
+
- @checkstack/ui@1.1.0
|
|
53
|
+
- @checkstack/auth-frontend@0.5.10
|
|
54
|
+
- @checkstack/dashboard-frontend@0.3.16
|
|
55
|
+
|
|
56
|
+
## 0.10.0
|
|
57
|
+
|
|
58
|
+
### Minor Changes
|
|
59
|
+
|
|
60
|
+
- 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
|
|
61
|
+
|
|
62
|
+
All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
|
|
63
|
+
|
|
64
|
+
**Key changes:**
|
|
65
|
+
|
|
66
|
+
- **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
|
|
67
|
+
- The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
|
|
68
|
+
- Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
|
|
69
|
+
- `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
|
|
70
|
+
- `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
|
|
71
|
+
- Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
|
|
72
|
+
|
|
73
|
+
**Breaking Change**: State objects now require `_type`. Merge functions automatically add `_type` to output. The bucket merging functions and `HealthCheckService` now require additional required parameters.
|
|
74
|
+
|
|
3
75
|
## 0.9.1
|
|
4
76
|
|
|
5
77
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"scripts": {
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
"lint:code": "eslint . --max-warnings 0"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@checkstack/auth-frontend": "0.5.
|
|
13
|
-
"@checkstack/catalog-common": "1.2.
|
|
14
|
-
"@checkstack/common": "0.6.
|
|
15
|
-
"@checkstack/dashboard-frontend": "0.3.
|
|
16
|
-
"@checkstack/frontend-api": "0.3.
|
|
17
|
-
"@checkstack/healthcheck-common": "0.8.
|
|
18
|
-
"@checkstack/signal-frontend": "0.0.
|
|
19
|
-
"@checkstack/ui": "0.
|
|
12
|
+
"@checkstack/auth-frontend": "0.5.9",
|
|
13
|
+
"@checkstack/catalog-common": "1.2.7",
|
|
14
|
+
"@checkstack/common": "0.6.2",
|
|
15
|
+
"@checkstack/dashboard-frontend": "0.3.15",
|
|
16
|
+
"@checkstack/frontend-api": "0.3.5",
|
|
17
|
+
"@checkstack/healthcheck-common": "0.8.2",
|
|
18
|
+
"@checkstack/signal-frontend": "0.0.12",
|
|
19
|
+
"@checkstack/ui": "1.0.0",
|
|
20
20
|
"ajv": "^8.17.1",
|
|
21
21
|
"ajv-formats": "^3.0.1",
|
|
22
22
|
"date-fns": "^4.1.0",
|
|
@@ -465,15 +465,18 @@ function CounterRenderer({ field, context }: ChartRendererProps) {
|
|
|
465
465
|
// Sort by count (descending) then by value
|
|
466
466
|
entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
467
467
|
|
|
468
|
-
// If there's only one unique value, show it prominently
|
|
468
|
+
// If there's only one unique value, show it prominently
|
|
469
|
+
// Only show count multiplier if > 1 (avoids confusing "(1×)" for aggregated counters)
|
|
469
470
|
if (entries.length === 1) {
|
|
470
471
|
const [value, count] = entries[0];
|
|
471
472
|
return (
|
|
472
473
|
<div className="text-2xl font-bold">
|
|
473
474
|
{value}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
475
|
+
{count > 1 && (
|
|
476
|
+
<span className="text-sm font-normal text-muted-foreground ml-2">
|
|
477
|
+
({count}×)
|
|
478
|
+
</span>
|
|
479
|
+
)}
|
|
477
480
|
</div>
|
|
478
481
|
);
|
|
479
482
|
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SingleRunChartGrid - Renders auto-generated charts for a single health check run.
|
|
3
|
+
*
|
|
4
|
+
* Unlike AutoChartGrid which shows time series data, this component displays
|
|
5
|
+
* static values from a single run's result/metadata.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ChartField } from "./schema-parser";
|
|
9
|
+
import { extractChartFields, getFieldValue } from "./schema-parser";
|
|
10
|
+
import { useStrategySchemas } from "./useStrategySchemas";
|
|
11
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@checkstack/ui";
|
|
12
|
+
import { RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
|
|
13
|
+
|
|
14
|
+
interface SingleRunChartGridProps {
|
|
15
|
+
/** Strategy ID (qualified, e.g., "healthcheck-http-backend.http") */
|
|
16
|
+
strategyId: string;
|
|
17
|
+
/** The run's result data containing metadata */
|
|
18
|
+
result: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Main component that renders a grid of charts for a single run.
|
|
23
|
+
*/
|
|
24
|
+
export function SingleRunChartGrid({
|
|
25
|
+
strategyId,
|
|
26
|
+
result,
|
|
27
|
+
}: SingleRunChartGridProps) {
|
|
28
|
+
const { schemas, loading } = useStrategySchemas(strategyId);
|
|
29
|
+
|
|
30
|
+
if (loading) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!schemas) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Use result schema for per-run data
|
|
39
|
+
const schema = schemas.resultSchema;
|
|
40
|
+
|
|
41
|
+
const schemaFields = extractChartFields(schema);
|
|
42
|
+
if (schemaFields.length === 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Get the metadata from the result
|
|
47
|
+
const metadata = result.metadata as Record<string, unknown> | undefined;
|
|
48
|
+
if (!metadata) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Discover collector instances from result data
|
|
53
|
+
const collectors = metadata.collectors as
|
|
54
|
+
| Record<string, Record<string, unknown>>
|
|
55
|
+
| undefined;
|
|
56
|
+
const collectorEntries = collectors ? Object.entries(collectors) : [];
|
|
57
|
+
|
|
58
|
+
// Separate strategy-level fields from collector fields
|
|
59
|
+
const strategyFields = schemaFields.filter((f) => !f.collectorId);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="space-y-6">
|
|
63
|
+
{/* Strategy-level fields */}
|
|
64
|
+
{strategyFields.length > 0 && (
|
|
65
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
66
|
+
{strategyFields.map((field) => (
|
|
67
|
+
<SingleValueCard
|
|
68
|
+
key={field.name}
|
|
69
|
+
field={field}
|
|
70
|
+
value={getFieldValue(metadata, field.name)}
|
|
71
|
+
/>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{/* Collector groups */}
|
|
77
|
+
{collectorEntries
|
|
78
|
+
.filter(([, collectorData]) => {
|
|
79
|
+
const collectorId = collectorData._collectorId as string | undefined;
|
|
80
|
+
if (!collectorId) return false;
|
|
81
|
+
const collectorFields = schemaFields.filter(
|
|
82
|
+
(f) => f.collectorId === collectorId,
|
|
83
|
+
);
|
|
84
|
+
return collectorFields.length > 0;
|
|
85
|
+
})
|
|
86
|
+
.map(([instanceId, collectorData]) => {
|
|
87
|
+
const collectorId = collectorData._collectorId as string;
|
|
88
|
+
const collectorFields = schemaFields.filter(
|
|
89
|
+
(f) => f.collectorId === collectorId,
|
|
90
|
+
);
|
|
91
|
+
return (
|
|
92
|
+
<CollectorSection
|
|
93
|
+
key={instanceId}
|
|
94
|
+
instanceId={instanceId}
|
|
95
|
+
collectorId={collectorId}
|
|
96
|
+
fields={collectorFields}
|
|
97
|
+
data={collectorData}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface CollectorSectionProps {
|
|
106
|
+
instanceId: string;
|
|
107
|
+
collectorId: string;
|
|
108
|
+
fields: ChartField[];
|
|
109
|
+
data: Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Section for a single collector instance.
|
|
114
|
+
*/
|
|
115
|
+
function CollectorSection({
|
|
116
|
+
instanceId,
|
|
117
|
+
collectorId,
|
|
118
|
+
fields,
|
|
119
|
+
data,
|
|
120
|
+
}: CollectorSectionProps) {
|
|
121
|
+
const displayName = collectorId.split(".").pop() || collectorId;
|
|
122
|
+
const assertionFailed = data._assertionFailed as string | undefined;
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="space-y-4">
|
|
126
|
+
<div className="flex items-center gap-2">
|
|
127
|
+
<h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
|
|
128
|
+
{displayName}
|
|
129
|
+
</h4>
|
|
130
|
+
<span className="text-xs text-muted-foreground">
|
|
131
|
+
({instanceId.slice(0, 8)})
|
|
132
|
+
</span>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Assertion status if present */}
|
|
136
|
+
{assertionFailed && (
|
|
137
|
+
<Card className="border-red-200 dark:border-red-900">
|
|
138
|
+
<CardHeader className="pb-2">
|
|
139
|
+
<CardTitle className="text-sm font-medium text-red-600">
|
|
140
|
+
Assertion Failed
|
|
141
|
+
</CardTitle>
|
|
142
|
+
</CardHeader>
|
|
143
|
+
<CardContent>
|
|
144
|
+
<div className="text-sm text-red-600 bg-red-50 dark:bg-red-950 px-2 py-1 rounded">
|
|
145
|
+
{assertionFailed}
|
|
146
|
+
</div>
|
|
147
|
+
</CardContent>
|
|
148
|
+
</Card>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
152
|
+
{fields.map((field) => (
|
|
153
|
+
<SingleValueCard
|
|
154
|
+
key={field.name}
|
|
155
|
+
field={field}
|
|
156
|
+
value={getFieldValue(data, field.name)}
|
|
157
|
+
/>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface SingleValueCardProps {
|
|
165
|
+
field: ChartField;
|
|
166
|
+
value: unknown;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Card displaying a single value based on its chart type.
|
|
171
|
+
*/
|
|
172
|
+
function SingleValueCard({ field, value }: SingleValueCardProps) {
|
|
173
|
+
return (
|
|
174
|
+
<Card>
|
|
175
|
+
<CardHeader className="pb-2">
|
|
176
|
+
<CardTitle className="text-sm font-medium">{field.label}</CardTitle>
|
|
177
|
+
</CardHeader>
|
|
178
|
+
<CardContent>
|
|
179
|
+
<SingleValueRenderer field={field} value={value} />
|
|
180
|
+
</CardContent>
|
|
181
|
+
</Card>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
interface SingleValueRendererProps {
|
|
186
|
+
field: ChartField;
|
|
187
|
+
value: unknown;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Dispatches to appropriate renderer based on chart type.
|
|
192
|
+
*/
|
|
193
|
+
function SingleValueRenderer({ field, value }: SingleValueRendererProps) {
|
|
194
|
+
switch (field.chartType) {
|
|
195
|
+
case "line":
|
|
196
|
+
case "counter": {
|
|
197
|
+
return <NumberRenderer value={value} unit={field.unit} />;
|
|
198
|
+
}
|
|
199
|
+
case "gauge": {
|
|
200
|
+
return <GaugeRenderer value={value} unit={field.unit} />;
|
|
201
|
+
}
|
|
202
|
+
case "boolean": {
|
|
203
|
+
return <BooleanRenderer value={value} />;
|
|
204
|
+
}
|
|
205
|
+
case "text": {
|
|
206
|
+
return <TextRenderer value={value} />;
|
|
207
|
+
}
|
|
208
|
+
case "status": {
|
|
209
|
+
return <StatusRenderer value={value} />;
|
|
210
|
+
}
|
|
211
|
+
case "bar":
|
|
212
|
+
case "pie": {
|
|
213
|
+
// For bar/pie, just show the value since we can't do distributions with a single point
|
|
214
|
+
return <TextRenderer value={value} />;
|
|
215
|
+
}
|
|
216
|
+
default: {
|
|
217
|
+
return <TextRenderer value={value} />;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Renders a numeric value with optional unit.
|
|
224
|
+
*/
|
|
225
|
+
function NumberRenderer({ value, unit }: { value: unknown; unit?: string }) {
|
|
226
|
+
if (value === undefined || value === null) {
|
|
227
|
+
return <div className="text-muted-foreground">—</div>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const numValue = typeof value === "number" ? value : Number(value);
|
|
231
|
+
if (Number.isNaN(numValue)) {
|
|
232
|
+
return <div className="text-muted-foreground">{String(value)}</div>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const formatted = Number.isInteger(numValue)
|
|
236
|
+
? String(numValue)
|
|
237
|
+
: numValue.toFixed(2);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="text-2xl font-bold">
|
|
241
|
+
{formatted}
|
|
242
|
+
{unit && (
|
|
243
|
+
<span className="text-sm font-normal text-muted-foreground ml-1">
|
|
244
|
+
{unit}
|
|
245
|
+
</span>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Renders a percentage gauge visualization.
|
|
253
|
+
*/
|
|
254
|
+
function GaugeRenderer({ value, unit }: { value: unknown; unit?: string }) {
|
|
255
|
+
if (value === undefined || value === null) {
|
|
256
|
+
return <div className="text-muted-foreground">—</div>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const numValue = typeof value === "number" ? value : Number(value);
|
|
260
|
+
if (Number.isNaN(numValue)) {
|
|
261
|
+
return <div className="text-muted-foreground">{String(value)}</div>;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const clampedValue = Math.min(100, Math.max(0, numValue));
|
|
265
|
+
const displayUnit = unit ?? "%";
|
|
266
|
+
|
|
267
|
+
// Determine color based on value
|
|
268
|
+
const fillColor =
|
|
269
|
+
clampedValue >= 90
|
|
270
|
+
? "hsl(var(--success))"
|
|
271
|
+
: clampedValue >= 70
|
|
272
|
+
? "hsl(var(--warning))"
|
|
273
|
+
: "hsl(var(--destructive))";
|
|
274
|
+
|
|
275
|
+
const data = [{ name: "value", value: clampedValue, fill: fillColor }];
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<div className="flex items-center gap-3">
|
|
279
|
+
<ResponsiveContainer width={80} height={80}>
|
|
280
|
+
<RadialBarChart
|
|
281
|
+
cx="50%"
|
|
282
|
+
cy="50%"
|
|
283
|
+
innerRadius="60%"
|
|
284
|
+
outerRadius="100%"
|
|
285
|
+
barSize={8}
|
|
286
|
+
data={data}
|
|
287
|
+
startAngle={90}
|
|
288
|
+
endAngle={-270}
|
|
289
|
+
>
|
|
290
|
+
<RadialBar
|
|
291
|
+
dataKey="value"
|
|
292
|
+
cornerRadius={4}
|
|
293
|
+
background={{ fill: "hsl(var(--muted))" }}
|
|
294
|
+
/>
|
|
295
|
+
</RadialBarChart>
|
|
296
|
+
</ResponsiveContainer>
|
|
297
|
+
<div className="text-2xl font-bold" style={{ color: fillColor }}>
|
|
298
|
+
{clampedValue.toFixed(1)}
|
|
299
|
+
{displayUnit}
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Renders a boolean indicator.
|
|
307
|
+
*/
|
|
308
|
+
function BooleanRenderer({ value }: { value: unknown }) {
|
|
309
|
+
if (value === undefined || value === null) {
|
|
310
|
+
return <div className="text-muted-foreground">—</div>;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const boolValue = Boolean(value);
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<div className="flex items-center gap-2">
|
|
317
|
+
<div
|
|
318
|
+
className={`w-3 h-3 rounded-full ${
|
|
319
|
+
boolValue ? "bg-green-500" : "bg-red-500"
|
|
320
|
+
}`}
|
|
321
|
+
/>
|
|
322
|
+
<span className={boolValue ? "text-green-600" : "text-red-600"}>
|
|
323
|
+
{boolValue ? "Yes" : "No"}
|
|
324
|
+
</span>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Renders a text value.
|
|
331
|
+
*/
|
|
332
|
+
function TextRenderer({ value }: { value: unknown }) {
|
|
333
|
+
if (value === undefined || value === null || value === "") {
|
|
334
|
+
return <div className="text-muted-foreground">—</div>;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const strValue = String(value);
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<div className="text-sm font-mono truncate" title={strValue}>
|
|
341
|
+
{strValue}
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Renders an error/status badge.
|
|
348
|
+
*/
|
|
349
|
+
function StatusRenderer({ value }: { value: unknown }) {
|
|
350
|
+
if (value === undefined || value === null || value === "") {
|
|
351
|
+
return <div className="text-sm text-muted-foreground">No errors</div>;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<div className="text-sm text-red-600 bg-red-50 dark:bg-red-950 px-2 py-1 rounded truncate">
|
|
356
|
+
{String(value)}
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
}
|
package/src/auto-charts/index.ts
CHANGED
|
@@ -8,5 +8,6 @@
|
|
|
8
8
|
export { extractChartFields, getFieldValue } from "./schema-parser";
|
|
9
9
|
export type { ChartField } from "./schema-parser";
|
|
10
10
|
export { AutoChartGrid } from "./AutoChartGrid";
|
|
11
|
+
export { SingleRunChartGrid } from "./SingleRunChartGrid";
|
|
11
12
|
export { useStrategySchemas } from "./useStrategySchemas";
|
|
12
13
|
export { autoChartExtension } from "./extension";
|
|
@@ -17,8 +17,7 @@ import type {
|
|
|
17
17
|
* JSON Schema property with healthcheck result-specific x-* extensions.
|
|
18
18
|
* Uses the generic core type for proper recursive typing.
|
|
19
19
|
*/
|
|
20
|
-
export interface ResultSchemaProperty
|
|
21
|
-
extends JsonSchemaPropertyCore<ResultSchemaProperty> {
|
|
20
|
+
export interface ResultSchemaProperty extends JsonSchemaPropertyCore<ResultSchemaProperty> {
|
|
22
21
|
// Result-specific x-* extensions for chart rendering
|
|
23
22
|
"x-chart-type"?: ChartType;
|
|
24
23
|
"x-chart-label"?: string;
|
|
@@ -58,7 +57,7 @@ export interface ChartField {
|
|
|
58
57
|
* @returns Array of chart fields with metadata
|
|
59
58
|
*/
|
|
60
59
|
export function extractChartFields(
|
|
61
|
-
schema: Record<string, unknown> | null | undefined
|
|
60
|
+
schema: Record<string, unknown> | null | undefined,
|
|
62
61
|
): ChartField[] {
|
|
63
62
|
if (!schema) return [];
|
|
64
63
|
|
|
@@ -72,13 +71,13 @@ export function extractChartFields(
|
|
|
72
71
|
if (name === "collectors" && prop.type === "object" && prop.properties) {
|
|
73
72
|
// Traverse each collector's schema
|
|
74
73
|
for (const [collectorId, collectorProp] of Object.entries(
|
|
75
|
-
prop.properties
|
|
74
|
+
prop.properties,
|
|
76
75
|
)) {
|
|
77
76
|
if (collectorProp.type === "object" && collectorProp.properties) {
|
|
78
77
|
// Extract fields from the collector's result schema
|
|
79
78
|
const collectorFields = extractFieldsFromProperties(
|
|
80
79
|
collectorProp.properties,
|
|
81
|
-
collectorId
|
|
80
|
+
collectorId,
|
|
82
81
|
);
|
|
83
82
|
fields.push(...collectorFields);
|
|
84
83
|
}
|
|
@@ -100,7 +99,7 @@ export function extractChartFields(
|
|
|
100
99
|
*/
|
|
101
100
|
function extractFieldsFromProperties(
|
|
102
101
|
properties: Record<string, ResultSchemaProperty>,
|
|
103
|
-
collectorId: string
|
|
102
|
+
collectorId: string,
|
|
104
103
|
): ChartField[] {
|
|
105
104
|
const fields: ChartField[] = [];
|
|
106
105
|
|
|
@@ -126,7 +125,7 @@ function extractFieldsFromProperties(
|
|
|
126
125
|
*/
|
|
127
126
|
function extractSingleField(
|
|
128
127
|
name: string,
|
|
129
|
-
prop: ResultSchemaProperty
|
|
128
|
+
prop: ResultSchemaProperty,
|
|
130
129
|
): ChartField {
|
|
131
130
|
let schemaType = prop.type ?? "unknown";
|
|
132
131
|
if (prop.type === "array" && prop.items?.type) {
|
|
@@ -164,6 +163,7 @@ function formatFieldName(name: string): string {
|
|
|
164
163
|
/**
|
|
165
164
|
* Get the value for a field from a data object.
|
|
166
165
|
* For strategy-level fields, also searches inside collectors as fallback.
|
|
166
|
+
* Automatically extracts computed values from aggregated state objects.
|
|
167
167
|
*
|
|
168
168
|
* @param data - The metadata object
|
|
169
169
|
* @param fieldName - Simple field name (no dot notation for collector fields)
|
|
@@ -172,7 +172,7 @@ function formatFieldName(name: string): string {
|
|
|
172
172
|
export function getFieldValue(
|
|
173
173
|
data: Record<string, unknown> | undefined,
|
|
174
174
|
fieldName: string,
|
|
175
|
-
collectorInstanceId?: string
|
|
175
|
+
collectorInstanceId?: string,
|
|
176
176
|
): unknown {
|
|
177
177
|
if (!data) return undefined;
|
|
178
178
|
|
|
@@ -184,7 +184,7 @@ export function getFieldValue(
|
|
|
184
184
|
if (collectors && typeof collectors === "object") {
|
|
185
185
|
const collectorData = collectors[collectorInstanceId];
|
|
186
186
|
if (collectorData && typeof collectorData === "object") {
|
|
187
|
-
return collectorData[fieldName];
|
|
187
|
+
return extractComputedValue(collectorData[fieldName]);
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
return undefined;
|
|
@@ -193,7 +193,7 @@ export function getFieldValue(
|
|
|
193
193
|
// For non-collector fields, try direct lookup first
|
|
194
194
|
const directValue = data[fieldName];
|
|
195
195
|
if (directValue !== undefined) {
|
|
196
|
-
return directValue;
|
|
196
|
+
return extractComputedValue(directValue);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
// Fallback: search all collectors for the field (for strategy schema fields)
|
|
@@ -205,7 +205,7 @@ export function getFieldValue(
|
|
|
205
205
|
if (collectorData && typeof collectorData === "object") {
|
|
206
206
|
const value = collectorData[fieldName];
|
|
207
207
|
if (value !== undefined) {
|
|
208
|
-
return value;
|
|
208
|
+
return extractComputedValue(value);
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
}
|
|
@@ -213,3 +213,49 @@ export function getFieldValue(
|
|
|
213
213
|
|
|
214
214
|
return undefined;
|
|
215
215
|
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Extract the computed value from an aggregated state object.
|
|
219
|
+
* Uses the required `_type` discriminator field for type detection.
|
|
220
|
+
* Logs errors instead of throwing to avoid breaking the app.
|
|
221
|
+
*
|
|
222
|
+
* @param value - Value from API data (unknown type from JSON parsing)
|
|
223
|
+
*/
|
|
224
|
+
function extractComputedValue(value: unknown): unknown {
|
|
225
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const obj = value as Record<string, unknown>;
|
|
230
|
+
|
|
231
|
+
// _type is required for all aggregated state objects
|
|
232
|
+
if (!("_type" in obj)) {
|
|
233
|
+
console.error(
|
|
234
|
+
"[AutoChart] Missing _type discriminator in aggregated state:",
|
|
235
|
+
obj,
|
|
236
|
+
);
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
switch (obj._type) {
|
|
241
|
+
case "average": {
|
|
242
|
+
return obj.avg;
|
|
243
|
+
}
|
|
244
|
+
case "rate": {
|
|
245
|
+
return obj.rate;
|
|
246
|
+
}
|
|
247
|
+
case "counter": {
|
|
248
|
+
return obj.count;
|
|
249
|
+
}
|
|
250
|
+
case "minmax": {
|
|
251
|
+
// Default to max for minmax; caller can access min directly if needed
|
|
252
|
+
return obj.max;
|
|
253
|
+
}
|
|
254
|
+
default: {
|
|
255
|
+
console.error(
|
|
256
|
+
`[AutoChart] Unrecognized aggregated state type: ${String(obj._type)}`,
|
|
257
|
+
);
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -11,6 +11,7 @@ import { usePluginClient } from "@checkstack/frontend-api";
|
|
|
11
11
|
import { HealthCheckApi } from "../api";
|
|
12
12
|
|
|
13
13
|
interface StrategySchemas {
|
|
14
|
+
resultSchema: Record<string, unknown> | undefined;
|
|
14
15
|
aggregatedResultSchema: Record<string, unknown> | undefined;
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -65,7 +66,23 @@ export function useStrategySchemas(strategyId: string): {
|
|
|
65
66
|
collectorAggregatedProperties,
|
|
66
67
|
);
|
|
67
68
|
|
|
69
|
+
// Build collector result schemas for nesting under resultSchema.properties.collectors
|
|
70
|
+
const collectorResultProperties: Record<string, unknown> = {};
|
|
71
|
+
|
|
72
|
+
for (const collector of collectors) {
|
|
73
|
+
if (collector.resultSchema) {
|
|
74
|
+
collectorResultProperties[collector.id] = collector.resultSchema;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Merge collector result schemas into strategy result schema
|
|
79
|
+
const mergedResultSchema = mergeCollectorSchemas(
|
|
80
|
+
strategy.resultSchema as Record<string, unknown> | undefined,
|
|
81
|
+
collectorResultProperties,
|
|
82
|
+
);
|
|
83
|
+
|
|
68
84
|
setSchemas({
|
|
85
|
+
resultSchema: mergedResultSchema,
|
|
69
86
|
aggregatedResultSchema: mergedAggregatedSchema,
|
|
70
87
|
});
|
|
71
88
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ExpandedResultView - Displays health check result
|
|
2
|
+
* ExpandedResultView - Displays basic health check result metadata.
|
|
3
|
+
*
|
|
4
|
+
* Shows status, latency, and connection time. Collector results and
|
|
5
|
+
* detailed visualizations are handled by SingleRunChartGrid.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
interface ExpandedResultViewProps {
|
|
@@ -7,134 +10,28 @@ interface ExpandedResultViewProps {
|
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
|
-
* Displays
|
|
11
|
-
* Shows collector results as cards with key-value pairs.
|
|
13
|
+
* Displays basic run metadata (status, latency, connection time).
|
|
12
14
|
*/
|
|
13
15
|
export function ExpandedResultView({ result }: ExpandedResultViewProps) {
|
|
14
16
|
const metadata = result.metadata as Record<string, unknown> | undefined;
|
|
15
|
-
const rawCollectors = metadata?.collectors;
|
|
16
|
-
|
|
17
|
-
// Type guard for collectors object
|
|
18
|
-
const collectors: Record<string, Record<string, unknown>> | undefined =
|
|
19
|
-
rawCollectors &&
|
|
20
|
-
typeof rawCollectors === "object" &&
|
|
21
|
-
!Array.isArray(rawCollectors)
|
|
22
|
-
? (rawCollectors as Record<string, Record<string, unknown>>)
|
|
23
|
-
: undefined;
|
|
24
|
-
|
|
25
|
-
// Check if we have collectors to display
|
|
26
|
-
const collectorEntries = collectors ? Object.entries(collectors) : [];
|
|
27
|
-
|
|
28
|
-
// Extract connection time as typed value
|
|
29
17
|
const connectionTimeMs = metadata?.connectionTimeMs as number | undefined;
|
|
30
18
|
|
|
31
19
|
return (
|
|
32
|
-
<div className="
|
|
33
|
-
<div
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
<span className="font-medium">{String(result.status)}</span>
|
|
37
|
-
</div>
|
|
38
|
-
<div>
|
|
39
|
-
<span className="text-muted-foreground">Latency: </span>
|
|
40
|
-
<span className="font-medium">{String(result.latencyMs)}ms</span>
|
|
41
|
-
</div>
|
|
42
|
-
{connectionTimeMs !== undefined && (
|
|
43
|
-
<div>
|
|
44
|
-
<span className="text-muted-foreground">Connection: </span>
|
|
45
|
-
<span className="font-medium">{connectionTimeMs}ms</span>
|
|
46
|
-
</div>
|
|
47
|
-
)}
|
|
20
|
+
<div className="flex flex-wrap gap-4 text-sm">
|
|
21
|
+
<div>
|
|
22
|
+
<span className="text-muted-foreground">Status: </span>
|
|
23
|
+
<span className="font-medium">{String(result.status)}</span>
|
|
48
24
|
</div>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
collectorId={collectorId}
|
|
58
|
-
result={collectorResult}
|
|
59
|
-
/>
|
|
60
|
-
))}
|
|
61
|
-
</div>
|
|
25
|
+
<div>
|
|
26
|
+
<span className="text-muted-foreground">Latency: </span>
|
|
27
|
+
<span className="font-medium">{String(result.latencyMs)}ms</span>
|
|
28
|
+
</div>
|
|
29
|
+
{connectionTimeMs !== undefined && (
|
|
30
|
+
<div>
|
|
31
|
+
<span className="text-muted-foreground">Connection: </span>
|
|
32
|
+
<span className="font-medium">{connectionTimeMs}ms</span>
|
|
62
33
|
</div>
|
|
63
34
|
)}
|
|
64
|
-
|
|
65
|
-
{result.message ? (
|
|
66
|
-
<div className="text-sm text-muted-foreground">
|
|
67
|
-
{String(result.message)}
|
|
68
|
-
</div>
|
|
69
|
-
) : undefined}
|
|
70
|
-
</div>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface CollectorResultCardProps {
|
|
75
|
-
collectorId: string;
|
|
76
|
-
result: Record<string, unknown>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Card displaying a single collector's result values.
|
|
81
|
-
*/
|
|
82
|
-
function CollectorResultCard({
|
|
83
|
-
collectorId,
|
|
84
|
-
result,
|
|
85
|
-
}: CollectorResultCardProps) {
|
|
86
|
-
if (!result || typeof result !== "object") {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Filter out null/undefined values
|
|
91
|
-
const entries = Object.entries(result).filter(
|
|
92
|
-
([, value]) => value !== null && value !== undefined,
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<div className="rounded-md border bg-card p-3 space-y-2">
|
|
97
|
-
<h5 className="text-sm font-medium text-primary">{collectorId}</h5>
|
|
98
|
-
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
|
|
99
|
-
{entries.map(([key, value]) => (
|
|
100
|
-
<div key={key} className="contents">
|
|
101
|
-
<span className="text-muted-foreground truncate">
|
|
102
|
-
{formatKey(key)}
|
|
103
|
-
</span>
|
|
104
|
-
<span className="font-mono text-xs truncate" title={String(value)}>
|
|
105
|
-
{formatValue(value)}
|
|
106
|
-
</span>
|
|
107
|
-
</div>
|
|
108
|
-
))}
|
|
109
|
-
</div>
|
|
110
35
|
</div>
|
|
111
36
|
);
|
|
112
37
|
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Format a camelCase key to a readable label.
|
|
116
|
-
*/
|
|
117
|
-
function formatKey(key: string): string {
|
|
118
|
-
return key
|
|
119
|
-
.replaceAll(/([a-z])([A-Z])/g, "$1 $2")
|
|
120
|
-
.replace(/^./, (c) => c.toUpperCase());
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Format a value for display.
|
|
125
|
-
*/
|
|
126
|
-
function formatValue(value: unknown): string {
|
|
127
|
-
if (value === null || value === undefined) return "—";
|
|
128
|
-
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
129
|
-
if (typeof value === "number") {
|
|
130
|
-
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
131
|
-
}
|
|
132
|
-
if (Array.isArray(value)) {
|
|
133
|
-
return value.length > 3
|
|
134
|
-
? `[${value.slice(0, 3).join(", ")}…]`
|
|
135
|
-
: `[${value.join(", ")}]`;
|
|
136
|
-
}
|
|
137
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
138
|
-
const str = String(value);
|
|
139
|
-
return str.length > 50 ? `${str.slice(0, 47)}…` : str;
|
|
140
|
-
}
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
CardContent,
|
|
37
37
|
CardHeader,
|
|
38
38
|
CardTitle,
|
|
39
|
+
AnimatedNumber,
|
|
39
40
|
} from "@checkstack/ui";
|
|
40
41
|
import { formatDistanceToNow } from "date-fns";
|
|
41
42
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
@@ -70,28 +71,21 @@ interface ExpandedRowProps {
|
|
|
70
71
|
systemId: string;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
// Helper to
|
|
74
|
-
const
|
|
74
|
+
// Helper to get color class for availability percentage
|
|
75
|
+
const getAvailabilityColorClass = (
|
|
75
76
|
value: number | null,
|
|
76
77
|
totalRuns: number,
|
|
77
|
-
):
|
|
78
|
+
): string => {
|
|
78
79
|
if (value === null || totalRuns === 0) {
|
|
79
|
-
return
|
|
80
|
+
return "text-muted-foreground";
|
|
80
81
|
}
|
|
81
|
-
const formatted = value.toFixed(2) + "%";
|
|
82
82
|
if (value >= 99.9) {
|
|
83
|
-
return
|
|
84
|
-
text: formatted,
|
|
85
|
-
className: "text-green-600 dark:text-green-400",
|
|
86
|
-
};
|
|
83
|
+
return "text-green-600 dark:text-green-400";
|
|
87
84
|
}
|
|
88
85
|
if (value >= 99) {
|
|
89
|
-
return
|
|
90
|
-
text: formatted,
|
|
91
|
-
className: "text-yellow-600 dark:text-yellow-400",
|
|
92
|
-
};
|
|
86
|
+
return "text-yellow-600 dark:text-yellow-400";
|
|
93
87
|
}
|
|
94
|
-
return
|
|
88
|
+
return "text-red-600 dark:text-red-400";
|
|
95
89
|
};
|
|
96
90
|
|
|
97
91
|
const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
@@ -199,11 +193,12 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
199
193
|
}
|
|
200
194
|
const runs = displayRuns;
|
|
201
195
|
|
|
202
|
-
// Listen for realtime health check updates to refresh history table
|
|
196
|
+
// Listen for realtime health check updates to refresh history table and availability stats
|
|
203
197
|
// Charts are refreshed automatically by useHealthCheckData
|
|
204
198
|
useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
|
|
205
199
|
if (changedId === systemId) {
|
|
206
200
|
void refetch();
|
|
201
|
+
void refetchAvailability();
|
|
207
202
|
}
|
|
208
203
|
});
|
|
209
204
|
|
|
@@ -214,7 +209,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
214
209
|
: "Using default thresholds";
|
|
215
210
|
|
|
216
211
|
// Fetch availability stats
|
|
217
|
-
const { data: availabilityData } =
|
|
212
|
+
const { data: availabilityData, refetch: refetchAvailability } =
|
|
218
213
|
healthCheckClient.getAvailabilityStats.useQuery({
|
|
219
214
|
systemId,
|
|
220
215
|
configurationId: item.configurationId,
|
|
@@ -302,16 +297,11 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
302
297
|
31-Day Availability
|
|
303
298
|
</span>
|
|
304
299
|
<div className="flex items-baseline gap-2">
|
|
305
|
-
<
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
{
|
|
309
|
-
|
|
310
|
-
availabilityData.availability31Days,
|
|
311
|
-
availabilityData.totalRuns31Days,
|
|
312
|
-
).text
|
|
313
|
-
}
|
|
314
|
-
</span>
|
|
300
|
+
<AnimatedNumber
|
|
301
|
+
value={availabilityData.availability31Days ?? undefined}
|
|
302
|
+
suffix="%"
|
|
303
|
+
className={`text-2xl font-bold ${getAvailabilityColorClass(availabilityData.availability31Days, availabilityData.totalRuns31Days)}`}
|
|
304
|
+
/>
|
|
315
305
|
{availabilityData.totalRuns31Days > 0 && (
|
|
316
306
|
<span className="text-sm text-muted-foreground">
|
|
317
307
|
({availabilityData.totalRuns31Days.toLocaleString()} runs)
|
|
@@ -324,16 +314,11 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
324
314
|
365-Day Availability
|
|
325
315
|
</span>
|
|
326
316
|
<div className="flex items-baseline gap-2">
|
|
327
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
{
|
|
331
|
-
|
|
332
|
-
availabilityData.availability365Days,
|
|
333
|
-
availabilityData.totalRuns365Days,
|
|
334
|
-
).text
|
|
335
|
-
}
|
|
336
|
-
</span>
|
|
317
|
+
<AnimatedNumber
|
|
318
|
+
value={availabilityData.availability365Days ?? undefined}
|
|
319
|
+
suffix="%"
|
|
320
|
+
className={`text-2xl font-bold ${getAvailabilityColorClass(availabilityData.availability365Days, availabilityData.totalRuns365Days)}`}
|
|
321
|
+
/>
|
|
337
322
|
{availabilityData.totalRuns365Days > 0 && (
|
|
338
323
|
<span className="text-sm text-muted-foreground">
|
|
339
324
|
({availabilityData.totalRuns365Days.toLocaleString()} runs)
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type HealthCheckRunDetailed,
|
|
34
34
|
} from "../components/HealthCheckRunsTable";
|
|
35
35
|
import { ExpandedResultView } from "../components/ExpandedResultView";
|
|
36
|
+
import { SingleRunChartGrid } from "../auto-charts";
|
|
36
37
|
|
|
37
38
|
const HealthCheckHistoryDetailPageContent = () => {
|
|
38
39
|
const { systemId, configurationId, runId } = useParams<{
|
|
@@ -63,6 +64,17 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
63
64
|
},
|
|
64
65
|
);
|
|
65
66
|
|
|
67
|
+
// Fetch configurations to get strategyId
|
|
68
|
+
const { data: configurations } = healthCheckClient.getConfigurations.useQuery(
|
|
69
|
+
{},
|
|
70
|
+
{
|
|
71
|
+
enabled: !!configurationId,
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
const configuration = configurations?.configurations.find(
|
|
75
|
+
(c) => c.id === configurationId,
|
|
76
|
+
);
|
|
77
|
+
|
|
66
78
|
// Fetch data with useQuery - newest first for table display
|
|
67
79
|
const { data, isLoading } = healthCheckClient.getDetailedHistory.useQuery({
|
|
68
80
|
systemId,
|
|
@@ -127,8 +139,14 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
127
139
|
{format(new Date(specificRun.timestamp), "PPpp")}
|
|
128
140
|
</p>
|
|
129
141
|
</CardHeader>
|
|
130
|
-
<CardContent>
|
|
142
|
+
<CardContent className="space-y-4">
|
|
131
143
|
<ExpandedResultView result={specificRun.result} />
|
|
144
|
+
{configuration?.strategyId && (
|
|
145
|
+
<SingleRunChartGrid
|
|
146
|
+
strategyId={configuration.strategyId}
|
|
147
|
+
result={specificRun.result}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
132
150
|
</CardContent>
|
|
133
151
|
</Card>
|
|
134
152
|
)}
|