@checkstack/healthcheck-frontend 0.17.1 → 0.18.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 +59 -0
- package/package.json +12 -12
- package/src/auto-charts/AutoChartGrid.tsx +266 -69
- package/src/components/HealthCheckDrawer.tsx +3 -15
- package/src/components/HealthCheckStatusTimeline.tsx +164 -72
- package/src/components/HealthCheckSystemOverview.tsx +7 -27
- package/src/components/SystemHealthBadge.tsx +9 -23
- package/src/components/editor/EditorPanel.tsx +25 -0
- package/src/components/editor/EditorTree.tsx +23 -1
- package/src/components/editor/SystemsSection.tsx +126 -0
- package/src/hooks/useHealthCheckData.ts +30 -27
- package/src/pages/AssignmentIDEPage.tsx +23 -7
- package/src/pages/HealthCheckIDEPage.tsx +77 -3
- package/src/pages/StrategyPickerPage.tsx +9 -2
|
@@ -74,35 +74,38 @@ export function useHealthCheckData({
|
|
|
74
74
|
healthCheckAccess.details,
|
|
75
75
|
);
|
|
76
76
|
|
|
77
|
-
// Always use aggregated data with fixed target points
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
77
|
+
// Always use aggregated data with fixed target points.
|
|
78
|
+
// Realtime refetches happen via SignalAutoInvalidator (auto-invalidates
|
|
79
|
+
// `[["healthcheck"]]` on HEALTH_CHECK_RUN_COMPLETED).
|
|
80
|
+
const { data: aggregatedData, isLoading } =
|
|
81
|
+
healthCheckClient.getDetailedAggregatedHistory.useQuery(
|
|
82
|
+
{
|
|
83
|
+
systemId,
|
|
84
|
+
configurationId,
|
|
85
|
+
startDate: dateRange.startDate,
|
|
86
|
+
endDate: dateRange.endDate,
|
|
87
|
+
sourceFilter,
|
|
88
|
+
targetPoints: 500,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
enabled: !!systemId && !!configurationId && hasAccess && !accessLoading,
|
|
92
|
+
// Keep previous data visible during refetch to prevent layout shift
|
|
93
|
+
placeholderData: (prev) => prev,
|
|
94
|
+
},
|
|
95
|
+
);
|
|
97
96
|
|
|
98
|
-
//
|
|
97
|
+
// For rolling presets, we still need an explicit signal handler to advance
|
|
98
|
+
// the endDate alongside cache invalidation — this is UI state (date range),
|
|
99
|
+
// not cache, so auto-invalidation is not enough.
|
|
99
100
|
useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
if (
|
|
102
|
+
changedId === systemId &&
|
|
103
|
+
hasAccess &&
|
|
104
|
+
!accessLoading &&
|
|
105
|
+
isRollingPreset &&
|
|
106
|
+
onDateRangeRefresh
|
|
107
|
+
) {
|
|
108
|
+
onDateRangeRefresh(new Date());
|
|
106
109
|
}
|
|
107
110
|
});
|
|
108
111
|
|
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
DEFAULT_RETENTION_CONFIG,
|
|
9
9
|
} from "@checkstack/healthcheck-common";
|
|
10
10
|
import type { StateThresholds } from "@checkstack/healthcheck-common";
|
|
11
|
-
import { PageLayout, IDELayout, useToast, BackLink } from "@checkstack/ui";
|
|
12
|
-
import { Settings } from "lucide-react";
|
|
11
|
+
import { PageLayout, IDELayout, useToast, BackLink, Button } from "@checkstack/ui";
|
|
12
|
+
import { Settings, Plus } from "lucide-react";
|
|
13
13
|
import { extractErrorMessage, resolveRoute } from "@checkstack/common";
|
|
14
14
|
import { catalogRoutes } from "@checkstack/catalog-common";
|
|
15
|
+
import { healthcheckRoutes } from "@checkstack/healthcheck-common";
|
|
15
16
|
import {
|
|
16
17
|
AssignmentTree,
|
|
17
18
|
type AssignmentNodeId,
|
|
@@ -480,11 +481,26 @@ const AssignmentIDEPageContent = () => {
|
|
|
480
481
|
icon={Settings}
|
|
481
482
|
maxWidth="full"
|
|
482
483
|
actions={
|
|
483
|
-
<
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
484
|
+
<div className="flex items-center gap-2">
|
|
485
|
+
{!isLocked && systemId && (
|
|
486
|
+
<Button
|
|
487
|
+
size="sm"
|
|
488
|
+
onClick={() =>
|
|
489
|
+
navigate(
|
|
490
|
+
`${resolveRoute(healthcheckRoutes.routes.create)}?systemId=${encodeURIComponent(systemId)}`,
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
>
|
|
494
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
495
|
+
Create new check
|
|
496
|
+
</Button>
|
|
497
|
+
)}
|
|
498
|
+
<BackLink
|
|
499
|
+
onClick={() => navigate(resolveRoute(catalogRoutes.routes.config))}
|
|
500
|
+
>
|
|
501
|
+
Back to Systems
|
|
502
|
+
</BackLink>
|
|
503
|
+
</div>
|
|
488
504
|
}
|
|
489
505
|
>
|
|
490
506
|
{isLocked && provenance && (
|
|
@@ -10,7 +10,9 @@ import { HealthCheckConfigIDEPanelSlot } from "../slots";
|
|
|
10
10
|
import {
|
|
11
11
|
healthcheckRoutes,
|
|
12
12
|
type CollectorConfigEntry,
|
|
13
|
+
DEFAULT_STATE_THRESHOLDS,
|
|
13
14
|
} from "@checkstack/healthcheck-common";
|
|
15
|
+
import { CatalogApi } from "@checkstack/catalog-common";
|
|
14
16
|
import { PageLayout, Button, useToast, IDELayout, type ValidationIssue } from "@checkstack/ui";
|
|
15
17
|
import { Save, Settings } from "lucide-react";
|
|
16
18
|
import { resolveRoute, extractErrorMessage} from "@checkstack/common";
|
|
@@ -44,6 +46,8 @@ const HealthCheckIDEPageContent = () => {
|
|
|
44
46
|
|
|
45
47
|
const isEditMode = !!configId && configId !== "new";
|
|
46
48
|
const strategyIdFromUrl = searchParams.get("strategy") ?? undefined;
|
|
49
|
+
const systemIdFromUrl = searchParams.get("systemId") ?? undefined;
|
|
50
|
+
const catalogClient = usePluginClient(CatalogApi);
|
|
47
51
|
|
|
48
52
|
// --- GitOps Provenance Lock ---
|
|
49
53
|
const { isLocked, provenance } = useProvenanceLock({
|
|
@@ -79,6 +83,19 @@ const HealthCheckIDEPageContent = () => {
|
|
|
79
83
|
const { collectors: availableCollectors, loading: collectorsLoading } =
|
|
80
84
|
useCollectors(activeStrategyId ?? "");
|
|
81
85
|
|
|
86
|
+
// Fetch systems for assignment (only in create mode)
|
|
87
|
+
const { data: systemsData, isLoading: systemsLoading } =
|
|
88
|
+
catalogClient.getSystems.useQuery({}, { enabled: !isEditMode });
|
|
89
|
+
const systems = useMemo(
|
|
90
|
+
() =>
|
|
91
|
+
(systemsData?.systems ?? []).map((s) => ({
|
|
92
|
+
id: s.id,
|
|
93
|
+
name: s.name,
|
|
94
|
+
description: s.description,
|
|
95
|
+
})),
|
|
96
|
+
[systemsData],
|
|
97
|
+
);
|
|
98
|
+
|
|
82
99
|
// --- Form State ---
|
|
83
100
|
|
|
84
101
|
const [formState, setFormState] = useState<EditorFormState>({
|
|
@@ -94,6 +111,9 @@ const HealthCheckIDEPageContent = () => {
|
|
|
94
111
|
Record<string, boolean>
|
|
95
112
|
>({});
|
|
96
113
|
const [isDirty, setIsDirty] = useState(false);
|
|
114
|
+
const [selectedSystemIds, setSelectedSystemIds] = useState<string[]>(
|
|
115
|
+
systemIdFromUrl ? [systemIdFromUrl] : [],
|
|
116
|
+
);
|
|
97
117
|
|
|
98
118
|
// Initialize form from existing configuration (edit mode)
|
|
99
119
|
useEffect(() => {
|
|
@@ -250,11 +270,55 @@ const HealthCheckIDEPageContent = () => {
|
|
|
250
270
|
|
|
251
271
|
// --- Save ---
|
|
252
272
|
|
|
273
|
+
const associateMutation = healthCheckClient.associateSystem.useMutation();
|
|
274
|
+
|
|
253
275
|
const createMutation = healthCheckClient.createConfiguration.useMutation({
|
|
254
|
-
onSuccess: () => {
|
|
276
|
+
onSuccess: async (created) => {
|
|
255
277
|
setIsDirty(false);
|
|
256
|
-
|
|
257
|
-
|
|
278
|
+
|
|
279
|
+
// Fan-out: assign the new config to each selected system.
|
|
280
|
+
if (selectedSystemIds.length > 0 && created?.id) {
|
|
281
|
+
const results = await Promise.allSettled(
|
|
282
|
+
selectedSystemIds.map((systemId) =>
|
|
283
|
+
associateMutation.mutateAsync({
|
|
284
|
+
systemId,
|
|
285
|
+
body: {
|
|
286
|
+
configurationId: created.id,
|
|
287
|
+
enabled: true,
|
|
288
|
+
stateThresholds: DEFAULT_STATE_THRESHOLDS,
|
|
289
|
+
includeLocal: true,
|
|
290
|
+
},
|
|
291
|
+
}),
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
const failed = results.filter((r) => r.status === "rejected").length;
|
|
295
|
+
if (failed > 0) {
|
|
296
|
+
toast.error(
|
|
297
|
+
`Health check created, but ${failed} of ${selectedSystemIds.length} system assignment${selectedSystemIds.length === 1 ? "" : "s"} failed.`,
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
toast.success(
|
|
301
|
+
`Health check created and assigned to ${selectedSystemIds.length} system${selectedSystemIds.length === 1 ? "" : "s"}`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
toast.success("Health check created");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Where to land: prefer back to the originating system's assignment IDE
|
|
309
|
+
// when the user came from there, otherwise the config list.
|
|
310
|
+
if (
|
|
311
|
+
systemIdFromUrl &&
|
|
312
|
+
selectedSystemIds.includes(systemIdFromUrl)
|
|
313
|
+
) {
|
|
314
|
+
navigate(
|
|
315
|
+
resolveRoute(healthcheckRoutes.routes.assignments, {
|
|
316
|
+
systemId: systemIdFromUrl,
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
} else {
|
|
320
|
+
navigate(resolveRoute(healthcheckRoutes.routes.config));
|
|
321
|
+
}
|
|
258
322
|
},
|
|
259
323
|
onError: (error) => {
|
|
260
324
|
toast.error(extractErrorMessage(error, "Failed to create"));
|
|
@@ -352,6 +416,8 @@ const HealthCheckIDEPageContent = () => {
|
|
|
352
416
|
validationIssues={validationIssues}
|
|
353
417
|
strategyId={activeStrategyId ?? ""}
|
|
354
418
|
configId={configId}
|
|
419
|
+
showSystemsNode={!isEditMode}
|
|
420
|
+
selectedSystemCount={selectedSystemIds.length}
|
|
355
421
|
/>
|
|
356
422
|
}
|
|
357
423
|
panel={
|
|
@@ -378,6 +444,14 @@ const HealthCheckIDEPageContent = () => {
|
|
|
378
444
|
onCollectorRemove={handleCollectorRemove}
|
|
379
445
|
onCollectorAdd={handleCollectorAdd}
|
|
380
446
|
strategyId={activeStrategyId ?? ""}
|
|
447
|
+
showSystemsSection={!isEditMode}
|
|
448
|
+
systems={systems}
|
|
449
|
+
systemsLoading={systemsLoading}
|
|
450
|
+
selectedSystemIds={selectedSystemIds}
|
|
451
|
+
onSystemsChange={(ids) => {
|
|
452
|
+
setSelectedSystemIds(ids);
|
|
453
|
+
setIsDirty(true);
|
|
454
|
+
}}
|
|
381
455
|
/>
|
|
382
456
|
{configId && (
|
|
383
457
|
<ExtensionSlot
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
Badge,
|
|
17
17
|
} from "@checkstack/ui";
|
|
18
18
|
import { Search, Zap } from "lucide-react";
|
|
19
|
-
import { useNavigate } from "react-router-dom";
|
|
19
|
+
import { useNavigate, useSearchParams } from "react-router-dom";
|
|
20
20
|
import { resolveRoute } from "@checkstack/common";
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -61,6 +61,8 @@ function StrategyCard({
|
|
|
61
61
|
const StrategyPickerPageContent = () => {
|
|
62
62
|
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
63
63
|
const navigate = useNavigate();
|
|
64
|
+
const [urlParams] = useSearchParams();
|
|
65
|
+
const systemIdFromUrl = urlParams.get("systemId");
|
|
64
66
|
const [searchQuery, setSearchQuery] = useState("");
|
|
65
67
|
|
|
66
68
|
const { data: strategies = [] } = healthCheckClient.getStrategies.useQuery(
|
|
@@ -96,8 +98,13 @@ const StrategyPickerPageContent = () => {
|
|
|
96
98
|
}, [strategies, searchQuery]);
|
|
97
99
|
|
|
98
100
|
const handleSelectStrategy = (strategy: HealthCheckStrategyDto) => {
|
|
101
|
+
const params = new URLSearchParams();
|
|
102
|
+
params.set("strategy", strategy.id);
|
|
103
|
+
if (systemIdFromUrl) {
|
|
104
|
+
params.set("systemId", systemIdFromUrl);
|
|
105
|
+
}
|
|
99
106
|
navigate(
|
|
100
|
-
`${resolveRoute(healthcheckRoutes.routes.edit, { configId: "new" })}
|
|
107
|
+
`${resolveRoute(healthcheckRoutes.routes.edit, { configId: "new" })}?${params.toString()}`,
|
|
101
108
|
);
|
|
102
109
|
};
|
|
103
110
|
|