@checkstack/healthcheck-frontend 0.12.1 → 0.13.1

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.
@@ -0,0 +1,254 @@
1
+ import React from "react";
2
+ import type { StateThresholds } from "@checkstack/healthcheck-common";
3
+
4
+ import {
5
+ Button,
6
+ Input,
7
+ Label,
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ Tooltip,
14
+ } from "@checkstack/ui";
15
+
16
+ interface ThresholdsPanelProps {
17
+ thresholds: StateThresholds;
18
+ onChange: (thresholds: StateThresholds) => void;
19
+ onSave: () => void;
20
+ saving: boolean;
21
+ }
22
+
23
+ /**
24
+ * Panel for configuring health check evaluation thresholds.
25
+ * Supports two modes: consecutive (streak-based) and window (count in last N runs).
26
+ */
27
+ export const ThresholdsPanel: React.FC<ThresholdsPanelProps> = ({
28
+ thresholds,
29
+ onChange,
30
+ onSave,
31
+ saving,
32
+ }) => {
33
+ return (
34
+ <div className="p-6 space-y-4">
35
+ <div>
36
+ <h3 className="text-sm font-semibold">State Thresholds</h3>
37
+ <p className="text-xs text-muted-foreground mt-1">
38
+ Configure how health check results determine the system&apos;s status.
39
+ </p>
40
+ </div>
41
+
42
+ {/* Mode Selector */}
43
+ <div className="p-4 bg-muted/50 rounded-lg border">
44
+ <div className="flex items-center gap-2 mb-2">
45
+ <Label className="text-sm font-medium">Evaluation Mode</Label>
46
+ <Tooltip content="How health status is calculated based on check results" />
47
+ </div>
48
+ <Select
49
+ value={thresholds.mode}
50
+ onValueChange={(value: "consecutive" | "window") => {
51
+ if (value === "consecutive") {
52
+ onChange({
53
+ mode: "consecutive",
54
+ healthy: { minSuccessCount: 1 },
55
+ degraded: { minFailureCount: 2 },
56
+ unhealthy: { minFailureCount: 5 },
57
+ });
58
+ } else {
59
+ onChange({
60
+ mode: "window",
61
+ windowSize: 10,
62
+ degraded: { minFailureCount: 3 },
63
+ unhealthy: { minFailureCount: 7 },
64
+ });
65
+ }
66
+ }}
67
+ >
68
+ <SelectTrigger className="w-full">
69
+ <SelectValue />
70
+ </SelectTrigger>
71
+ <SelectContent>
72
+ <SelectItem value="consecutive">
73
+ Consecutive (streak-based)
74
+ </SelectItem>
75
+ <SelectItem value="window">
76
+ Window (count in last N runs)
77
+ </SelectItem>
78
+ </SelectContent>
79
+ </Select>
80
+ <p className="text-xs text-muted-foreground mt-2">
81
+ {thresholds.mode === "consecutive"
82
+ ? "Status changes when a streak of consecutive results is reached."
83
+ : "Status is based on how many failures occur within a rolling window."}
84
+ </p>
85
+ </div>
86
+
87
+ {/* Threshold Cards */}
88
+ {thresholds.mode === "consecutive" ? (
89
+ <div className="space-y-3">
90
+ <ThresholdCard
91
+ status="healthy"
92
+ label="Healthy"
93
+ tooltip="System returns to healthy after this many consecutive successful checks"
94
+ value={thresholds.healthy.minSuccessCount}
95
+ suffix="consecutive ✓"
96
+ onChange={(v) =>
97
+ onChange({ ...thresholds, healthy: { minSuccessCount: v } })
98
+ }
99
+ />
100
+ <ThresholdCard
101
+ status="warning"
102
+ label="Degraded"
103
+ tooltip="System becomes degraded after this many consecutive failures"
104
+ value={thresholds.degraded.minFailureCount}
105
+ suffix="consecutive ✗"
106
+ onChange={(v) =>
107
+ onChange({ ...thresholds, degraded: { minFailureCount: v } })
108
+ }
109
+ />
110
+ <ThresholdCard
111
+ status="destructive"
112
+ label="Unhealthy"
113
+ tooltip="System becomes unhealthy after this many consecutive failures"
114
+ value={thresholds.unhealthy.minFailureCount}
115
+ suffix="consecutive ✗"
116
+ onChange={(v) =>
117
+ onChange({ ...thresholds, unhealthy: { minFailureCount: v } })
118
+ }
119
+ />
120
+ </div>
121
+ ) : (
122
+ <div className="space-y-3">
123
+ {/* Window Size */}
124
+ <div className="p-3 rounded-lg border bg-muted/30">
125
+ <div className="flex items-center justify-between">
126
+ <div className="flex items-center gap-2">
127
+ <span className="text-sm font-medium">Window Size</span>
128
+ <Tooltip content="How many recent runs to analyze when calculating status" />
129
+ </div>
130
+ <div className="flex items-center gap-2">
131
+ <Input
132
+ type="number"
133
+ min={3}
134
+ max={100}
135
+ value={thresholds.windowSize}
136
+ onChange={(e) =>
137
+ onChange({
138
+ ...thresholds,
139
+ windowSize: Number.parseInt(e.target.value) || 10,
140
+ })
141
+ }
142
+ className="h-8 w-16 text-center"
143
+ />
144
+ <span className="text-xs text-muted-foreground">runs</span>
145
+ </div>
146
+ </div>
147
+ </div>
148
+
149
+ <ThresholdCard
150
+ status="warning"
151
+ label="Degraded"
152
+ tooltip="System becomes degraded when failures in the window reach this count"
153
+ value={thresholds.degraded.minFailureCount}
154
+ prefix="≥"
155
+ suffix="failures"
156
+ onChange={(v) =>
157
+ onChange({ ...thresholds, degraded: { minFailureCount: v } })
158
+ }
159
+ />
160
+ <ThresholdCard
161
+ status="destructive"
162
+ label="Unhealthy"
163
+ tooltip="System becomes unhealthy when failures in the window reach this count"
164
+ value={thresholds.unhealthy.minFailureCount}
165
+ prefix="≥"
166
+ suffix="failures"
167
+ onChange={(v) =>
168
+ onChange({ ...thresholds, unhealthy: { minFailureCount: v } })
169
+ }
170
+ />
171
+ </div>
172
+ )}
173
+
174
+ {/* Save Button */}
175
+ <div className="flex justify-end pt-2 border-t">
176
+ <Button size="sm" onClick={onSave} disabled={saving}>
177
+ {saving ? "Saving..." : "Save Thresholds"}
178
+ </Button>
179
+ </div>
180
+ </div>
181
+ );
182
+ };
183
+
184
+ // =============================================================================
185
+ // THRESHOLD CARD — Shared sub-component
186
+ // =============================================================================
187
+
188
+ const STATUS_STYLES = {
189
+ healthy: {
190
+ dot: "bg-success",
191
+ text: "text-success",
192
+ border: "border-success/30",
193
+ bg: "bg-success/5",
194
+ },
195
+ warning: {
196
+ dot: "bg-warning",
197
+ text: "text-warning",
198
+ border: "border-warning/30",
199
+ bg: "bg-warning/5",
200
+ },
201
+ destructive: {
202
+ dot: "bg-destructive",
203
+ text: "text-destructive",
204
+ border: "border-destructive/30",
205
+ bg: "bg-destructive/5",
206
+ },
207
+ } as const;
208
+
209
+ function ThresholdCard({
210
+ status,
211
+ label,
212
+ tooltip,
213
+ value,
214
+ prefix,
215
+ suffix,
216
+ onChange,
217
+ }: {
218
+ status: keyof typeof STATUS_STYLES;
219
+ label: string;
220
+ tooltip: string;
221
+ value: number;
222
+ prefix?: string;
223
+ suffix: string;
224
+ onChange: (value: number) => void;
225
+ }) {
226
+ const styles = STATUS_STYLES[status];
227
+ return (
228
+ <div className={`p-3 rounded-lg border ${styles.border} ${styles.bg}`}>
229
+ <div className="flex items-center justify-between">
230
+ <div className="flex items-center gap-2">
231
+ <div className={`h-2 w-2 rounded-full ${styles.dot}`} />
232
+ <span className={`text-sm font-medium ${styles.text}`}>{label}</span>
233
+ <Tooltip content={tooltip} />
234
+ </div>
235
+ <div className="flex items-center gap-2">
236
+ {prefix && (
237
+ <span className="text-xs text-muted-foreground">{prefix}</span>
238
+ )}
239
+ <Input
240
+ type="number"
241
+ min={1}
242
+ value={value}
243
+ onChange={(e) => onChange(Number.parseInt(e.target.value) || 1)}
244
+ className="h-8 w-16 text-center"
245
+ />
246
+ <span className="text-xs text-muted-foreground w-20">{suffix}</span>
247
+ </div>
248
+ </div>
249
+ </div>
250
+ );
251
+ }
252
+
253
+ // Re-export DEFAULT_STATE_THRESHOLDS for convenience
254
+ export { DEFAULT_STATE_THRESHOLDS } from "@checkstack/healthcheck-common";
@@ -5,8 +5,11 @@ import type {
5
5
  } from "@checkstack/healthcheck-common";
