@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.
@@ -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
- const {
79
- data: aggregatedData,
80
- isLoading,
81
- refetch,
82
- } = healthCheckClient.getDetailedAggregatedHistory.useQuery(
83
- {
84
- systemId,
85
- configurationId,
86
- startDate: dateRange.startDate,
87
- endDate: dateRange.endDate,
88
- sourceFilter,
89
- targetPoints: 500,
90
- },
91
- {
92
- enabled: !!systemId && !!configurationId && hasAccess && !accessLoading,
93
- // Keep previous data visible during refetch to prevent layout shift
94
- placeholderData: (prev) => prev,
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
- // Listen for realtime health check updates to refresh data silently
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 (changedId === systemId && hasAccess && !accessLoading) {
101
- // Update endDate to current time only for rolling presets (not custom ranges)
102
- if (isRollingPreset && onDateRangeRefresh) {
103
- onDateRangeRefresh(new Date());
104
- }
105
- void refetch();
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
- <BackLink
484
- onClick={() => navigate(resolveRoute(catalogRoutes.routes.config))}
485
- >
486
- Back to Systems
487
- </BackLink>
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
- toast.success("Health check created");
257
- navigate(resolveRoute(healthcheckRoutes.routes.config));
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" })}?strategy=${encodeURIComponent(strategy.id)}`,
107
+ `${resolveRoute(healthcheckRoutes.routes.edit, { configId: "new" })}?${params.toString()}`,
101
108
  );
102
109
  };
103
110