@checkstack/healthcheck-frontend 0.16.5 → 0.17.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 +50 -0
- package/package.json +8 -7
- package/src/auto-charts/AutoChartGrid.tsx +259 -51
- package/src/components/HealthCheckDrawer.tsx +8 -0
- package/src/components/HealthCheckLatencyChart.tsx +57 -15
- package/src/components/assignments/AssignmentTree.tsx +15 -5
- package/src/components/editor/EditorTree.tsx +19 -1
- package/src/index.tsx +8 -1
- package/src/pages/AssignmentIDEPage.tsx +17 -1
- package/src/pages/HealthCheckHistoryDetailPage.tsx +1 -1
- package/src/pages/HealthCheckIDEPage.tsx +40 -23
- package/src/slots.tsx +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# @checkstack/healthcheck-frontend
|
|
2
2
|
|
|
3
|
+
## 0.17.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8d1ef12: ## Anomaly Detection & UI Improvements
|
|
8
|
+
|
|
9
|
+
### Anomaly Detection Enhancements (Phase 2)
|
|
10
|
+
|
|
11
|
+
- **`@checkstack/anomaly-backend`**: Implemented background baseline analyzer jobs and anomaly trend deviation detection mechanics.
|
|
12
|
+
- **`@checkstack/anomaly-common`**: Added new baseline statistical logic and inference rules.
|
|
13
|
+
- **`@checkstack/anomaly-frontend`**: Added new Anomaly Widget and refactored system detail rendering to be more human-readable.
|
|
14
|
+
- **`@checkstack/dashboard-frontend`**: Refined the global anomaly widget and fixed hardcoded access gating to render appropriately.
|
|
15
|
+
- **`@checkstack/healthcheck-backend`**: Connected executor telemetry to the anomaly pipeline.
|
|
16
|
+
- **`@checkstack/healthcheck-frontend`**: Reconciled baseline display consistency in Drawer and charts.
|
|
17
|
+
|
|
18
|
+
### Notification Identifiers
|
|
19
|
+
|
|
20
|
+
- **`@checkstack/incident-backend`**: Resolved system IDs to human-readable System Names within Incident notifications to eliminate ID-only alert content.
|
|
21
|
+
- **`@checkstack/maintenance-backend`**: Adopted the same resolution strategy for Maintenance notifications to keep parity.
|
|
22
|
+
|
|
23
|
+
### UI Experience
|
|
24
|
+
|
|
25
|
+
- **`@checkstack/incident-frontend`**: Fixed the "Back to X" BackLink to properly use `react-router` hook `useNavigate` instead of doing a full application reload.
|
|
26
|
+
- **`@checkstack/healthcheck-frontend`**: Implemented `useNavigate` for seamless SPA back-linking.
|
|
27
|
+
- **`@checkstack/integration-frontend`**: Updated connections and delivery logs links to navigate without hard reloads.
|
|
28
|
+
|
|
29
|
+
- 8d1ef12: Phase 2 of anomaly detection: trend drift detection.
|
|
30
|
+
|
|
31
|
+
The background baseline analyzer now computes a linear regression slope across each field's chronologically-ordered history and runs a `detectDrift` evaluator that catches gradual "creeping degradation" never reaching the 3σ spike threshold. Drifts share the same `anomalies` table as spike anomalies via a new `kind` column (`spike` | `drift`, default `spike`); the existing suspicious → anomaly → recovered lifecycle is reused, ticking at the analyzer's hourly cadence with a default 2-run confirmation window.
|
|
32
|
+
|
|
33
|
+
User-facing additions: a Trend Drift toggle and threshold slider on both the template and assignment anomaly settings panels (with per-field overrides), drift rows in the System Anomaly widget, dashed regression-line overlays on the auto-generated line charts, and a new `ANOMALY_TREND_DETECTED` signal for live UI updates. Plugin authors can disable drift per chartable field via `x-anomaly-drift-enabled: false` or tighten/loosen it via `x-anomaly-drift-threshold`.
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- Updated dependencies [8d1ef12]
|
|
38
|
+
- Updated dependencies [8d1ef12]
|
|
39
|
+
- Updated dependencies [8d1ef12]
|
|
40
|
+
- Updated dependencies [8d1ef12]
|
|
41
|
+
- @checkstack/healthcheck-common@0.12.0
|
|
42
|
+
- @checkstack/anomaly-common@0.2.0
|
|
43
|
+
- @checkstack/dashboard-frontend@0.5.0
|
|
44
|
+
- @checkstack/common@0.7.0
|
|
45
|
+
- @checkstack/ui@1.6.0
|
|
46
|
+
- @checkstack/satellite-common@0.2.1
|
|
47
|
+
- @checkstack/auth-frontend@0.5.30
|
|
48
|
+
- @checkstack/catalog-common@1.5.2
|
|
49
|
+
- @checkstack/frontend-api@0.3.11
|
|
50
|
+
- @checkstack/gitops-frontend@0.3.5
|
|
51
|
+
- @checkstack/signal-frontend@0.0.16
|
|
52
|
+
|
|
3
53
|
## 0.16.5
|
|
4
54
|
|
|
5
55
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"checkstack": {
|
|
@@ -12,15 +12,16 @@
|
|
|
12
12
|
"lint:code": "eslint . --max-warnings 0"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@checkstack/
|
|
16
|
-
"@checkstack/
|
|
15
|
+
"@checkstack/anomaly-common": "0.1.0",
|
|
16
|
+
"@checkstack/auth-frontend": "0.5.29",
|
|
17
|
+
"@checkstack/catalog-common": "1.5.1",
|
|
17
18
|
"@checkstack/common": "0.6.5",
|
|
18
|
-
"@checkstack/dashboard-frontend": "0.4.
|
|
19
|
-
"@checkstack/frontend-api": "0.3.
|
|
20
|
-
"@checkstack/gitops-frontend": "0.3.
|
|
19
|
+
"@checkstack/dashboard-frontend": "0.4.6",
|
|
20
|
+
"@checkstack/frontend-api": "0.3.10",
|
|
21
|
+
"@checkstack/gitops-frontend": "0.3.4",
|
|
21
22
|
"@checkstack/healthcheck-common": "0.11.0",
|
|
22
23
|
"@checkstack/signal-frontend": "0.0.15",
|
|
23
|
-
"@checkstack/ui": "1.5.
|
|
24
|
+
"@checkstack/ui": "1.5.1",
|
|
24
25
|
"ajv": "^8.18.0",
|
|
25
26
|
"ajv-formats": "^3.0.1",
|
|
26
27
|
"date-fns": "^4.1.0",
|
|
@@ -25,7 +25,13 @@ import {
|
|
|
25
25
|
Area,
|
|
26
26
|
RadialBarChart,
|
|
27
27
|
RadialBar,
|
|
28
|
+
ReferenceArea,
|
|
28
29
|
} from "recharts";
|
|
30
|
+
import {
|
|
31
|
+
AnomalyApi,
|
|
32
|
+
type AnomalyBaselineDto,
|
|
33
|
+
} from "@checkstack/anomaly-common";
|
|
34
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
29
35
|
import { format } from "date-fns";
|
|
30
36
|
import { MAX_SPARKLINE_BARS } from "../utils/sparkline-downsampling";
|
|
31
37
|
|
|
@@ -41,6 +47,11 @@ interface AutoChartGridProps {
|
|
|
41
47
|
*/
|
|
42
48
|
export function AutoChartGrid({ context }: AutoChartGridProps) {
|
|
43
49
|
const { schemas, loading } = useStrategySchemas(context.strategyId);
|
|
50
|
+
const anomalyClient = usePluginClient(AnomalyApi);
|
|
51
|
+
const { data: baselines = [] } = anomalyClient.getAnomalyBaselines.useQuery({
|
|
52
|
+
systemId: context.systemId,
|
|
53
|
+
configurationId: context.configurationId,
|
|
54
|
+
});
|
|
44
55
|
|
|
45
56
|
if (loading) {
|
|
46
57
|
return; // Don't show loading state, let custom charts render first
|
|
@@ -77,7 +88,12 @@ export function AutoChartGrid({ context }: AutoChartGridProps) {
|
|
|
77
88
|
{strategyFields.length > 0 && (
|
|
78
89
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
79
90
|
{strategyFields.map((field) => (
|
|
80
|
-
<AutoChartCard
|
|
91
|
+
<AutoChartCard
|
|
92
|
+
key={field.name}
|
|
93
|
+
field={field}
|
|
94
|
+
context={context}
|
|
95
|
+
baselines={baselines}
|
|
96
|
+
/>
|
|
81
97
|
))}
|
|
82
98
|
</div>
|
|
83
99
|
)}
|
|
@@ -88,6 +104,7 @@ export function AutoChartGrid({ context }: AutoChartGridProps) {
|
|
|
88
104
|
key={group.instanceKey}
|
|
89
105
|
group={group}
|
|
90
106
|
context={context}
|
|
107
|
+
baselines={baselines}
|
|
91
108
|
/>
|
|
92
109
|
))}
|
|
93
110
|
</div>
|
|
@@ -151,9 +168,11 @@ function buildCollectorGroups(
|
|
|
151
168
|
function CollectorGroup({
|
|
152
169
|
group,
|
|
153
170
|
context,
|
|
171
|
+
baselines,
|
|
154
172
|
}: {
|
|
155
173
|
group: CollectorGroupData;
|
|
156
174
|
context: HealthCheckDiagramSlotContext;
|
|
175
|
+
baselines: AnomalyBaselineDto[];
|
|
157
176
|
}) {
|
|
158
177
|
// Separate fields into narrow (grid) and wide (full-width) categories
|
|
159
178
|
const narrowFields = group.fields.filter(
|
|
@@ -177,6 +196,7 @@ function CollectorGroup({
|
|
|
177
196
|
key={`${field.instanceKey}-${field.name}`}
|
|
178
197
|
field={field}
|
|
179
198
|
context={context}
|
|
199
|
+
baselines={baselines}
|
|
180
200
|
/>
|
|
181
201
|
))}
|
|
182
202
|
</div>
|
|
@@ -194,6 +214,7 @@ function CollectorGroup({
|
|
|
194
214
|
key={`${field.instanceKey}-${field.name}`}
|
|
195
215
|
field={field}
|
|
196
216
|
context={context}
|
|
217
|
+
baselines={baselines}
|
|
197
218
|
/>
|
|
198
219
|
))}
|
|
199
220
|
</div>
|
|
@@ -383,6 +404,7 @@ interface ExpandedChartField extends ChartField {
|
|
|
383
404
|
interface AutoChartCardProps {
|
|
384
405
|
field: ExpandedChartField;
|
|
385
406
|
context: HealthCheckDiagramSlotContext;
|
|
407
|
+
baselines?: AnomalyBaselineDto[];
|
|
386
408
|
}
|
|
387
409
|
|
|
388
410
|
/**
|
|
@@ -393,14 +415,44 @@ const WIDE_CHART_TYPES = new Set(["line", "boolean", "text"]);
|
|
|
393
415
|
/**
|
|
394
416
|
* Individual chart card that renders based on field type.
|
|
395
417
|
*/
|
|
396
|
-
function AutoChartCard({ field, context }: AutoChartCardProps) {
|
|
418
|
+
function AutoChartCard({ field, context, baselines }: AutoChartCardProps) {
|
|
419
|
+
const fullFieldPath = field.collectorId
|
|
420
|
+
? `collectors.${field.collectorId}.${field.name}`
|
|
421
|
+
: field.name;
|
|
422
|
+
|
|
423
|
+
let baseline = baselines?.find((b) => b.fieldPath === fullFieldPath);
|
|
424
|
+
|
|
425
|
+
// If no exact match, try mapping aggregated field names back to raw field names
|
|
426
|
+
if (!baseline) {
|
|
427
|
+
let rawFieldName = field.name;
|
|
428
|
+
if (rawFieldName.startsWith("avg")) {
|
|
429
|
+
rawFieldName =
|
|
430
|
+
rawFieldName.charAt(3).toLowerCase() + rawFieldName.slice(4);
|
|
431
|
+
} else if (rawFieldName.startsWith("min")) {
|
|
432
|
+
rawFieldName =
|
|
433
|
+
rawFieldName.charAt(3).toLowerCase() + rawFieldName.slice(4);
|
|
434
|
+
} else if (rawFieldName.startsWith("max")) {
|
|
435
|
+
rawFieldName =
|
|
436
|
+
rawFieldName.charAt(3).toLowerCase() + rawFieldName.slice(4);
|
|
437
|
+
} else if (rawFieldName === "successRate") {
|
|
438
|
+
rawFieldName = "success";
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (rawFieldName !== field.name) {
|
|
442
|
+
const rawFullFieldPath = field.collectorId
|
|
443
|
+
? `collectors.${field.collectorId}.${rawFieldName}`
|
|
444
|
+
: rawFieldName;
|
|
445
|
+
baseline = baselines?.find((b) => b.fieldPath === rawFullFieldPath);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
397
449
|
return (
|
|
398
450
|
<Card>
|
|
399
451
|
<CardHeader className="pb-2">
|
|
400
452
|
<CardTitle className="text-sm font-medium">{field.label}</CardTitle>
|
|
401
453
|
</CardHeader>
|
|
402
454
|
<CardContent>
|
|
403
|
-
<ChartRenderer field={field} context={context} />
|
|
455
|
+
<ChartRenderer field={field} context={context} baseline={baseline} />
|
|
404
456
|
</CardContent>
|
|
405
457
|
</Card>
|
|
406
458
|
);
|
|
@@ -409,36 +461,57 @@ function AutoChartCard({ field, context }: AutoChartCardProps) {
|
|
|
409
461
|
interface ChartRendererProps {
|
|
410
462
|
field: ExpandedChartField;
|
|
411
463
|
context: HealthCheckDiagramSlotContext;
|
|
464
|
+
baseline?: AnomalyBaselineDto;
|
|
412
465
|
}
|
|
413
466
|
|
|
414
467
|
/**
|
|
415
468
|
* Dispatches to appropriate chart renderer based on chart type.
|
|
416
469
|
*/
|
|
417
|
-
function ChartRenderer({ field, context }: ChartRendererProps) {
|
|
470
|
+
function ChartRenderer({ field, context, baseline }: ChartRendererProps) {
|
|
418
471
|
switch (field.chartType) {
|
|
419
472
|
case "line": {
|
|
420
|
-
return
|
|
473
|
+
return (
|
|
474
|
+
<LineChartRenderer
|
|
475
|
+
field={field}
|
|
476
|
+
context={context}
|
|
477
|
+
baseline={baseline}
|
|
478
|
+
/>
|
|
479
|
+
);
|
|
421
480
|
}
|
|
422
481
|
case "gauge": {
|
|
423
|
-
return
|
|
482
|
+
return (
|
|
483
|
+
<GaugeRenderer field={field} context={context} baseline={baseline} />
|
|
484
|
+
);
|
|
424
485
|
}
|
|
425
486
|
case "counter": {
|
|
426
|
-
return
|
|
487
|
+
return (
|
|
488
|
+
<CounterRenderer field={field} context={context} baseline={baseline} />
|
|
489
|
+
);
|
|
427
490
|
}
|
|
428
491
|
case "bar": {
|
|
429
|
-
return
|
|
492
|
+
return (
|
|
493
|
+
<BarChartRenderer field={field} context={context} baseline={baseline} />
|
|
494
|
+
);
|
|
430
495
|
}
|
|
431
496
|
case "pie": {
|
|
432
|
-
return
|
|
497
|
+
return (
|
|
498
|
+
<PieChartRenderer field={field} context={context} baseline={baseline} />
|
|
499
|
+
);
|
|
433
500
|
}
|
|
434
501
|
case "boolean": {
|
|
435
|
-
return
|
|
502
|
+
return (
|
|
503
|
+
<BooleanRenderer field={field} context={context} baseline={baseline} />
|
|
504
|
+
);
|
|
436
505
|
}
|
|
437
506
|
case "text": {
|
|
438
|
-
return
|
|
507
|
+
return (
|
|
508
|
+
<TextRenderer field={field} context={context} baseline={baseline} />
|
|
509
|
+
);
|
|
439
510
|
}
|
|
440
511
|
case "status": {
|
|
441
|
-
return
|
|
512
|
+
return (
|
|
513
|
+
<StatusRenderer field={field} context={context} baseline={baseline} />
|
|
514
|
+
);
|
|
442
515
|
}
|
|
443
516
|
default: {
|
|
444
517
|
return;
|
|
@@ -502,7 +575,7 @@ function CounterRenderer({ field, context }: ChartRendererProps) {
|
|
|
502
575
|
/**
|
|
503
576
|
* Renders a percentage gauge visualization using Recharts RadialBarChart.
|
|
504
577
|
*/
|
|
505
|
-
function GaugeRenderer({ field, context }: ChartRendererProps) {
|
|
578
|
+
function GaugeRenderer({ field, context, baseline }: ChartRendererProps) {
|
|
506
579
|
const value = getLatestValue(field.name, context, field.instanceKey);
|
|
507
580
|
const numValue =
|
|
508
581
|
typeof value === "number" ? Math.min(100, Math.max(0, value)) : 0;
|
|
@@ -519,29 +592,66 @@ function GaugeRenderer({ field, context }: ChartRendererProps) {
|
|
|
519
592
|
const data = [{ name: field.label, value: numValue, fill: fillColor }];
|
|
520
593
|
|
|
521
594
|
return (
|
|
522
|
-
<div className="flex
|
|
523
|
-
<
|
|
524
|
-
<
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
{
|
|
543
|
-
|
|
595
|
+
<div className="flex flex-col gap-2">
|
|
596
|
+
<div className="flex items-center gap-3">
|
|
597
|
+
<ResponsiveContainer width={80} height={80}>
|
|
598
|
+
<RadialBarChart
|
|
599
|
+
cx="50%"
|
|
600
|
+
cy="50%"
|
|
601
|
+
innerRadius="60%"
|
|
602
|
+
outerRadius="100%"
|
|
603
|
+
barSize={8}
|
|
604
|
+
data={data}
|
|
605
|
+
startAngle={90}
|
|
606
|
+
endAngle={-270}
|
|
607
|
+
>
|
|
608
|
+
<RadialBar
|
|
609
|
+
dataKey="value"
|
|
610
|
+
cornerRadius={4}
|
|
611
|
+
background={{ fill: "hsl(var(--muted))" }}
|
|
612
|
+
/>
|
|
613
|
+
</RadialBarChart>
|
|
614
|
+
</ResponsiveContainer>
|
|
615
|
+
<div className="text-2xl font-bold" style={{ color: fillColor }}>
|
|
616
|
+
{numValue.toFixed(1)}
|
|
617
|
+
{unit}
|
|
618
|
+
</div>
|
|
544
619
|
</div>
|
|
620
|
+
{baseline &&
|
|
621
|
+
(() => {
|
|
622
|
+
if (
|
|
623
|
+
baseline.dominantValue !== undefined &&
|
|
624
|
+
baseline.dominantValue !== null
|
|
625
|
+
) {
|
|
626
|
+
let expectedNum = Number(baseline.dominantValue);
|
|
627
|
+
if (baseline.dominantValue === "true" || baseline.dominantValue === "false") {
|
|
628
|
+
const ratio = baseline.dominantRatio ?? (baseline.dominantValue === "true" ? 1 : 0);
|
|
629
|
+
expectedNum = baseline.dominantValue === "true" ? (ratio * 100) : ((1 - ratio) * 100);
|
|
630
|
+
}
|
|
631
|
+
if (!Number.isNaN(expectedNum)) {
|
|
632
|
+
return (
|
|
633
|
+
<div className="text-xs text-muted-foreground mt-1">
|
|
634
|
+
Expected: {expectedNum.toFixed(1)}
|
|
635
|
+
{unit}
|
|
636
|
+
</div>
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (typeof baseline.mean === "number") {
|
|
642
|
+
const min = Math.max(0, baseline.mean - baseline.stdDev * 3);
|
|
643
|
+
const max = baseline.mean + baseline.stdDev * 3;
|
|
644
|
+
return (
|
|
645
|
+
<div className="text-xs text-muted-foreground mt-1">
|
|
646
|
+
Expected: {baseline.mean.toFixed(1)}
|
|
647
|
+
{unit} (±{(baseline.stdDev * 3).toFixed(1)}) [{min.toFixed(1)} -{" "}
|
|
648
|
+
{max.toFixed(1)}]
|
|
649
|
+
</div>
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return <></>;
|
|
654
|
+
})()}
|
|
545
655
|
</div>
|
|
546
656
|
);
|
|
547
657
|
}
|
|
@@ -549,7 +659,7 @@ function GaugeRenderer({ field, context }: ChartRendererProps) {
|
|
|
549
659
|
/**
|
|
550
660
|
* Renders a boolean indicator with historical sparkline.
|
|
551
661
|
*/
|
|
552
|
-
function BooleanRenderer({ field, context }: ChartRendererProps) {
|
|
662
|
+
function BooleanRenderer({ field, context, baseline }: ChartRendererProps) {
|
|
553
663
|
const valuesWithTime = getAllBooleanValuesWithTime(
|
|
554
664
|
field.name,
|
|
555
665
|
context,
|
|
@@ -587,6 +697,17 @@ function BooleanRenderer({ field, context }: ChartRendererProps) {
|
|
|
587
697
|
)}
|
|
588
698
|
</div>
|
|
589
699
|
|
|
700
|
+
{baseline &&
|
|
701
|
+
baseline.dominantValue !== undefined &&
|
|
702
|
+
baseline.dominantValue !== null && (
|
|
703
|
+
<div className="text-xs text-muted-foreground">
|
|
704
|
+
Expected: {String(baseline.dominantValue)}
|
|
705
|
+
{baseline.dominantRatio
|
|
706
|
+
? ` (${(baseline.dominantRatio * 100).toFixed(0)}%)`
|
|
707
|
+
: ""}
|
|
708
|
+
</div>
|
|
709
|
+
)}
|
|
710
|
+
|
|
590
711
|
{/* Sparkline timeline - render each value as a bar */}
|
|
591
712
|
<div className="flex h-2 gap-px rounded">
|
|
592
713
|
{valuesWithTime.map((item, index) => {
|
|
@@ -611,7 +732,7 @@ function BooleanRenderer({ field, context }: ChartRendererProps) {
|
|
|
611
732
|
/**
|
|
612
733
|
* Renders text value with historical sparkline for status-type fields.
|
|
613
734
|
*/
|
|
614
|
-
function TextRenderer({ field, context }: ChartRendererProps) {
|
|
735
|
+
function TextRenderer({ field, context, baseline }: ChartRendererProps) {
|
|
615
736
|
const valuesWithTime = getAllStringValuesWithTime(
|
|
616
737
|
field.name,
|
|
617
738
|
context,
|
|
@@ -641,6 +762,17 @@ function TextRenderer({ field, context }: ChartRendererProps) {
|
|
|
641
762
|
)}
|
|
642
763
|
</div>
|
|
643
764
|
|
|
765
|
+
{baseline &&
|
|
766
|
+
baseline.dominantValue !== undefined &&
|
|
767
|
+
baseline.dominantValue !== null && (
|
|
768
|
+
<div className="text-xs text-muted-foreground">
|
|
769
|
+
Expected: {String(baseline.dominantValue)}
|
|
770
|
+
{baseline.dominantRatio
|
|
771
|
+
? ` (${(baseline.dominantRatio * 100).toFixed(0)}%)`
|
|
772
|
+
: ""}
|
|
773
|
+
</div>
|
|
774
|
+
)}
|
|
775
|
+
|
|
644
776
|
{/* Sparkline timeline - always show for historical context */}
|
|
645
777
|
{(() => {
|
|
646
778
|
// Downsample for string values - bucket is "primary" if all values match latest
|
|
@@ -733,7 +865,7 @@ function StatusRenderer({ field, context }: ChartRendererProps) {
|
|
|
733
865
|
/**
|
|
734
866
|
* Renders an area chart for time series data using Recharts AreaChart.
|
|
735
867
|
*/
|
|
736
|
-
function LineChartRenderer({ field, context }: ChartRendererProps) {
|
|
868
|
+
function LineChartRenderer({ field, context, baseline }: ChartRendererProps) {
|
|
737
869
|
const valuesWithTime = getAllValuesWithTime(
|
|
738
870
|
field.name,
|
|
739
871
|
context,
|
|
@@ -745,28 +877,69 @@ function LineChartRenderer({ field, context }: ChartRendererProps) {
|
|
|
745
877
|
return <div className="text-muted-foreground">No data</div>;
|
|
746
878
|
}
|
|
747
879
|
|
|
748
|
-
|
|
880
|
+
const avg =
|
|
881
|
+
valuesWithTime.reduce((a, b) => a + b.value, 0) / valuesWithTime.length;
|
|
882
|
+
|
|
883
|
+
// Calculate average runs per bucket to adjust the visual standard deviation.
|
|
884
|
+
// Since the chart plots the *average* of the bucket, the standard deviation
|
|
885
|
+
// of that average is sigma / sqrt(n). This makes the baseline band visually
|
|
886
|
+
// correct for the aggregated data being displayed.
|
|
887
|
+
const totalRuns = context.buckets.reduce(
|
|
888
|
+
(sum, b) => sum + (b.runCount || 1),
|
|
889
|
+
0,
|
|
890
|
+
);
|
|
891
|
+
const avgRunCount = Math.max(
|
|
892
|
+
1,
|
|
893
|
+
totalRuns / Math.max(1, context.buckets.length),
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
// Trend is the slope projected over the baseline window: slope × sampleCount
|
|
897
|
+
// — the same scalar the drift evaluator uses. Surfaced as a header chip rather
|
|
898
|
+
// than a diagonal line because it's a rate, not an absolute value, and shares
|
|
899
|
+
// no natural axis with the data series.
|
|
900
|
+
const projectedChange = baseline ? baseline.trendSlope * baseline.sampleCount : 0;
|
|
901
|
+
const showTrend = !!baseline && Math.abs(projectedChange) > 0.01;
|
|
902
|
+
const driftSigmas = baseline && baseline.stdDev > 0
|
|
903
|
+
? Math.abs(projectedChange) / baseline.stdDev
|
|
904
|
+
: 0;
|
|
905
|
+
const isDrifting = driftSigmas >= 2;
|
|
906
|
+
|
|
749
907
|
const chartData = valuesWithTime.map((item, index) => ({
|
|
750
908
|
index,
|
|
751
909
|
value: item.value,
|
|
752
910
|
timeLabel: item.timeLabel,
|
|
753
911
|
}));
|
|
754
912
|
|
|
755
|
-
const avg =
|
|
756
|
-
valuesWithTime.reduce((a, b) => a + b.value, 0) / valuesWithTime.length;
|
|
757
|
-
|
|
758
913
|
return (
|
|
759
914
|
<div className="space-y-2">
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
{unit}
|
|
915
|
+
{baseline ? (
|
|
916
|
+
<div className="flex items-center justify-between text-xs px-1 gap-3">
|
|
917
|
+
<span className="text-warning font-medium">
|
|
918
|
+
Expected: {baseline.mean.toFixed(1)}{unit} (±{((baseline.stdDev / Math.sqrt(avgRunCount)) * 3).toFixed(1)})
|
|
765
919
|
</span>
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
920
|
+
<div className="flex items-center gap-3">
|
|
921
|
+
{showTrend && (
|
|
922
|
+
<span className={isDrifting ? "text-warning font-medium" : "text-muted-foreground"}>
|
|
923
|
+
Trend: {projectedChange >= 0 ? "↑ +" : "↓ "}{projectedChange.toFixed(1)}{unit}
|
|
924
|
+
</span>
|
|
925
|
+
)}
|
|
926
|
+
<span className="text-muted-foreground">
|
|
927
|
+
Avg: {avg.toFixed(1)}{unit}
|
|
928
|
+
</span>
|
|
929
|
+
</div>
|
|
930
|
+
</div>
|
|
931
|
+
) : (
|
|
932
|
+
<div className="flex items-center justify-end text-xs px-1">
|
|
933
|
+
<span className="text-muted-foreground">
|
|
934
|
+
Avg: {avg.toFixed(1)}{unit}
|
|
935
|
+
</span>
|
|
936
|
+
</div>
|
|
937
|
+
)}
|
|
938
|
+
<ResponsiveContainer width="100%" height={120}>
|
|
939
|
+
<AreaChart
|
|
940
|
+
data={chartData}
|
|
941
|
+
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
|
|
942
|
+
>
|
|
770
943
|
<defs>
|
|
771
944
|
<linearGradient
|
|
772
945
|
id={`gradient-${field.name}`}
|
|
@@ -787,6 +960,41 @@ function LineChartRenderer({ field, context }: ChartRendererProps) {
|
|
|
787
960
|
/>
|
|
788
961
|
</linearGradient>
|
|
789
962
|
</defs>
|
|
963
|
+
<XAxis
|
|
964
|
+
dataKey="index"
|
|
965
|
+
type="number"
|
|
966
|
+
domain={["dataMin", "dataMax"]}
|
|
967
|
+
tickFormatter={(index: number) => {
|
|
968
|
+
const label = chartData[index]?.timeLabel;
|
|
969
|
+
return label ? label.split(" - ")[0] : "";
|
|
970
|
+
}}
|
|
971
|
+
stroke="hsl(var(--muted-foreground))"
|
|
972
|
+
fontSize={12}
|
|
973
|
+
minTickGap={30}
|
|
974
|
+
/>
|
|
975
|
+
<YAxis
|
|
976
|
+
stroke="hsl(var(--muted-foreground))"
|
|
977
|
+
fontSize={12}
|
|
978
|
+
tickFormatter={(v: number) => `${v}${unit}`}
|
|
979
|
+
width={60}
|
|
980
|
+
/>
|
|
981
|
+
{baseline && (
|
|
982
|
+
<ReferenceArea
|
|
983
|
+
y1={Math.max(
|
|
984
|
+
0,
|
|
985
|
+
baseline.mean -
|
|
986
|
+
(baseline.stdDev / Math.sqrt(avgRunCount)) * 3,
|
|
987
|
+
)}
|
|
988
|
+
y2={
|
|
989
|
+
baseline.mean + (baseline.stdDev / Math.sqrt(avgRunCount)) * 3
|
|
990
|
+
}
|
|
991
|
+
fill="hsl(var(--warning))"
|
|
992
|
+
fillOpacity={0.08}
|
|
993
|
+
stroke="hsl(var(--warning))"
|
|
994
|
+
strokeOpacity={0.4}
|
|
995
|
+
strokeWidth={1}
|
|
996
|
+
/>
|
|
997
|
+
)}
|
|
790
998
|
<Area
|
|
791
999
|
type="monotone"
|
|
792
1000
|
dataKey="value"
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
healthcheckRoutes,
|
|
16
16
|
} from "@checkstack/healthcheck-common";
|
|
17
17
|
import { SatelliteApi, satelliteAccess } from "@checkstack/satellite-common";
|
|
18
|
+
import { AnomalyApi } from "@checkstack/anomaly-common";
|
|
18
19
|
import { resolveRoute } from "@checkstack/common";
|
|
19
20
|
import {
|
|
20
21
|
HealthBadge,
|
|
@@ -189,6 +190,12 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
|
|
|
189
190
|
},
|
|
190
191
|
});
|
|
191
192
|
|
|
193
|
+
const anomalyClient = usePluginClient(AnomalyApi);
|
|
194
|
+
const { data: baselines = [] } = anomalyClient.getAnomalyBaselines.useQuery(
|
|
195
|
+
{ systemId, configurationId: item.configurationId },
|
|
196
|
+
{ enabled: !!systemId && !!item.configurationId }
|
|
197
|
+
);
|
|
198
|
+
|
|
192
199
|
// Pagination for history table
|
|
193
200
|
const pagination = usePagination({ defaultLimit: 5 });
|
|
194
201
|
|
|
@@ -425,6 +432,7 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
|
|
|
425
432
|
context={chartContext}
|
|
426
433
|
height={120}
|
|
427
434
|
showAverage
|
|
435
|
+
baselines={baselines}
|
|
428
436
|
/>
|
|
429
437
|
</CardContent>
|
|
430
438
|
</Card>
|
|
@@ -5,15 +5,18 @@ import {
|
|
|
5
5
|
YAxis,
|
|
6
6
|
Tooltip,
|
|
7
7
|
ResponsiveContainer,
|
|
8
|
-
|
|
8
|
+
ReferenceArea,
|
|
9
9
|
} from "recharts";
|
|
10
10
|
import { format } from "date-fns";
|
|
11
11
|
import type { HealthCheckDiagramSlotContext } from "../slots";
|
|
12
12
|
|
|
13
|
+
import type { AnomalyBaselineDto } from "@checkstack/anomaly-common";
|
|
14
|
+
|
|
13
15
|
interface HealthCheckLatencyChartProps {
|
|
14
16
|
context: HealthCheckDiagramSlotContext;
|
|
15
17
|
height?: number;
|
|
16
18
|
showAverage?: boolean;
|
|
19
|
+
baselines?: AnomalyBaselineDto[];
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
/**
|
|
@@ -23,7 +26,7 @@ interface HealthCheckLatencyChartProps {
|
|
|
23
26
|
*/
|
|
24
27
|
export const HealthCheckLatencyChart: React.FC<
|
|
25
28
|
HealthCheckLatencyChartProps
|
|
26
|
-
> = ({ context, height = 200, showAverage = true }) => {
|
|
29
|
+
> = ({ context, height = 200, showAverage = true, baselines }) => {
|
|
27
30
|
const buckets = context.buckets.filter((b) => b.avgLatencyMs !== undefined);
|
|
28
31
|
|
|
29
32
|
if (buckets.length === 0) {
|
|
@@ -37,6 +40,20 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
37
40
|
);
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
const baseline = baselines?.find((b) => b.fieldPath === "latencyMs");
|
|
44
|
+
const totalRuns = context.buckets.reduce((sum, b) => sum + (b.runCount || 1), 0);
|
|
45
|
+
const avgRunCount = Math.max(1, totalRuns / Math.max(1, context.buckets.length));
|
|
46
|
+
|
|
47
|
+
// Trend is the slope projected over the baseline window — a rate, surfaced in
|
|
48
|
+
// the header chip rather than as a chart line so it doesn't share a y-axis
|
|
49
|
+
// with absolute latency values.
|
|
50
|
+
const projectedChange = baseline ? baseline.trendSlope * baseline.sampleCount : 0;
|
|
51
|
+
const showTrend = !!baseline && Math.abs(projectedChange) > 0.01;
|
|
52
|
+
const driftSigmas = baseline && baseline.stdDev > 0
|
|
53
|
+
? Math.abs(projectedChange) / baseline.stdDev
|
|
54
|
+
: 0;
|
|
55
|
+
const isDrifting = driftSigmas >= 2;
|
|
56
|
+
|
|
40
57
|
const chartData = buckets.map((d) => ({
|
|
41
58
|
timestamp: new Date(d.bucketStart).getTime(),
|
|
42
59
|
bucketEndTimestamp: new Date(d.bucketEnd).getTime(),
|
|
@@ -57,8 +74,34 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
57
74
|
: "MMM d, HH:mm";
|
|
58
75
|
|
|
59
76
|
return (
|
|
60
|
-
<
|
|
61
|
-
|
|
77
|
+
<div className="space-y-2">
|
|
78
|
+
{showAverage && (
|
|
79
|
+
baseline ? (
|
|
80
|
+
<div className="flex items-center justify-between text-xs px-1 gap-3">
|
|
81
|
+
<span className="text-warning font-medium">
|
|
82
|
+
Expected: {baseline.mean.toFixed(0)}ms (±{((baseline.stdDev / Math.sqrt(avgRunCount)) * 3).toFixed(0)})
|
|
83
|
+
</span>
|
|
84
|
+
<div className="flex items-center gap-3">
|
|
85
|
+
{showTrend && (
|
|
86
|
+
<span className={isDrifting ? "text-warning font-medium" : "text-muted-foreground"}>
|
|
87
|
+
Trend: {projectedChange >= 0 ? "↑ +" : "↓ "}{projectedChange.toFixed(0)}ms
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
<span className="text-muted-foreground">
|
|
91
|
+
Avg: {avgLatency.toFixed(0)}ms
|
|
92
|
+
</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
) : (
|
|
96
|
+
<div className="flex items-center justify-end text-xs px-1">
|
|
97
|
+
<span className="text-muted-foreground">
|
|
98
|
+
Avg: {avgLatency.toFixed(0)}ms
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
)}
|
|
103
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
104
|
+
<AreaChart data={chartData}>
|
|
62
105
|
<defs>
|
|
63
106
|
<linearGradient id="latencyGradient" x1="0" y1="0" x2="0" y2="1">
|
|
64
107
|
<stop
|
|
@@ -109,17 +152,15 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
109
152
|
);
|
|
110
153
|
}}
|
|
111
154
|
/>
|
|
112
|
-
{
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
fontSize: 12,
|
|
122
|
-
}}
|
|
155
|
+
{baseline && (
|
|
156
|
+
<ReferenceArea
|
|
157
|
+
y1={Math.max(0, baseline.mean - (baseline.stdDev / Math.sqrt(avgRunCount)) * 3)}
|
|
158
|
+
y2={baseline.mean + (baseline.stdDev / Math.sqrt(avgRunCount)) * 3}
|
|
159
|
+
fill="hsl(var(--warning))"
|
|
160
|
+
fillOpacity={0.08}
|
|
161
|
+
stroke="hsl(var(--warning))"
|
|
162
|
+
strokeOpacity={0.4}
|
|
163
|
+
strokeWidth={1}
|
|
123
164
|
/>
|
|
124
165
|
)}
|
|
125
166
|
<Area
|
|
@@ -131,5 +172,6 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
131
172
|
/>
|
|
132
173
|
</AreaChart>
|
|
133
174
|
</ResponsiveContainer>
|
|
175
|
+
</div>
|
|
134
176
|
);
|
|
135
177
|
};
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Settings, Gauge, Database, Radio, Plus, Check } from "lucide-react";
|
|
3
3
|
import { IDETreeNode, IDETreeSection } from "@checkstack/ui";
|
|
4
|
+
import { ExtensionSlot } from "@checkstack/frontend-api";
|
|
5
|
+
import { AssignmentIDENodeSlot } from "../../slots";
|
|
4
6
|
|
|
5
7
|
// =============================================================================
|
|
6
8
|
// TYPES
|
|
7
9
|
// =============================================================================
|
|
8
10
|
|
|
9
|
-
export type AssignmentNodeId =
|
|
10
|
-
| `general:${string}`
|
|
11
|
-
| `thresholds:${string}`
|
|
12
|
-
| `retention:${string}`
|
|
13
|
-
| `execution:${string}`;
|
|
11
|
+
export type AssignmentNodeId = string;
|
|
14
12
|
|
|
15
13
|
interface AssignmentConfig {
|
|
16
14
|
configurationId: string;
|
|
@@ -20,6 +18,7 @@ interface AssignmentConfig {
|
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
interface AssignmentTreeProps {
|
|
21
|
+
systemId: string;
|
|
23
22
|
assigned: AssignmentConfig[];
|
|
24
23
|
available: Array<{ id: string; name: string; strategyId: string }>;
|
|
25
24
|
selectedNode: AssignmentNodeId | undefined;
|
|
@@ -33,6 +32,7 @@ interface AssignmentTreeProps {
|
|
|
33
32
|
// =============================================================================
|
|
34
33
|
|
|
35
34
|
export const AssignmentTree: React.FC<AssignmentTreeProps> = ({
|
|
35
|
+
systemId,
|
|
36
36
|
assigned,
|
|
37
37
|
available,
|
|
38
38
|
selectedNode,
|
|
@@ -97,6 +97,16 @@ export const AssignmentTree: React.FC<AssignmentTreeProps> = ({
|
|
|
97
97
|
indent
|
|
98
98
|
badge={assoc.satelliteCount > 0 ? `${assoc.satelliteCount}` : undefined}
|
|
99
99
|
/>
|
|
100
|
+
<ExtensionSlot
|
|
101
|
+
slot={AssignmentIDENodeSlot}
|
|
102
|
+
context={{
|
|
103
|
+
systemId,
|
|
104
|
+
configurationId: assoc.configurationId,
|
|
105
|
+
selectedNode,
|
|
106
|
+
onSelectNode,
|
|
107
|
+
isLocked
|
|
108
|
+
}}
|
|
109
|
+
/>
|
|
100
110
|
</div>
|
|
101
111
|
))}
|
|
102
112
|
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
IDETreeSection,
|
|
11
11
|
type ValidationIssue,
|
|
12
12
|
} from "@checkstack/ui";
|
|
13
|
+
import { ExtensionSlot } from "@checkstack/frontend-api";
|
|
14
|
+
import { HealthCheckConfigIDENodeSlot } from "../../slots";
|
|
13
15
|
|
|
14
16
|
// =============================================================================
|
|
15
17
|
// TYPES
|
|
@@ -19,7 +21,8 @@ export type TreeNodeId =
|
|
|
19
21
|
| "general"
|
|
20
22
|
| "access"
|
|
21
23
|
| "collector-picker"
|
|
22
|
-
| `collector:${string}
|
|
24
|
+
| `collector:${string}`
|
|
25
|
+
| (string & {});
|
|
23
26
|
|
|
24
27
|
interface EditorTreeProps {
|
|
25
28
|
collectors: CollectorConfigEntry[];
|
|
@@ -29,6 +32,7 @@ interface EditorTreeProps {
|
|
|
29
32
|
onAddCollector: (collectorId: string) => void;
|
|
30
33
|
validationIssues: ValidationIssue[];
|
|
31
34
|
strategyId: string;
|
|
35
|
+
configId?: string;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
// =============================================================================
|
|
@@ -42,6 +46,7 @@ export const EditorTree: React.FC<EditorTreeProps> = ({
|
|
|
42
46
|
onSelectNode,
|
|
43
47
|
validationIssues,
|
|
44
48
|
strategyId,
|
|
49
|
+
configId,
|
|
45
50
|
}) => {
|
|
46
51
|
// Check if there are addable collectors remaining
|
|
47
52
|
const hasAddableCollectors = useMemo(() => {
|
|
@@ -115,6 +120,19 @@ export const EditorTree: React.FC<EditorTreeProps> = ({
|
|
|
115
120
|
onClick={() => onSelectNode("access")}
|
|
116
121
|
issues={validationIssues}
|
|
117
122
|
/>
|
|
123
|
+
|
|
124
|
+
{/* Plugin Configuration Slots */}
|
|
125
|
+
{configId && (
|
|
126
|
+
<ExtensionSlot
|
|
127
|
+
slot={HealthCheckConfigIDENodeSlot}
|
|
128
|
+
context={{
|
|
129
|
+
configurationId: configId,
|
|
130
|
+
strategyId,
|
|
131
|
+
selectedNode,
|
|
132
|
+
onSelectNode,
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
118
136
|
</div>
|
|
119
137
|
);
|
|
120
138
|
};
|
package/src/index.tsx
CHANGED
|
@@ -29,13 +29,20 @@ import {
|
|
|
29
29
|
// Export slot definitions for other plugins to use
|
|
30
30
|
export {
|
|
31
31
|
HealthCheckDiagramSlot,
|
|
32
|
-
|
|
32
|
+
AssignmentIDENodeSlot,
|
|
33
|
+
AssignmentIDEPanelSlot,
|
|
34
|
+
HealthCheckConfigIDENodeSlot,
|
|
35
|
+
HealthCheckConfigIDEPanelSlot,
|
|
33
36
|
type HealthCheckDiagramSlotContext,
|
|
37
|
+
type AssignmentIDEContext,
|
|
38
|
+
type HealthCheckConfigIDEContext,
|
|
39
|
+
createDiagramExtensionFactory,
|
|
34
40
|
type TypedAggregatedBucket,
|
|
35
41
|
} from "./slots";
|
|
36
42
|
|
|
37
43
|
// Export hooks for reusable data fetching
|
|
38
44
|
export { useHealthCheckData } from "./hooks";
|
|
45
|
+
export { useStrategySchemas } from "./auto-charts/useStrategySchemas";
|
|
39
46
|
|
|
40
47
|
export default createFrontendPlugin({
|
|
41
48
|
metadata: pluginMetadata,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo } from "react";
|
|
2
2
|
import { useParams, useNavigate } from "react-router-dom";
|
|
3
|
-
import { usePluginClient, wrapInSuspense } from "@checkstack/frontend-api";
|
|
3
|
+
import { usePluginClient, wrapInSuspense, ExtensionSlot } from "@checkstack/frontend-api";
|
|
4
4
|
import { HealthCheckApi } from "../api";
|
|
5
5
|
import { SatelliteApi } from "@checkstack/satellite-common";
|
|
6
6
|
import {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type RetentionData,
|
|
25
25
|
} from "../components/assignments/RetentionPanel";
|
|
26
26
|
import { ExecutionPanel } from "../components/assignments/ExecutionPanel";
|
|
27
|
+
import { AssignmentIDEPanelSlot } from "../slots";
|
|
27
28
|
|
|
28
29
|
// =============================================================================
|
|
29
30
|
// HELPERS
|
|
@@ -448,6 +449,20 @@ const AssignmentIDEPageContent = () => {
|
|
|
448
449
|
/>
|
|
449
450
|
);
|
|
450
451
|
}
|
|
452
|
+
default: {
|
|
453
|
+
return (
|
|
454
|
+
<ExtensionSlot
|
|
455
|
+
slot={AssignmentIDEPanelSlot}
|
|
456
|
+
context={{
|
|
457
|
+
systemId: systemId ?? "",
|
|
458
|
+
configurationId: configId,
|
|
459
|
+
selectedNode,
|
|
460
|
+
onSelectNode: setSelectedNode,
|
|
461
|
+
isLocked
|
|
462
|
+
}}
|
|
463
|
+
/>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
451
466
|
}
|
|
452
467
|
};
|
|
453
468
|
|
|
@@ -480,6 +495,7 @@ const AssignmentIDEPageContent = () => {
|
|
|
480
495
|
<IDELayout
|
|
481
496
|
tree={
|
|
482
497
|
<AssignmentTree
|
|
498
|
+
systemId={systemId ?? ""}
|
|
483
499
|
assigned={assignedConfigs}
|
|
484
500
|
available={availableConfigs}
|
|
485
501
|
selectedNode={selectedNode}
|
|
@@ -121,7 +121,7 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
121
121
|
loading={accessLoading}
|
|
122
122
|
allowed={canManage}
|
|
123
123
|
actions={
|
|
124
|
-
<BackLink
|
|
124
|
+
<BackLink onClick={() => navigate(resolveRoute(healthcheckRoutes.routes.history))}>
|
|
125
125
|
Back to All History
|
|
126
126
|
</BackLink>
|
|
127
127
|
}
|
|
@@ -3,8 +3,10 @@ import { useParams, useSearchParams, useNavigate } from "react-router-dom";
|
|
|
3
3
|
import {
|
|
4
4
|
usePluginClient,
|
|
5
5
|
wrapInSuspense,
|
|
6
|
+
ExtensionSlot,
|
|
6
7
|
} from "@checkstack/frontend-api";
|
|
7
8
|
import { HealthCheckApi } from "../api";
|
|
9
|
+
import { HealthCheckConfigIDEPanelSlot } from "../slots";
|
|
8
10
|
import {
|
|
9
11
|
healthcheckRoutes,
|
|
10
12
|
type CollectorConfigEntry,
|
|
@@ -349,32 +351,47 @@ const HealthCheckIDEPageContent = () => {
|
|
|
349
351
|
onAddCollector={handleCollectorAdd}
|
|
350
352
|
validationIssues={validationIssues}
|
|
351
353
|
strategyId={activeStrategyId ?? ""}
|
|
354
|
+
configId={configId}
|
|
352
355
|
/>
|
|
353
356
|
}
|
|
354
357
|
panel={
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
358
|
+
<>
|
|
359
|
+
<EditorPanel
|
|
360
|
+
selectedNode={selectedNode}
|
|
361
|
+
formState={formState}
|
|
362
|
+
strategy={activeStrategy}
|
|
363
|
+
availableCollectors={availableCollectors}
|
|
364
|
+
collectorsLoading={collectorsLoading}
|
|
365
|
+
isEditMode={isEditMode}
|
|
366
|
+
configId={configId}
|
|
367
|
+
onNameChange={(name) => updateField("name", name)}
|
|
368
|
+
onIntervalChange={(interval) =>
|
|
369
|
+
updateField("intervalSeconds", interval)
|
|
370
|
+
}
|
|
371
|
+
onStrategyConfigChange={(config) =>
|
|
372
|
+
updateField("strategyConfig", config)
|
|
373
|
+
}
|
|
374
|
+
onStrategyConfigValidChange={setStrategyConfigValid}
|
|
375
|
+
onCollectorConfigChange={handleCollectorConfigChange}
|
|
376
|
+
onCollectorAssertionsChange={handleCollectorAssertionsChange}
|
|
377
|
+
onCollectorValidChange={handleCollectorValidChange}
|
|
378
|
+
onCollectorRemove={handleCollectorRemove}
|
|
379
|
+
onCollectorAdd={handleCollectorAdd}
|
|
380
|
+
strategyId={activeStrategyId ?? ""}
|
|
381
|
+
/>
|
|
382
|
+
{configId && (
|
|
383
|
+
<ExtensionSlot
|
|
384
|
+
slot={HealthCheckConfigIDEPanelSlot}
|
|
385
|
+
context={{
|
|
386
|
+
configurationId: configId,
|
|
387
|
+
strategyId: activeStrategyId ?? "",
|
|
388
|
+
selectedNode,
|
|
389
|
+
onSelectNode: setSelectedNode,
|
|
390
|
+
isLocked,
|
|
391
|
+
}}
|
|
392
|
+
/>
|
|
393
|
+
)}
|
|
394
|
+
</>
|
|
378
395
|
}
|
|
379
396
|
issues={validationIssues}
|
|
380
397
|
onIssueClick={(nodeId) => setSelectedNode(nodeId as TreeNodeId)}
|
package/src/slots.tsx
CHANGED
|
@@ -44,6 +44,47 @@ export const HealthCheckDiagramSlot = createSlot<HealthCheckDiagramSlotContext>(
|
|
|
44
44
|
"healthcheck.diagram",
|
|
45
45
|
);
|
|
46
46
|
|
|
47
|
+
export interface AssignmentIDEContext {
|
|
48
|
+
systemId: string;
|
|
49
|
+
configurationId: string;
|
|
50
|
+
selectedNode: string | undefined;
|
|
51
|
+
onSelectNode: (nodeId: string) => void;
|
|
52
|
+
isLocked?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extension slot for adding items to the Assignment IDE tree
|
|
57
|
+
*/
|
|
58
|
+
export const AssignmentIDENodeSlot = createSlot<AssignmentIDEContext>(
|
|
59
|
+
"healthcheck.assignment.ide.node"
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
export const AssignmentIDEPanelSlot = createSlot<AssignmentIDEContext>(
|
|
63
|
+
"healthcheck.assignment.ide.panel"
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
export interface HealthCheckConfigIDEContext {
|
|
67
|
+
configurationId: string;
|
|
68
|
+
strategyId: string;
|
|
69
|
+
selectedNode: string | undefined;
|
|
70
|
+
onSelectNode: (nodeId: string) => void;
|
|
71
|
+
isLocked?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extension slot for adding items to the Health Check Configuration IDE tree
|
|
76
|
+
*/
|
|
77
|
+
export const HealthCheckConfigIDENodeSlot = createSlot<HealthCheckConfigIDEContext>(
|
|
78
|
+
"healthcheck.config.ide.node"
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extension slot for rendering the panel of a Health Check Configuration IDE item
|
|
83
|
+
*/
|
|
84
|
+
export const HealthCheckConfigIDEPanelSlot = createSlot<HealthCheckConfigIDEContext>(
|
|
85
|
+
"healthcheck.config.ide.panel"
|
|
86
|
+
);
|
|
87
|
+
|
|
47
88
|
// =============================================================================
|
|
48
89
|
// DIAGRAM EXTENSION FACTORY
|
|
49
90
|
// =============================================================================
|