6
6
  import { Plus, Settings, Shield, ChevronRight } from "lucide-react";
7
7
  import { isBuiltInCollector } from "../../hooks/useCollectors";
8
- import type { ValidationIssue } from "./IDEStatusBar";
9
- import { Badge } from "@checkstack/ui";
8
+ import {
9
+ IDETreeNode,
10
+ IDETreeSection,
11
+ type ValidationIssue,
12
+ } from "@checkstack/ui";
10
13
 
11
14
  // =============================================================================
12
15
  // TYPES
@@ -28,66 +31,6 @@ interface EditorTreeProps {
28
31
  strategyId: string;
29
32
  }
30
33
 
31
- // =============================================================================
32
- // VALIDATION INDICATOR
33
- // =============================================================================
34
-
35
- function ValidationDot({ nodeId, issues }: { nodeId: string; issues: ValidationIssue[] }) {
36
- const nodeIssues = issues.filter((i) => i.nodeId === nodeId);
37
- if (nodeIssues.length === 0) return;
38
-
39
- return (
40
- <span className="ml-auto flex h-2 w-2 rounded-full bg-destructive shrink-0" />
41
- );
42
- }
43
-
44
- // =============================================================================
45
- // TREE NODE
46
- // =============================================================================
47
-
48
- function TreeNode({
49
- nodeId,
50
- label,
51
- icon: Icon,
52
- selected,
53
- onClick,
54
- issues,
55
- indent = false,
56
- badge,
57
- }: {
58
- nodeId: string;
59
- label: string;
60
- icon: React.ElementType;
61
- selected: boolean;
62
- onClick: () => void;
63
- issues: ValidationIssue[];
64
- indent?: boolean;
65
- badge?: string;
66
- }) {
67
- return (
68
- <button
69
- type="button"
70
- onClick={onClick}
71
- className={`flex items-center gap-2 w-full px-3 py-2 text-sm text-left transition-colors ${
72
- indent ? "pl-7" : ""
73
- } ${
74
- selected
75
- ? "bg-primary/10 text-primary border-l-2 border-primary"
76
- : "hover:bg-muted/50 border-l-2 border-transparent"
77
- }`}
78
- >
79
- <Icon className="h-4 w-4 shrink-0 opacity-60" />
80
- <span className="truncate flex-1">{label}</span>
81
- {badge && (
82
- <Badge variant="secondary" className="text-[10px] shrink-0">
83
- {badge}
84
- </Badge>
85
- )}
86
- <ValidationDot nodeId={nodeId} issues={issues} />
87
- </button>
88
- );
89
- }
90
-
91
34
  // =============================================================================
