@checkstack/healthcheck-frontend 0.17.0 → 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.
@@ -8,7 +8,7 @@
8
8
  import type { ChartField } from "./schema-parser";
9
9
  import { extractChartFields, getFieldValue } from "./schema-parser";
10
10
  import { useStrategySchemas } from "./useStrategySchemas";
11
- import { Card, CardContent, CardHeader, CardTitle } from "@checkstack/ui";
11
+ import { Badge, Card, CardContent, CardHeader, CardTitle } from "@checkstack/ui";
12
12
  import { RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
13
13
 
14
14
  interface SingleRunChartGridProps {
@@ -62,7 +62,7 @@ export function SingleRunChartGrid({
62
62
  <div className="space-y-6">
63
63
  {/* Strategy-level fields */}
64
64
  {strategyFields.length > 0 && (
65
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
65
+ <div className="space-y-4">
66
66
  {strategyFields.map((field) => (
67
67
  <SingleValueCard
68
68
  key={field.name}
@@ -124,12 +124,13 @@ function CollectorSection({
124
124
 
125
125
  return (
126
126
  <div className="space-y-4">
127
- <div className="flex items-center gap-2">
128
- <h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
129
- {displayName}
130
- </h4>
131
- <span className="text-xs text-muted-foreground">
132
- ({instanceId.slice(0, 8)})
127
+ <div className="flex items-center gap-2 flex-wrap border-b pb-2">
128
+ <h3 className="text-lg font-semibold capitalize">{displayName}</h3>
129
+ <Badge variant="outline" className="font-mono">
130
+ {collectorId}
131
+ </Badge>
132
+ <span className="text-xs text-muted-foreground font-mono">
133
+ {instanceId.slice(0, 8)}
133
134
  </span>
134
135
  </div>
135
136
 
@@ -165,7 +166,7 @@ function CollectorSection({
165
166
  </Card>
166
167
  )}
167
168
 
168
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
169
+ <div className="space-y-4">
169
170
  {fields.map((field) => (
170
171
  <SingleValueCard
171
172
  key={field.name}
@@ -190,9 +191,11 @@ function SingleValueCard({ field, value }: SingleValueCardProps) {
190
191
  return (
191
192
  <Card>
192
193
  <CardHeader className="pb-2">
193
- <CardTitle className="text-sm font-medium">{field.label}</CardTitle>
194
+ <CardTitle className="text-sm font-medium text-center">
195
+ {field.label}
196
+ </CardTitle>
194
197
  </CardHeader>
195
- <CardContent>
198
+ <CardContent className="flex flex-col items-center text-center [&>*]:w-full">
196
199
  <SingleValueRenderer field={field} value={value} />
197
200
  </CardContent>
198
201
  </Card>
@@ -292,7 +295,7 @@ function GaugeRenderer({ value, unit }: { value: unknown; unit?: string }) {
292
295
  const data = [{ name: "value", value: clampedValue, fill: fillColor }];
293
296
 
294
297
  return (
295
- <div className="flex items-center gap-3">
298
+ <div className="flex items-center justify-center gap-3">
296
299
  <ResponsiveContainer width={80} height={80}>
297
300
  <RadialBarChart
298
301
  cx="50%"
@@ -330,7 +333,7 @@ function BooleanRenderer({ value }: { value: unknown }) {
330
333
  const boolValue = Boolean(value);
331
334
 
332
335
  return (
333
- <div className="flex items-center gap-2">
336
+ <div className="flex items-center justify-center gap-2">
334
337
  <div
335
338
  className={`w-3 h-3 rounded-full ${
336
339
  boolValue ? "bg-green-500" : "bg-red-500"
@@ -108,12 +108,9 @@ function extractFieldsFromProperties(
108
108
  if (!chartType) continue;
109
109
 
110
110
  // Use just field name - collectorId is stored separately for data lookup
111
+ // and surfaced via a separate badge in the group header
111
112
  const field = extractSingleField(fieldName, prop);
112
113
  field.collectorId = collectorId;
113
- // Prefix label with collector ID for clarity
114
- if (!prop["x-chart-label"]?.includes(collectorId)) {
115
- field.label = `${collectorId}: ${field.label}`;
116
- }
117
114
  fields.push(field);
118
115
  }
119
116
 
@@ -7,9 +7,7 @@ import {
7
7
  useApi,
8
8
  accessApiRef,
9
9
  } from "@checkstack/frontend-api";
10
- import { useSignal } from "@checkstack/signal-frontend";
11
10
  import {
12
- HEALTH_CHECK_RUN_COMPLETED,
13
11
  HealthCheckApi,
14
12
  healthCheckAccess,
15
13
  healthcheckRoutes,
@@ -199,11 +197,8 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
199
197
  // Pagination for history table
200
198
  const pagination = usePagination({ defaultLimit: 5 });
201
199
 
202
- const {
203
- data: historyData,
204
- isLoading: historyLoading,
205
- refetch,
206
- } = healthCheckClient.getHistory.useQuery({
200
+ const { data: historyData, isLoading: historyLoading } =
201
+ healthCheckClient.getHistory.useQuery({
207
202
  systemId,
208
203
  configurationId: item.configurationId,
209
204
  limit: pagination.limit,
@@ -226,13 +221,6 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
226
221
  }
227
222
  const runs = displayRuns;
228
223
 
229
- // Realtime updates
230
- useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
231
- if (changedId === systemId) {
232
- void refetch();
233
- }
234
- });
235
-
236
224
  return (
237
225
  <Sheet open={open} onOpenChange={onOpenChange}>
238
226
  <SheetContent size="lg">
@@ -417,7 +405,7 @@ export const HealthCheckDrawer: React.FC<HealthCheckDrawerProps> = ({
417
405
  <CardContent>
418
406
  <HealthCheckStatusTimeline
419
407
  context={chartContext}
420
- height={40}
408
+ height={96}
421
409
  />
422
410
  </CardContent>
423
411
  </Card>
@@ -1,6 +1,14 @@
1
1
  import { format } from "date-fns";
2
+ import { usePerformance } from "@checkstack/ui";
3
+ import {
4
+ Bar,
5
+ BarChart,
6
+ ResponsiveContainer,
7
+ Tooltip,
8
+ XAxis,
9
+ YAxis,
10
+ } from "recharts";
2
11
  import type { HealthCheckDiagramSlotContext } from "../slots";
3
- import { SparklineTooltip } from "./SparklineTooltip";
4
12
 
5
13
  interface HealthCheckStatusTimelineProps {
6
14
  context: HealthCheckDiagramSlotContext;
@@ -13,19 +21,114 @@ const statusColors = {
13
21
  unhealthy: "hsl(var(--destructive))",
14
22
  };
15
23
 
24
+ interface TimelineDatum {
25
+ bucketStart: number;
26
+ bucketEnd: number;
27
+ healthy: number;
28
+ degraded: number;
29
+ unhealthy: number;
30
+ runCount: number;
31
+ }
32
+
33
+ const MAX_TIMELINE_BARS = 60;
34
+
35
+ function downsampleTimeline(
36
+ buckets: HealthCheckDiagramSlotContext["buckets"],
37
+ ): TimelineDatum[] {
38
+ if (buckets.length === 0) return [];
39
+
40
+ const groupSize = Math.max(1, Math.ceil(buckets.length / MAX_TIMELINE_BARS));
41
+ const result: TimelineDatum[] = [];
42
+
43
+ for (let i = 0; i < buckets.length; i += groupSize) {
44
+ const slice = buckets.slice(i, i + groupSize);
45
+ const first = slice[0];
46
+ const last = slice.at(-1) ?? first;
47
+ let healthy = 0;
48
+ let degraded = 0;
49
+ let unhealthy = 0;
50
+ let runCount = 0;
51
+ for (const b of slice) {
52
+ healthy += b.healthyCount;
53
+ degraded += b.degradedCount;
54
+ unhealthy += b.unhealthyCount;
55
+ runCount += b.runCount;
56
+ }
57
+ result.push({
58
+ bucketStart: new Date(first.bucketStart).getTime(),
59
+ bucketEnd: new Date(last.bucketEnd).getTime(),
60
+ healthy,
61
+ degraded,
62
+ unhealthy,
63
+ runCount,
64
+ });
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ function pickTickFormat(intervalSeconds: number): string {
71
+ if (intervalSeconds >= 86_400) return "MMM d";
72
+ if (intervalSeconds >= 3600) return "MMM d HH:mm";
73
+ return "HH:mm";
74
+ }
75
+
76
+ function StatusTimelineTooltip({
77
+ active,
78
+ payload,
79
+ }: {
80
+ active?: boolean;
81
+ payload?: { payload: TimelineDatum }[];
82
+ }) {
83
+ if (!active || !payload?.length) return <></>;
84
+ const datum = payload[0].payload;
85
+ const span = `${format(new Date(datum.bucketStart), "MMM d, HH:mm")} – ${format(new Date(datum.bucketEnd), "HH:mm")}`;
86
+ return (
87
+ <div
88
+ className="rounded-md p-2 text-xs shadow-md"
89
+ style={{
90
+ backgroundColor: "hsl(var(--popover))",
91
+ border: "1px solid hsl(var(--border))",
92
+ color: "hsl(var(--popover-foreground))",
93
+ }}
94
+ >
95
+ <p className="text-muted-foreground mb-1">{span}</p>
96
+ <div className="space-y-0.5">
97
+ <p>
98
+ <span style={{ color: statusColors.healthy }}>●</span> Healthy:{" "}
99
+ {datum.healthy}
100
+ </p>
101
+ {datum.degraded > 0 && (
102
+ <p>
103
+ <span style={{ color: statusColors.degraded }}>●</span> Degraded:{" "}
104
+ {datum.degraded}
105
+ </p>
106
+ )}
107
+ {datum.unhealthy > 0 && (
108
+ <p>
109
+ <span style={{ color: statusColors.unhealthy }}>●</span> Unhealthy:{" "}
110
+ {datum.unhealthy}
111
+ </p>
112
+ )}
113
+ </div>
114
+ </div>
115
+ );
116
+ }
117
+
16
118
  /**
17
119
  * Timeline bar chart showing health check status distribution over time.
18
- * Each bar shows the distribution of statuses in that aggregated bucket.
120
+ * Each bar shows the stacked distribution of statuses in that aggregated bucket.
19
121
  */
20
122
  export const HealthCheckStatusTimeline: React.FC<
21
123
  HealthCheckStatusTimelineProps
22
- > = ({ context, height = 60 }) => {
23
- const buckets = context.buckets;
124
+ > = ({ context, height = 96 }) => {
125
+ const { isLowPower } = usePerformance();
126
+ const { buckets } = context;
24
127
 
25
128
  if (buckets.length === 0) {
26
129
  return (
27
130
  <div
28
- className="flex items-center justify-center text-muted-foreground"
131
+ className="flex items-center justify-center text-muted-foreground text-sm"
29
132
  style={{ height }}
30
133
  >
31
134
  No status data available
@@ -33,75 +136,64 @@ export const HealthCheckStatusTimeline: React.FC<
33
136
  );
34
137
  }
35
138
 
36
- // Use daily format for intervals >= 6 hours, otherwise include time
37
- const timeFormat =
38
- (buckets[0]?.bucketIntervalSeconds ?? 3600) >= 21_600
39
- ? "MMM d"
40
- : "MMM d HH:mm";
41
-
42
- // Calculate time range for labels
43
- const firstTime = new Date(buckets[0].bucketStart).getTime();
44
- const lastTime = new Date(buckets.at(-1)!.bucketStart).getTime();
139
+ const data = downsampleTimeline(buckets);
140
+ const effectiveIntervalSeconds =
141
+ (buckets[0]?.bucketIntervalSeconds ?? 3600) *
142
+ Math.max(1, Math.ceil(buckets.length / MAX_TIMELINE_BARS));
143
+ const tickFormat = pickTickFormat(effectiveIntervalSeconds);
45
144
 
46
145
  return (
47
- <div style={{ height }} className="flex flex-col justify-between">
48
- {/* Status strip - equal width stacked segments for each bucket */}
49
- <div className="flex h-4 gap-px rounded-md bg-muted/30">
50
- {buckets.map((bucket, index) => {
51
- const total = bucket.runCount || 1;
52
- const healthyPct = (bucket.healthyCount / total) * 100;
53
- const degradedPct = (bucket.degradedCount / total) * 100;
54
- const unhealthyPct = (bucket.unhealthyCount / total) * 100;
55
-
56
- // Use actual bucket end time from response (critical for last bucket which extends to query end)
57
- const bucketStart = new Date(bucket.bucketStart);
58
- const bucketEnd = new Date(bucket.bucketEnd);
59
- const timeSpan = `${format(bucketStart, "MMM d, HH:mm")} - ${format(bucketEnd, "HH:mm")}`;
60
-
61
- return (
62
- <SparklineTooltip
63
- key={index}
64
- content={`${timeSpan}\nHealthy: ${bucket.healthyCount}\nDegraded: ${bucket.degradedCount}\nUnhealthy: ${bucket.unhealthyCount}`}
65
- >
66
- <div className="flex-1 h-full flex flex-col cursor-pointer group">
67
- {bucket.healthyCount > 0 && (
68
- <div
69
- className="w-full transition-opacity group-hover:opacity-80"
70
- style={{
71
- height: `${healthyPct}%`,
72
- backgroundColor: statusColors.healthy,
73
- }}
74
- />
75
- )}
76
- {bucket.degradedCount > 0 && (
77
- <div
78
- className="w-full transition-opacity group-hover:opacity-80"
79
- style={{
80
- height: `${degradedPct}%`,
81
- backgroundColor: statusColors.degraded,
82
- }}
83
- />
84
- )}
85
- {bucket.unhealthyCount > 0 && (
86
- <div
87
- className="w-full transition-opacity group-hover:opacity-80"
88
- style={{
89
- height: `${unhealthyPct}%`,
90
- backgroundColor: statusColors.unhealthy,
91
- }}
92
- />
93
- )}
94
- </div>
95
- </SparklineTooltip>
96
- );
97
- })}
98
- </div>
99
-
100
- {/* Time axis labels */}
101
- <div className="flex justify-between text-xs text-muted-foreground mt-1">
102
- <span>{format(new Date(firstTime), timeFormat)}</span>
103
- <span>{format(new Date(lastTime), timeFormat)}</span>
104
- </div>
146
+ <div style={{ height, width: "100%" }}>
147
+ <ResponsiveContainer width="100%" height="100%">
148
+ <BarChart
149
+ data={data}
150
+ margin={{ top: 4, right: 4, left: 4, bottom: 0 }}
151
+ barCategoryGap={1}
152
+ >
153
+ <XAxis
154
+ dataKey="bucketStart"
155
+ tickFormatter={(value: number) => format(new Date(value), tickFormat)}
156
+ stroke="hsl(var(--muted-foreground))"
157
+ fontSize={11}
158
+ minTickGap={48}
159
+ tickLine={false}
160
+ axisLine={false}
161
+ interval="preserveStartEnd"
162
+ />
163
+ <YAxis hide domain={[0, "dataMax"]} />
164
+ <Tooltip
165
+ cursor={{ fill: "hsl(var(--muted))", fillOpacity: 0.3 }}
166
+ content={(props) => (
167
+ <StatusTimelineTooltip
168
+ active={props.active}
169
+ payload={
170
+ props.payload as unknown as
171
+ | { payload: TimelineDatum }[]
172
+ | undefined
173
+ }
174
+ />
175
+ )}
176
+ />
177
+ <Bar
178
+ dataKey="healthy"
179
+ stackId="status"
180
+ fill={statusColors.healthy}
181
+ isAnimationActive={!isLowPower}
182
+ />
183
+ <Bar
184
+ dataKey="degraded"
185
+ stackId="status"
186
+ fill={statusColors.degraded}
187
+ isAnimationActive={!isLowPower}
188
+ />
189
+ <Bar
190
+ dataKey="unhealthy"
191
+ stackId="status"
192
+ fill={statusColors.unhealthy}
193
+ isAnimationActive={!isLowPower}
194
+ />
195
+ </BarChart>
196
+ </ResponsiveContainer>
105
197
  </div>
106
198
  );
107
199
  };
@@ -1,14 +1,10 @@
1
- import React, { useState, useCallback } from "react";
1
+ import React, { useState } from "react";
2
2
  import {
3
3
  usePluginClient,
4
4
  type SlotContext,
5
5
  } from "@checkstack/frontend-api";
6
- import { useSignal } from "@checkstack/signal-frontend";
7
6
  import { SystemDetailsSlot } from "@checkstack/catalog-common";
8
- import {
9
- HEALTH_CHECK_RUN_COMPLETED,
10
- HealthCheckApi,
11
- } from "@checkstack/healthcheck-common";
7
+ import { HealthCheckApi } from "@checkstack/healthcheck-common";
12
8
  import {
13
9
  HealthBadge,
14
10
  LoadingSpinner,
@@ -63,14 +59,11 @@ export function HealthCheckSystemOverview(props: SlotProps) {
63
59
  HealthCheckOverviewItem | undefined
64
60
  >();
65
61
 
66
- // Fetch health check overview using useQuery
67
- const {
68
- data: overviewData,
69
- isLoading: initialLoading,
70
- refetch,
71
- } = healthCheckClient.getSystemHealthOverview.useQuery({
72
- systemId,
73
- });
62
+ // Fetch health check overview using useQuery — kept fresh via SignalAutoInvalidator.
63
+ const { data: overviewData, isLoading: initialLoading } =
64
+ healthCheckClient.getSystemHealthOverview.useQuery({
65
+ systemId,
66
+ });
74
67
 
75
68
  // Transform API response to component format
76
69
  const overview: HealthCheckOverviewItem[] = React.useMemo(() => {
@@ -89,19 +82,6 @@ export function HealthCheckSystemOverview(props: SlotProps) {
89
82
  }));
90
83
  }, [overviewData]);
91
84
 
92
- // Listen for realtime health check updates to refresh overview
93
- useSignal(
94
- HEALTH_CHECK_RUN_COMPLETED,
95
- useCallback(
96
- ({ systemId: changedId }) => {
97
- if (changedId === systemId) {
98
- void refetch();
99
- }
100
- },
101
- [systemId, refetch],
102
- ),
103
- );
104
-
105
85
  if (initialLoading) {
106
86
  return <LoadingSpinner />;
107
87
  }
@@ -1,8 +1,6 @@
1
1
  import React from "react";
2
2
  import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
3
- import { useSignal } from "@checkstack/signal-frontend";
4
3
  import { SystemStateBadgesSlot } from "@checkstack/catalog-common";
5
- import { SYSTEM_STATUS_CHANGED } from "@checkstack/healthcheck-common";
6
4
  import { HealthCheckApi } from "../api";
7
5
  import { HealthBadge } from "@checkstack/ui";
8
6
  import { useSystemBadgeDataOptional } from "@checkstack/dashboard-frontend";
@@ -17,37 +15,25 @@ type Props = SlotContext<typeof SystemStateBadgesSlot>;
17
15
  * When rendered within SystemBadgeDataProvider, uses bulk-fetched data.
18
16
  * Otherwise, falls back to individual fetch.
19
17
  *
20
- * Listens for realtime updates via signals.
18
+ * Realtime updates arrive via the SignalAutoInvalidator (auto-invalidates
19
+ * `[["healthcheck"]]` queries when SYSTEM_STATUS_CHANGED fires).
21
20
  */
22
21
  export const SystemHealthBadge: React.FC<Props> = ({ system }) => {
23
22
  const healthCheckClient = usePluginClient(HealthCheckApi);
24
23
  const badgeData = useSystemBadgeDataOptional();
25
24
 
26
- // Try to get data from provider first
27
25
  const providerData = badgeData?.getSystemBadgeData(system?.id ?? "");
28
26
  const providerStatus = providerData?.health?.status;
29
27
 
30
- // Query for health status if not using provider
31
- // When badgeData exists (inside provider), this query is disabled
32
- const { data: healthData, refetch } =
33
- healthCheckClient.getSystemHealthStatus.useQuery(
34
- { systemId: system?.id ?? "" },
35
- {
36
- enabled: !badgeData && !!system?.id,
37
- staleTime: 30_000, // Prevent unnecessary refetches
38
- },
39
- );
28
+ const { data: healthData } = healthCheckClient.getSystemHealthStatus.useQuery(
29
+ { systemId: system?.id ?? "" },
30
+ {
31
+ enabled: !badgeData && !!system?.id,
32
+ staleTime: 30_000,
33
+ },
34
+ );
40
35
 
41
36
  const localStatus = healthData?.status;
42
-
43
- // Listen for realtime system status changes (only in fallback mode)
44
- useSignal(SYSTEM_STATUS_CHANGED, ({ systemId: changedId }) => {
45
- if (!badgeData && changedId === system?.id) {
46
- void refetch();
47
- }
48
- });
49
-
50
- // Use provider data if available, otherwise use local state
51
37
  const status = providerStatus ?? localStatus;
52
38
 
53
39
  if (!status || status === "healthy") return <></>;
@@ -8,6 +8,7 @@ import type { TreeNodeId } from "./EditorTree";
8
8
  import { GeneralSection } from "./GeneralSection";
9
9
  import { CollectorSection } from "./CollectorSection";
10
10
  import { CollectorPicker } from "./CollectorPicker";
11
+ import { SystemsSection } from "./SystemsSection";
11
12
  import { TeamAccessEditor } from "@checkstack/auth-frontend";
12
13
 
13
14
  // =============================================================================
@@ -43,6 +44,11 @@ interface EditorPanelProps {
43
44
  onCollectorRemove: (entryId: string) => void;
44
45
  onCollectorAdd: (collectorId: string) => void;
45
46
  strategyId: string;
47
+ showSystemsSection?: boolean;
48
+ systems?: Array<{ id: string; name: string; description?: string | null }>;
49
+ systemsLoading?: boolean;
50
+ selectedSystemIds?: string[];
51
+ onSystemsChange?: (systemIds: string[]) => void;
46
52
  }
47
53
 
48
54
  // =============================================================================
@@ -67,6 +73,11 @@ export const EditorPanel: React.FC<EditorPanelProps> = ({
67
73
  onCollectorRemove,
68
74
  onCollectorAdd,
69
75
  strategyId,
76
+ showSystemsSection = false,
77
+ systems = [],
78
+ systemsLoading = false,
79
+ selectedSystemIds = [],
80
+ onSystemsChange,
70
81
  }) => {
71
82
  // --- General Section ---
72
83
  if (selectedNode === "general") {
@@ -101,6 +112,20 @@ export const EditorPanel: React.FC<EditorPanelProps> = ({
101
112
  );
102
113
  }
103
114
 
115
+ // --- Systems Section ---
116
+ if (selectedNode === "systems" && showSystemsSection) {
117
+ return (
118
+ <div className="p-6">
119
+ <SystemsSection
120
+ systems={systems}
121
+ selectedSystemIds={selectedSystemIds}
122
+ loading={systemsLoading}
123
+ onChange={(ids) => onSystemsChange?.(ids)}
124
+ />
125
+ </div>
126
+ );
127
+ }
128
+
104
129
  // --- Access Control ---
105
130
  if (selectedNode === "access") {
106
131
  return (
@@ -3,7 +3,7 @@ import type {
3
3
  CollectorConfigEntry,
4
4
  CollectorDto,
5
5
  } from "@checkstack/healthcheck-common";
6
- import { Plus, Settings, Shield, ChevronRight } from "lucide-react";
6
+ import { Plus, Settings, Shield, ChevronRight, Server } from "lucide-react";
7
7
  import { isBuiltInCollector } from "../../hooks/useCollectors";
8
8
  import {
9
9
  IDETreeNode,
@@ -20,6 +20,7 @@ import { HealthCheckConfigIDENodeSlot } from "../../slots";
20
20
  export type TreeNodeId =
21
21
  | "general"
22
22
  | "access"
23
+ | "systems"
23
24
  | "collector-picker"
24
25
  | `collector:${string}`
25
26
  | (string & {});
@@ -33,6 +34,8 @@ interface EditorTreeProps {
33
34
  validationIssues: ValidationIssue[];
34
35
  strategyId: string;
35
36
  configId?: string;
37
+ showSystemsNode?: boolean;
38
+ selectedSystemCount?: number;
36
39
  }
37
40
 
38
41
  // =============================================================================
@@ -47,6 +50,8 @@ export const EditorTree: React.FC<EditorTreeProps> = ({
47
50
  validationIssues,
48
51
  strategyId,
49
52
  configId,
53
+ showSystemsNode = false,
54
+ selectedSystemCount = 0,
50
55
  }) => {
51
56
  // Check if there are addable collectors remaining
52
57
  const hasAddableCollectors = useMemo(() => {
@@ -109,6 +114,23 @@ export const EditorTree: React.FC<EditorTreeProps> = ({
109
114
  </button>
110
115
  )}
111
116
 
117
+ {showSystemsNode && (
118
+ <>
119
+ <IDETreeSection label="Assignment" />
120
+ <IDETreeNode
121
+ nodeId="systems"
122
+ label="Systems"
123
+ icon={Server}
124
+ selected={selectedNode === "systems"}
125
+ onClick={() => onSelectNode("systems")}
126
+ issues={validationIssues}
127
+ badge={
128
+ selectedSystemCount > 0 ? String(selectedSystemCount) : undefined
129
+ }
130
+ />
131
+ </>
132
+ )}
133
+
112
134
  {/* Access Control */}
113
135
  <IDETreeSection label="Permissions" />
114
136