@checkstack/healthcheck-frontend 0.8.2 → 0.9.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 +34 -0
- package/package.json +1 -1
- package/src/auto-charts/AutoChartGrid.tsx +94 -208
- package/src/auto-charts/useStrategySchemas.ts +5 -17
- package/src/components/HealthCheckEditor.tsx +8 -0
- package/src/components/HealthCheckLatencyChart.tsx +20 -128
- package/src/components/HealthCheckStatusTimeline.tsx +60 -150
- package/src/components/HealthCheckSystemOverview.tsx +3 -10
- package/src/hooks/useHealthCheckData.ts +22 -138
- package/src/index.tsx +0 -4
- package/src/slots.tsx +24 -113
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @checkstack/healthcheck-frontend
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1b9cb25: Unified chart data to always use aggregated history with fixed target points.
|
|
8
|
+
|
|
9
|
+
**Breaking Changes:**
|
|
10
|
+
|
|
11
|
+
- Removed `RawDiagramContext` type - chart context no longer has a `type` discriminator
|
|
12
|
+
- Removed `TypedHealthCheckRun` type export - charts only use aggregated buckets now
|
|
13
|
+
- Removed `createStrategyDiagramExtension` deprecated function
|
|
14
|
+
- Removed `isAggregated` and `retentionConfig` from `useHealthCheckData` return value
|
|
15
|
+
|
|
16
|
+
**Migration:**
|
|
17
|
+
|
|
18
|
+
- Strategy diagram extensions should use `createDiagramExtensionFactory` instead of `createStrategyDiagramExtension`
|
|
19
|
+
- Extensions no longer need separate `rawComponent` and `aggregatedComponent` - use a single `component` prop
|
|
20
|
+
- `HealthCheckDiagramSlotContext` now always contains `buckets` array (no `type` field)
|
|
21
|
+
|
|
22
|
+
**Benefits:**
|
|
23
|
+
|
|
24
|
+
- Simplified frontend logic - no more mode switching based on retention config
|
|
25
|
+
- Consistent chart visualization regardless of selected time range
|
|
26
|
+
- Backend's cross-tier aggregation engine automatically selects optimal data source
|
|
27
|
+
|
|
28
|
+
**Other Changes:**
|
|
29
|
+
|
|
30
|
+
- Added warning message when configuring sub-minute check intervals, alerting users about potential performance implications
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- f1ebac2: - Fixed raw data visualization being cut off when viewing "Last 24 hours" timeframe. The `useHealthCheckData` hook was incorrectly applying pagination limits to chart data queries, causing only the oldest runs to be displayed when there were more runs than the limit. Charts now fetch all runs within the selected date range.
|
|
35
|
+
- Updated Status Timeline visualization for raw data to show stacked status distribution (green/yellow/red proportions) instead of the previous "worst status wins" approach. This makes the raw data view consistent with the aggregated data view.
|
|
36
|
+
|
|
3
37
|
## 0.8.2
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -9,7 +9,6 @@ import type { ChartField } from "./schema-parser";
|
|
|
9
9
|
import { extractChartFields, getFieldValue } from "./schema-parser";
|
|
10
10
|
import { useStrategySchemas } from "./useStrategySchemas";
|
|
11
11
|
import type { HealthCheckDiagramSlotContext } from "../slots";
|
|
12
|
-
import type { StoredHealthCheckResult } from "@checkstack/healthcheck-common";
|
|
13
12
|
import { SparklineTooltip } from "../components/SparklineTooltip";
|
|
14
13
|
import { Card, CardContent, CardHeader, CardTitle } from "@checkstack/ui";
|
|
15
14
|
import {
|
|
@@ -28,10 +27,7 @@ import {
|
|
|
28
27
|
RadialBar,
|
|
29
28
|
} from "recharts";
|
|
30
29
|
import { format } from "date-fns";
|
|
31
|
-
import {
|
|
32
|
-
downsampleSparkline,
|
|
33
|
-
MAX_SPARKLINE_BARS,
|
|
34
|
-
} from "../utils/sparkline-downsampling";
|
|
30
|
+
import { MAX_SPARKLINE_BARS } from "../utils/sparkline-downsampling";
|
|
35
31
|
|
|
36
32
|
interface AutoChartGridProps {
|
|
37
33
|
context: HealthCheckDiagramSlotContext;
|
|
@@ -54,11 +50,8 @@ export function AutoChartGrid({ context }: AutoChartGridProps) {
|
|
|
54
50
|
return;
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
//
|
|
58
|
-
const schema =
|
|
59
|
-
context.type === "raw"
|
|
60
|
-
? schemas.resultSchema
|
|
61
|
-
: schemas.aggregatedResultSchema;
|
|
53
|
+
// Always use aggregated result schema
|
|
54
|
+
const schema = schemas.aggregatedResultSchema;
|
|
62
55
|
|
|
63
56
|
if (!schema) {
|
|
64
57
|
return;
|
|
@@ -211,38 +204,35 @@ function CollectorGroup({
|
|
|
211
204
|
/**
|
|
212
205
|
* Get all assertion results for a specific collector instance.
|
|
213
206
|
* Returns array of results with timestamps/time spans in chronological order.
|
|
214
|
-
*
|
|
215
|
-
* For raw data: extracts run status with timestamp.
|
|
216
|
-
* For aggregated data: uses bucket counts with time span.
|
|
207
|
+
* Uses bucket counts with time span from aggregated data.
|
|
217
208
|
*/
|
|
218
209
|
function getAllAssertionResults(
|
|
219
210
|
context: HealthCheckDiagramSlotContext,
|
|
220
211
|
_instanceKey: string,
|
|
221
212
|
): { passed: boolean; errorMessage?: string; timeLabel?: string }[] {
|
|
222
|
-
if (context.type === "raw") {
|
|
223
|
-
return context.runs.map((run) => {
|
|
224
|
-
const result = run.result as StoredHealthCheckResult | undefined;
|
|
225
|
-
const isUnhealthy = result?.status === "unhealthy";
|
|
226
|
-
return {
|
|
227
|
-
passed: !isUnhealthy,
|
|
228
|
-
errorMessage: isUnhealthy ? result?.message : undefined,
|
|
229
|
-
timeLabel: format(new Date(run.timestamp), "MMM d, HH:mm:ss"),
|
|
230
|
-
};
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// For aggregated data, return one result per bucket with time span
|
|
235
213
|
return context.buckets.map((bucket) => {
|
|
236
214
|
const failedCount = bucket.degradedCount + bucket.unhealthyCount;
|
|
237
215
|
const passed = failedCount === 0;
|
|
238
216
|
const bucketStart = new Date(bucket.bucketStart);
|
|
239
217
|
const bucketEnd = new Date(bucket.bucketEnd);
|
|
240
218
|
const timeSpan = `${format(bucketStart, "MMM d, HH:mm")} - ${format(bucketEnd, "HH:mm")}`;
|
|
219
|
+
|
|
220
|
+
// Build detailed error message showing breakdown by type
|
|
221
|
+
let errorMessage: string | undefined;
|
|
222
|
+
if (!passed) {
|
|
223
|
+
const parts: string[] = [];
|
|
224
|
+
if (bucket.unhealthyCount > 0) {
|
|
225
|
+
parts.push(`${bucket.unhealthyCount} unhealthy`);
|
|
226
|
+
}
|
|
227
|
+
if (bucket.degradedCount > 0) {
|
|
228
|
+
parts.push(`${bucket.degradedCount} degraded`);
|
|
229
|
+
}
|
|
230
|
+
errorMessage = `${parts.join(", ")} of ${bucket.runCount} runs`;
|
|
231
|
+
}
|
|
232
|
+
|
|
241
233
|
return {
|
|
242
234
|
passed,
|
|
243
|
-
errorMessage
|
|
244
|
-
? undefined
|
|
245
|
-
: `${failedCount} failed of ${bucket.runCount}`,
|
|
235
|
+
errorMessage,
|
|
246
236
|
timeLabel: timeSpan,
|
|
247
237
|
};
|
|
248
238
|
});
|
|
@@ -319,39 +309,30 @@ function AssertionStatusCard({
|
|
|
319
309
|
</div>
|
|
320
310
|
)}
|
|
321
311
|
|
|
322
|
-
{/* Sparkline timeline -
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
<div
|
|
340
|
-
className={`flex-1 h-full ${bucket.passed ? "bg-green-500" : "bg-red-500"} hover:opacity-80`}
|
|
341
|
-
/>
|
|
342
|
-
</SparklineTooltip>
|
|
343
|
-
);
|
|
344
|
-
})}
|
|
345
|
-
</div>
|
|
346
|
-
);
|
|
347
|
-
})()}
|
|
312
|
+
{/* Sparkline timeline - render each bucket as a bar */}
|
|
313
|
+
<div className="flex h-2 gap-px rounded">
|
|
314
|
+
{results.map((result, index) => {
|
|
315
|
+
const tooltip = result.timeLabel
|
|
316
|
+
? `${result.timeLabel}\n${result.passed ? "Passed" : result.errorMessage || "Failed"}`
|
|
317
|
+
: result.passed
|
|
318
|
+
? "Passed"
|
|
319
|
+
: "Failed";
|
|
320
|
+
return (
|
|
321
|
+
<SparklineTooltip key={index} content={tooltip}>
|
|
322
|
+
<div
|
|
323
|
+
className={`flex-1 h-full ${result.passed ? "bg-green-500" : "bg-red-500"} hover:opacity-80`}
|
|
324
|
+
/>
|
|
325
|
+
</SparklineTooltip>
|
|
326
|
+
);
|
|
327
|
+
})}
|
|
328
|
+
</div>
|
|
348
329
|
</CardContent>
|
|
349
330
|
</Card>
|
|
350
331
|
);
|
|
351
332
|
}
|
|
352
333
|
|
|
353
334
|
/**
|
|
354
|
-
* Discover collector instances from
|
|
335
|
+
* Discover collector instances from aggregated bucket data.
|
|
355
336
|
* Returns a map from base collector ID (type) to array of instance UUIDs.
|
|
356
337
|
*/
|
|
357
338
|
function discoverCollectorInstances(
|
|
@@ -376,21 +357,11 @@ function discoverCollectorInstances(
|
|
|
376
357
|
}
|
|
377
358
|
};
|
|
378
359
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
| undefined;
|
|
385
|
-
addInstances(collectors);
|
|
386
|
-
}
|
|
387
|
-
} else {
|
|
388
|
-
for (const bucket of context.buckets) {
|
|
389
|
-
const collectors = (
|
|
390
|
-
bucket.aggregatedResult as Record<string, unknown> | undefined
|
|
391
|
-
)?.collectors as Record<string, unknown> | undefined;
|
|
392
|
-
addInstances(collectors);
|
|
393
|
-
}
|
|
360
|
+
for (const bucket of context.buckets) {
|
|
361
|
+
const collectors = (
|
|
362
|
+
bucket.aggregatedResult as Record<string, unknown> | undefined
|
|
363
|
+
)?.collectors as Record<string, unknown> | undefined;
|
|
364
|
+
addInstances(collectors);
|
|
394
365
|
}
|
|
395
366
|
|
|
396
367
|
// Convert sets to arrays
|
|
@@ -613,32 +584,23 @@ function BooleanRenderer({ field, context }: ChartRendererProps) {
|
|
|
613
584
|
)}
|
|
614
585
|
</div>
|
|
615
586
|
|
|
616
|
-
{/* Sparkline timeline -
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
<div
|
|
634
|
-
className={`flex-1 h-full ${bucket.passed ? "bg-green-500" : "bg-red-500"} hover:opacity-80`}
|
|
635
|
-
/>
|
|
636
|
-
</SparklineTooltip>
|
|
637
|
-
);
|
|
638
|
-
})}
|
|
639
|
-
</div>
|
|
640
|
-
);
|
|
641
|
-
})()}
|
|
587
|
+
{/* Sparkline timeline - render each value as a bar */}
|
|
588
|
+
<div className="flex h-2 gap-px rounded">
|
|
589
|
+
{valuesWithTime.map((item, index) => {
|
|
590
|
+
const tooltip = item.timeLabel
|
|
591
|
+
? `${item.timeLabel}\n${item.value ? "Yes" : "No"}`
|
|
592
|
+
: item.value
|
|
593
|
+
? "Yes"
|
|
594
|
+
: "No";
|
|
595
|
+
return (
|
|
596
|
+
<SparklineTooltip key={index} content={tooltip}>
|
|
597
|
+
<div
|
|
598
|
+
className={`flex-1 h-full ${item.value ? "bg-green-500" : "bg-red-500"} hover:opacity-80`}
|
|
599
|
+
/>
|
|
600
|
+
</SparklineTooltip>
|
|
601
|
+
);
|
|
602
|
+
})}
|
|
603
|
+
</div>
|
|
642
604
|
</div>
|
|
643
605
|
);
|
|
644
606
|
}
|
|
@@ -1029,9 +991,7 @@ function PieChartRenderer({ field, context }: ChartRendererProps) {
|
|
|
1029
991
|
|
|
1030
992
|
/**
|
|
1031
993
|
* Get the aggregated value for a field from the context.
|
|
1032
|
-
*
|
|
1033
|
-
* For raw runs: returns the latest value from result.metadata
|
|
1034
|
-
* For aggregated buckets: combines record values (counters) across ALL buckets,
|
|
994
|
+
* Combines record values (counters) across ALL buckets,
|
|
1035
995
|
* or returns the latest for non-aggregatable types.
|
|
1036
996
|
*/
|
|
1037
997
|
function getLatestValue(
|
|
@@ -1039,48 +999,31 @@ function getLatestValue(
|
|
|
1039
999
|
context: HealthCheckDiagramSlotContext,
|
|
1040
1000
|
collectorId?: string,
|
|
1041
1001
|
): unknown {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
if (firstVal && typeof firstVal === "object" && !Array.isArray(firstVal)) {
|
|
1054
|
-
return combineRecordValues(allValues as Record<string, number>[]);
|
|
1055
|
-
}
|
|
1056
|
-
// For simple values, return the latest (first in array since runs are newest-first)
|
|
1057
|
-
return allValues.at(0);
|
|
1058
|
-
} else {
|
|
1059
|
-
const buckets = context.buckets;
|
|
1060
|
-
if (buckets.length === 0) return undefined;
|
|
1061
|
-
|
|
1062
|
-
// Get all values for this field from all buckets
|
|
1063
|
-
const allValues = buckets.map((bucket) =>
|
|
1064
|
-
getFieldValue(
|
|
1065
|
-
bucket.aggregatedResult as Record<string, unknown>,
|
|
1066
|
-
fieldName,
|
|
1067
|
-
),
|
|
1068
|
-
);
|
|
1002
|
+
const buckets = context.buckets;
|
|
1003
|
+
if (buckets.length === 0) return undefined;
|
|
1004
|
+
|
|
1005
|
+
// Get all values for this field from all buckets
|
|
1006
|
+
const allValues = buckets.map((bucket) =>
|
|
1007
|
+
getFieldValue(
|
|
1008
|
+
bucket.aggregatedResult as Record<string, unknown>,
|
|
1009
|
+
fieldName,
|
|
1010
|
+
collectorId,
|
|
1011
|
+
),
|
|
1012
|
+
);
|
|
1069
1013
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
}
|
|
1075
|
-
// For simple values (like errorCount), sum them
|
|
1076
|
-
if (typeof firstVal === "number") {
|
|
1077
|
-
return allValues
|
|
1078
|
-
.filter((v): v is number => typeof v === "number")
|
|
1079
|
-
.reduce((sum, v) => sum + v, 0);
|
|
1080
|
-
}
|
|
1081
|
-
// For other types, return the latest (first in array since buckets are newest-first)
|
|
1082
|
-
return allValues.at(0);
|
|
1014
|
+
// If the values are record types (like statusCodeCounts), combine them
|
|
1015
|
+
const firstVal = allValues.find((v) => v !== undefined);
|
|
1016
|
+
if (firstVal && typeof firstVal === "object" && !Array.isArray(firstVal)) {
|
|
1017
|
+
return combineRecordValues(allValues as Record<string, number>[]);
|
|
1083
1018
|
}
|
|
1019
|
+
// For simple values (like errorCount), sum them
|
|
1020
|
+
if (typeof firstVal === "number") {
|
|
1021
|
+
return allValues
|
|
1022
|
+
.filter((v): v is number => typeof v === "number")
|
|
1023
|
+
.reduce((sum, v) => sum + v, 0);
|
|
1024
|
+
}
|
|
1025
|
+
// For other types, return the latest
|
|
1026
|
+
return allValues.at(-1);
|
|
1084
1027
|
}
|
|
1085
1028
|
|
|
1086
1029
|
/**
|
|
@@ -1103,7 +1046,7 @@ function combineRecordValues(
|
|
|
1103
1046
|
}
|
|
1104
1047
|
|
|
1105
1048
|
/**
|
|
1106
|
-
* Count occurrences of each unique value for a field across all
|
|
1049
|
+
* Count occurrences of each unique value for a field across all buckets.
|
|
1107
1050
|
* Returns a record mapping each unique value to its count.
|
|
1108
1051
|
*/
|
|
1109
1052
|
function getValueCounts(
|
|
@@ -1113,27 +1056,15 @@ function getValueCounts(
|
|
|
1113
1056
|
): Record<string, number> {
|
|
1114
1057
|
const counts: Record<string, number> = {};
|
|
1115
1058
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
} else {
|
|
1126
|
-
// For aggregated buckets, we need to look at each bucket's data
|
|
1127
|
-
for (const bucket of context.buckets) {
|
|
1128
|
-
const value = getFieldValue(
|
|
1129
|
-
bucket.aggregatedResult as Record<string, unknown>,
|
|
1130
|
-
fieldName,
|
|
1131
|
-
collectorId,
|
|
1132
|
-
);
|
|
1133
|
-
if (value !== undefined && value !== null) {
|
|
1134
|
-
const key = String(value);
|
|
1135
|
-
counts[key] = (counts[key] || 0) + 1;
|
|
1136
|
-
}
|
|
1059
|
+
for (const bucket of context.buckets) {
|
|
1060
|
+
const value = getFieldValue(
|
|
1061
|
+
bucket.aggregatedResult as Record<string, unknown>,
|
|
1062
|
+
fieldName,
|
|
1063
|
+
collectorId,
|
|
1064
|
+
);
|
|
1065
|
+
if (value !== undefined && value !== null) {
|
|
1066
|
+
const key = String(value);
|
|
1067
|
+
counts[key] = (counts[key] || 0) + 1;
|
|
1137
1068
|
}
|
|
1138
1069
|
}
|
|
1139
1070
|
|
|
@@ -1142,28 +1073,13 @@ function getValueCounts(
|
|
|
1142
1073
|
|
|
1143
1074
|
/**
|
|
1144
1075
|
* Get all numeric values for a field with time labels.
|
|
1145
|
-
* Returns values in chronological order with
|
|
1076
|
+
* Returns values in chronological order with time spans for tooltips.
|
|
1146
1077
|
*/
|
|
1147
1078
|
function getAllValuesWithTime(
|
|
1148
1079
|
fieldName: string,
|
|
1149
1080
|
context: HealthCheckDiagramSlotContext,
|
|
1150
1081
|
collectorId?: string,
|
|
1151
1082
|
): { value: number; timeLabel: string }[] {
|
|
1152
|
-
if (context.type === "raw") {
|
|
1153
|
-
return context.runs
|
|
1154
|
-
.map((run) => {
|
|
1155
|
-
const result = run.result as StoredHealthCheckResult;
|
|
1156
|
-
const value = getFieldValue(result?.metadata, fieldName, collectorId);
|
|
1157
|
-
if (typeof value !== "number") return;
|
|
1158
|
-
return {
|
|
1159
|
-
value,
|
|
1160
|
-
timeLabel: format(new Date(run.timestamp), "MMM d, HH:mm:ss"),
|
|
1161
|
-
};
|
|
1162
|
-
})
|
|
1163
|
-
.filter(
|
|
1164
|
-
(v): v is { value: number; timeLabel: string } => v !== undefined,
|
|
1165
|
-
);
|
|
1166
|
-
}
|
|
1167
1083
|
return context.buckets
|
|
1168
1084
|
.map((bucket) => {
|
|
1169
1085
|
const value = getFieldValue(
|
|
@@ -1191,21 +1107,6 @@ function getAllBooleanValuesWithTime(
|
|
|
1191
1107
|
context: HealthCheckDiagramSlotContext,
|
|
1192
1108
|
collectorId?: string,
|
|
1193
1109
|
): { value: boolean; timeLabel: string }[] {
|
|
1194
|
-
if (context.type === "raw") {
|
|
1195
|
-
return context.runs
|
|
1196
|
-
.map((run) => {
|
|
1197
|
-
const result = run.result as StoredHealthCheckResult;
|
|
1198
|
-
const value = getFieldValue(result?.metadata, fieldName, collectorId);
|
|
1199
|
-
if (typeof value !== "boolean") return;
|
|
1200
|
-
return {
|
|
1201
|
-
value,
|
|
1202
|
-
timeLabel: format(new Date(run.timestamp), "MMM d, HH:mm:ss"),
|
|
1203
|
-
};
|
|
1204
|
-
})
|
|
1205
|
-
.filter(
|
|
1206
|
-
(v): v is { value: boolean; timeLabel: string } => v !== undefined,
|
|
1207
|
-
);
|
|
1208
|
-
}
|
|
1209
1110
|
return context.buckets
|
|
1210
1111
|
.map((bucket) => {
|
|
1211
1112
|
const value = getFieldValue(
|
|
@@ -1233,21 +1134,6 @@ function getAllStringValuesWithTime(
|
|
|
1233
1134
|
context: HealthCheckDiagramSlotContext,
|
|
1234
1135
|
collectorId?: string,
|
|
1235
1136
|
): { value: string; timeLabel: string }[] {
|
|
1236
|
-
if (context.type === "raw") {
|
|
1237
|
-
return context.runs
|
|
1238
|
-
.map((run) => {
|
|
1239
|
-
const result = run.result as StoredHealthCheckResult;
|
|
1240
|
-
const value = getFieldValue(result?.metadata, fieldName, collectorId);
|
|
1241
|
-
if (typeof value !== "string") return;
|
|
1242
|
-
return {
|
|
1243
|
-
value,
|
|
1244
|
-
timeLabel: format(new Date(run.timestamp), "MMM d, HH:mm:ss"),
|
|
1245
|
-
};
|
|
1246
|
-
})
|
|
1247
|
-
.filter(
|
|
1248
|
-
(v): v is { value: string; timeLabel: string } => v !== undefined,
|
|
1249
|
-
);
|
|
1250
|
-
}
|
|
1251
1137
|
return context.buckets
|
|
1252
1138
|
.map((bucket) => {
|
|
1253
1139
|
const value = getFieldValue(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hook to fetch and cache strategy schemas.
|
|
2
|
+
* Hook to fetch and cache strategy schemas for auto-chart rendering.
|
|
3
3
|
*
|
|
4
|
-
* Fetches
|
|
4
|
+
* Fetches strategy aggregated result schemas AND collector aggregated result schemas,
|
|
5
5
|
* merging them into a unified schema where collector schemas are nested
|
|
6
6
|
* under `properties.collectors.<collectorId>`.
|
|
7
7
|
*/
|
|
@@ -11,15 +11,14 @@ import { usePluginClient } from "@checkstack/frontend-api";
|
|
|
11
11
|
import { HealthCheckApi } from "../api";
|
|
12
12
|
|
|
13
13
|
interface StrategySchemas {
|
|
14
|
-
resultSchema: Record<string, unknown> | undefined;
|
|
15
14
|
aggregatedResultSchema: Record<string, unknown> | undefined;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* Fetch and cache strategy schemas for auto-chart rendering.
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
* so that chart fields from collectors are properly extracted.
|
|
20
|
+
* Fetches collector aggregated schemas and merges them into the strategy's
|
|
21
|
+
* aggregated result schema so that chart fields from collectors are properly extracted.
|
|
23
22
|
*
|
|
24
23
|
* @param strategyId - The strategy ID to fetch schemas for
|
|
25
24
|
* @returns Schemas for the strategy, or undefined if not found
|
|
@@ -49,27 +48,17 @@ export function useStrategySchemas(strategyId: string): {
|
|
|
49
48
|
const strategy = strategies.find((s) => s.id === strategyId);
|
|
50
49
|
|
|
51
50
|
if (strategy) {
|
|
52
|
-
// Build collector schemas
|
|
53
|
-
const collectorProperties: Record<string, unknown> = {};
|
|
51
|
+
// Build collector aggregated schemas for nesting under aggregatedResultSchema.properties.collectors
|
|
54
52
|
const collectorAggregatedProperties: Record<string, unknown> = {};
|
|
55
53
|
|
|
56
54
|
for (const collector of collectors) {
|
|
57
55
|
// Use full ID so it matches stored data keys like "healthcheck-http.request"
|
|
58
|
-
collectorProperties[collector.id] = collector.resultSchema;
|
|
59
|
-
|
|
60
|
-
// Also collect aggregated schemas if available
|
|
61
56
|
if (collector.aggregatedResultSchema) {
|
|
62
57
|
collectorAggregatedProperties[collector.id] =
|
|
63
58
|
collector.aggregatedResultSchema;
|
|
64
59
|
}
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
// Merge collector schemas into strategy result schema
|
|
68
|
-
const mergedResultSchema = mergeCollectorSchemas(
|
|
69
|
-
strategy.resultSchema as Record<string, unknown> | undefined,
|
|
70
|
-
collectorProperties,
|
|
71
|
-
);
|
|
72
|
-
|
|
73
62
|
// Merge collector aggregated schemas into strategy aggregated schema
|
|
74
63
|
const mergedAggregatedSchema = mergeCollectorSchemas(
|
|
75
64
|
strategy.aggregatedResultSchema as Record<string, unknown> | undefined,
|
|
@@ -77,7 +66,6 @@ export function useStrategySchemas(strategyId: string): {
|
|
|
77
66
|
);
|
|
78
67
|
|
|
79
68
|
setSchemas({
|
|
80
|
-
resultSchema: mergedResultSchema,
|
|
81
69
|
aggregatedResultSchema: mergedAggregatedSchema,
|
|
82
70
|
});
|
|
83
71
|
}
|
|
@@ -132,6 +132,14 @@ export const HealthCheckEditor: React.FC<HealthCheckEditorProps> = ({
|
|
|
132
132
|
onChange={(e) => setInterval(e.target.value)}
|
|
133
133
|
required
|
|
134
134
|
/>
|
|
135
|
+
{Number.parseInt(interval, 10) < 60 && (
|
|
136
|
+
<p className="text-sm text-amber-600 dark:text-amber-400">
|
|
137
|
+
⚠️ Sub-minute intervals generate large amounts of data and may
|
|
138
|
+
impact chart loading performance. Consider using intervals of
|
|
139
|
+
60 seconds or more or drastically reduce the retention period
|
|
140
|
+
for raw data.
|
|
141
|
+
</p>
|
|
142
|
+
)}
|
|
135
143
|
</div>
|
|
136
144
|
|
|
137
145
|
<PluginConfigForm
|