92
35
  // EDITOR TREE
93
36
  // =============================================================================
@@ -111,7 +54,7 @@ export const EditorTree: React.FC<EditorTreeProps> = ({
111
54
  return (
112
55
  <div className="py-2">
113
56
  {/* General */}
114
- <TreeNode
57
+ <IDETreeNode
115
58
  nodeId="general"
116
59
  label="General"
117
60
  icon={Settings}
@@ -121,11 +64,7 @@ export const EditorTree: React.FC<EditorTreeProps> = ({
121
64
  />
122
65
 
123
66
  {/* Collectors Section Header */}
124
- <div className="px-3 pt-4 pb-1">
125
- <span className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
126
- Check Items
127
- </span>
128
- </div>
67
+ <IDETreeSection label="Check Items" />
129
68
 
130
69
  {/* Configured Collectors */}
131
70
  {collectors.map((entry) => {
@@ -135,7 +74,7 @@ export const EditorTree: React.FC<EditorTreeProps> = ({
135
74
  const builtIn = isBuiltInCollector(entry.collectorId, strategyId);
136
75
 
137
76
  return (
138
- <TreeNode
77
+ <IDETreeNode
139
78
  key={entry.id}
140
79
  nodeId={`collector:${entry.id}`}
141
80
  label={collector?.displayName ?? entry.collectorId}
@@ -166,13 +105,9 @@ export const EditorTree: React.FC<EditorTreeProps> = ({
166
105
  )}
167
106
 
168
107
  {/* Access Control */}
169
- <div className="px-3 pt-4 pb-1">
170
- <span className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
171
- Permissions
172
- </span>
173
- </div>
108
+ <IDETreeSection label="Permissions" />
174
109
 
175
- <TreeNode
110
+ <IDETreeNode
176
111
  nodeId="access"
177
112
  label="Access Control"
178
113
  icon={Shield}
@@ -1,58 +1,5 @@
1
- import React from "react";
2
- import type { TreeNodeId } from "./EditorTree";
3
- import { AlertCircle, CheckCircle2 } from "lucide-react";
4
-
5
- // =============================================================================
6
- // TYPES
7
- // =============================================================================
8
-
9
- export interface ValidationIssue {
10
- nodeId: TreeNodeId;
11
- message: string;
12
- }
13
-
14
- interface IDEStatusBarProps {
15
- issues: ValidationIssue[];
16
- onIssueClick: (nodeId: TreeNodeId) => void;
17
- }
18
-
19
- // =============================================================================
20
- // STATUS BAR
21
- // =============================================================================
22
-
23
- export const IDEStatusBar: React.FC<IDEStatusBarProps> = ({
24
- issues,
25
- onIssueClick,
26
- }) => {
27
- if (issues.length === 0) {
28
- return (
29
- <div className="flex items-center gap-2 px-4 py-2 mt-2 rounded-md border bg-card text-xs text-muted-foreground">
30
- <CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
31
- <span>No issues found</span>
32
- </div>
33
- );
34
- }
35
-
36
- return (
37
- <div className="flex items-center gap-3 px-4 py-2 mt-2 rounded-md border bg-card text-xs">
38
- <div className="flex items-center gap-1.5 text-destructive shrink-0">
39
- <AlertCircle className="h-3.5 w-3.5" />
40
- <span className="font-medium">
41
- {issues.length} {issues.length === 1 ? "issue" : "issues"}
42
- </span>
43
- </div>
44
- <div className="flex items-center gap-2 overflow-x-auto">
45
- {issues.map((issue, i) => (
46
- <button
47
- key={`${issue.nodeId}-${i}`}
48
- type="button"
49
- onClick={() => onIssueClick(issue.nodeId)}
50
- className="text-muted-foreground hover:text-foreground transition-colors whitespace-nowrap underline-offset-2 hover:underline"
51
- >
52
- {issue.message}
53
- </button>
54
- ))}
55
- </div>
56
- </div>
57
- );
58
- };
1
+ /**
2
+ * Re-export IDEStatusBar and ValidationIssue from the shared UI package.
3
+ * Kept for backward compatibility — existing imports from this path continue to work.
4
+ */
5
+ export { IDEStatusBar, type ValidationIssue } from "@checkstack/ui";
@@ -23,6 +23,8 @@ interface UseHealthCheckDataProps {
23
23
  startDate: Date;
24
24
  endDate: Date;
25
25
  };
26
+ /** Filter by source: "local" = core only, satellite UUID = specific satellite, undefined = all */
27
+ sourceFilter?: string;
26
28
  /** Whether the date range is a rolling preset (e.g., 'Last 7 days') that should auto-update */
27
29
  isRollingPreset?: boolean;
28
30
  /** Callback to update the date range (e.g., to refresh endDate to current time) */
@@ -60,6 +62,7 @@ export function useHealthCheckData({
60
62
  configurationId,
61
63
  strategyId,
62
64
  dateRange,
65
+ sourceFilter,
63
66
  isRollingPreset = false,
64
67
  onDateRangeRefresh,
65
68
  }: UseHealthCheckDataProps): UseHealthCheckDataResult {
@@ -82,6 +85,7 @@ export function useHealthCheckData({
82
85
  configurationId,
83
86
  startDate: dateRange.startDate,
84
87
  endDate: dateRange.endDate,
88
+ sourceFilter,
85
89
  targetPoints: 500,
86
90
  },
87
91
  {
package/src/index.tsx CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  import { HealthCheckConfigPage } from "./pages/HealthCheckConfigPage";
7
7
  import { StrategyPickerPage } from "./pages/StrategyPickerPage";
8
8
  import { HealthCheckIDEPage } from "./pages/HealthCheckIDEPage";
9
+ import { AssignmentIDEPage } from "./pages/AssignmentIDEPage";
9
10
  import { HealthCheckHistoryPage } from "./pages/HealthCheckHistoryPage";
10
11
  import { HealthCheckHistoryDetailPage } from "./pages/HealthCheckHistoryDetailPage";
11
12
  import { HealthCheckMenuItems } from "./components/HealthCheckMenuItems";
@@ -57,6 +58,12 @@ export default createFrontendPlugin({
57
58
  title: "Edit Health Check",
58
59
  accessRule: healthCheckAccess.configuration.manage,
59
60
  },
61
+ {
62
+ route: healthcheckRoutes.routes.assignments,
63
+ element: <AssignmentIDEPage />,
64
+ title: "Health Check Assignments",
65
+ accessRule: healthCheckAccess.configuration.manage,
66
+ },
60
67
  {
61
68
  route: healthcheckRoutes.routes.history,
62
69
  element: <HealthCheckHistoryPage />,