@checkstack/healthcheck-frontend 0.15.0 → 0.16.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # @checkstack/healthcheck-frontend
2
2
 
3
+ ## 0.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 80cbc51: Enforce GitOps provenance lock on backend API endpoints to prevent manual configuration drift for synchronized resources.
8
+
9
+ ### Patch Changes
10
+
11
+ - @checkstack/dashboard-frontend@0.4.1
12
+
3
13
  ## 0.15.0
4
14
 
5
15
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-frontend",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "checkstack": {
@@ -14,6 +14,7 @@ import {
14
14
  Badge,
15
15
  } from "@checkstack/ui";
16
16
  import { Trash2, Edit, Pause, Play } from "lucide-react";
17
+ import { useProvenanceLock } from "@checkstack/gitops-frontend";
17
18
 
18
19
  interface HealthCheckListProps {
19
20
  configurations: HealthCheckConfiguration[];
@@ -59,64 +60,16 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
59
60
  </TableRow>
60
61
  ) : (
61
62
  configurations.map((config) => (
62
- <TableRow
63
+ <HealthCheckRow
63
64
  key={config.id}
64
- className={config.paused ? "opacity-60" : ""}
65
- >
66
- <TableCell className="font-medium">{config.name}</TableCell>
67
- <TableCell>{getStrategyName(config.strategyId)}</TableCell>
68
- <TableCell>{config.intervalSeconds}</TableCell>
69
- <TableCell>
70
- {config.paused ? (
71
- <Badge variant="secondary">Paused</Badge>
72
- ) : (
73
- <Badge variant="default">Active</Badge>
74
- )}
75
- </TableCell>
76
- <TableCell className="text-right">
77
- <div className="flex justify-end gap-2">
78
- {canManage &&
79
- onPause &&
80
- onResume &&
81
- (config.paused ? (
82
- <Button
83
- variant="ghost"
84
- size="icon"
85
- onClick={() => onResume(config.id)}
86
- title="Resume health check"
87
- >
88
- <Play className="h-4 w-4" />
89
- </Button>
90
- ) : (
91
- <Button
92
- variant="ghost"
93
- size="icon"
94
- onClick={() => onPause(config.id)}
95
- title="Pause health check"
96
- >
97
- <Pause className="h-4 w-4" />
98
- </Button>
99
- ))}
100
- <Button
101
- variant="ghost"
102
- size="icon"
103
- onClick={() => onEdit(config)}
104
- >
105
- <Edit className="h-4 w-4" />
106
- </Button>
107
- {canManage && (
108
- <Button
109
- variant="ghost"
110
- size="icon"
111
- className="text-destructive hover:text-destructive"
112
- onClick={() => onDelete(config.id)}
113
- >
114
- <Trash2 className="h-4 w-4" />
115
- </Button>
116
- )}
117
- </div>
118
- </TableCell>
119
- </TableRow>
65
+ config={config}
66
+ strategyName={getStrategyName(config.strategyId)}
67
+ onEdit={onEdit}
68
+ onDelete={onDelete}
69
+ onPause={onPause}
70
+ onResume={onResume}
71
+ canManage={canManage}
72
+ />
120
73
  ))
121
74
  )}
122
75
  </TableBody>
@@ -124,3 +77,95 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
124
77
  </div>
125
78
  );
126
79
  };
80
+
81
+ interface HealthCheckRowProps {
82
+ config: HealthCheckConfiguration;
83
+ strategyName: string;
84
+ onEdit: (config: HealthCheckConfiguration) => void;
85
+ onDelete: (id: string) => void;
86
+ onPause?: (id: string) => void;
87
+ onResume?: (id: string) => void;
88
+ canManage: boolean;
89
+ }
90
+
91
+ const HealthCheckRow: React.FC<HealthCheckRowProps> = ({
92
+ config,
93
+ strategyName,
94
+ onEdit,
95
+ onDelete,
96
+ onPause,
97
+ onResume,
98
+ canManage,
99
+ }) => {
100
+ const { isLocked } = useProvenanceLock({
101
+ kind: "Healthcheck",
102
+ entityId: config.id,
103
+ });
104
+
105
+ return (
106
+ <TableRow className={config.paused ? "opacity-60" : ""}>
107
+ <TableCell className="font-medium">{config.name}</TableCell>
108
+ <TableCell>{strategyName}</TableCell>
109
+ <TableCell>{config.intervalSeconds}</TableCell>
110
+ <TableCell>
111
+ {config.paused ? (
112
+ <Badge variant="secondary">Paused</Badge>
113
+ ) : (
114
+ <Badge variant="default">Active</Badge>
115
+ )}
116
+ </TableCell>
117
+ <TableCell className="text-right">
118
+ <div className="flex justify-end gap-2">
119
+ {canManage &&
120
+ onPause &&
121
+ onResume &&
122
+ (config.paused ? (
123
+ <Button
124
+ variant="ghost"
125
+ size="icon"
126
+ onClick={() => onResume(config.id)}
127
+ title={isLocked ? "Managed by GitOps" : "Resume health check"}
128
+ disabled={isLocked}
129
+ >
130
+ <Play className="h-4 w-4" />
131
+ </Button>
132
+ ) : (
133
+ <Button
134
+ variant="ghost"
135
+ size="icon"
136
+ onClick={() => onPause(config.id)}
137
+ title={isLocked ? "Managed by GitOps" : "Pause health check"}
138
+ disabled={isLocked}
139
+ >
140
+ <Pause className="h-4 w-4" />
141
+ </Button>
142
+ ))}
143
+ <Button
144
+ variant="ghost"
145
+ size="icon"
146
+ onClick={() => onEdit(config)}
147
+ title={
148
+ isLocked
149
+ ? "View configuration (Managed by GitOps)"
150
+ : "Edit configuration"
151
+ }
152
+ >
153
+ <Edit className="h-4 w-4" />
154
+ </Button>
155
+ {canManage && (
156
+ <Button
157
+ variant="ghost"
158
+ size="icon"
159
+ className="text-destructive hover:text-destructive"
160
+ onClick={() => onDelete(config.id)}
161
+ disabled={isLocked}
162
+ title={isLocked ? "Managed by GitOps" : "Delete configuration"}
163
+ >
164
+ <Trash2 className="h-4 w-4" />
165
+ </Button>
166
+ )}
167
+ </div>
168
+ </TableCell>
169
+ </TableRow>
170
+ );
171
+ };
@@ -25,6 +25,7 @@ interface AssignmentTreeProps {
25
25
  selectedNode: AssignmentNodeId | undefined;
26
26
  onSelectNode: (nodeId: AssignmentNodeId) => void;
27
27
  onToggleAssignment: (configId: string, assigned: boolean) => void;
28
+ isLocked?: boolean;
28
29
  }
29
30
 
30
31
  // =============================================================================
@@ -37,6 +38,7 @@ export const AssignmentTree: React.FC<AssignmentTreeProps> = ({
37
38
  selectedNode,
38
39
  onSelectNode,
39
40
  onToggleAssignment,
41
+ isLocked,
40
42
  }) => {
41
43
  return (
42
44
  <div className="py-2">
@@ -99,7 +101,7 @@ export const AssignmentTree: React.FC<AssignmentTreeProps> = ({
99
101
  ))}
100
102
 
101
103
  {/* Available (unassigned) health checks */}
102
- {available.length > 0 && (
104
+ {!isLocked && available.length > 0 && (
103
105
  <>
104
106
  <IDETreeSection label="Available" />
105
107
  {available.map((config) => (
@@ -16,6 +16,7 @@ interface ExecutionPanelProps {
16
16
  onToggleLocal: () => void;
17
17
  onToggleSatellite: (satelliteId: string) => void;
18
18
  saving: boolean;
19
+ isLocked?: boolean;
19
20
  }
20
21
 
21
22
  /**
@@ -29,6 +30,7 @@ export const ExecutionPanel: React.FC<ExecutionPanelProps> = ({
29
30
  onToggleLocal,
30
31
  onToggleSatellite,
31
32
  saving,
33
+ isLocked,
32
34
  }) => {
33
35
  const hasSatellites = satelliteIds.length > 0;
34
36
  const willRunAnywhere = includeLocal || hasSatellites;
@@ -49,7 +51,7 @@ export const ExecutionPanel: React.FC<ExecutionPanelProps> = ({
49
51
  <Checkbox
50
52
  checked={includeLocal}
51
53
  onCheckedChange={onToggleLocal}
52
- disabled={saving || (!hasSatellites && includeLocal)}
54
+ disabled={saving || isLocked || (!hasSatellites && includeLocal)}
53
55
  />
54
56
  <div>
55
57
  <Label className="text-sm font-medium">Run Locally</Label>
@@ -87,7 +89,7 @@ export const ExecutionPanel: React.FC<ExecutionPanelProps> = ({
87
89
  <Checkbox
88
90
  checked={isChecked}
89
91
  onCheckedChange={() => onToggleSatellite(sat.id)}
90
- disabled={saving}
92
+ disabled={saving || isLocked}
91
93
  />
92
94
  <div className="flex-1 min-w-0">
93
95
  <div className="flex items-center gap-2">
@@ -13,6 +13,7 @@ interface GeneralPanelProps {
13
13
  onToggleEnabled: () => void;
14
14
  onUnassign: () => void;
15
15
  saving: boolean;
16
+ isLocked?: boolean;
16
17
  }
17
18
 
18
19
  /**
@@ -26,6 +27,7 @@ export const GeneralPanel: React.FC<GeneralPanelProps> = ({
26
27
  onToggleEnabled,
27
28
  onUnassign,
28
29
  saving,
30
+ isLocked,
29
31
  }) => {
30
32
  const editUrl = resolveRoute(healthcheckRoutes.routes.edit, {
31
33
  configId: configurationId,
@@ -46,7 +48,7 @@ export const GeneralPanel: React.FC<GeneralPanelProps> = ({
46
48
  <Checkbox
47
49
  checked={enabled}
48
50
  onCheckedChange={onToggleEnabled}
49
- disabled={saving}
51
+ disabled={saving || isLocked}
50
52
  />
51
53
  <div>
52
54
  <Label className="text-sm font-medium">Enabled</Label>
@@ -85,7 +87,8 @@ export const GeneralPanel: React.FC<GeneralPanelProps> = ({
85
87
  variant="ghost"
86
88
  size="sm"
87
89
  onClick={onUnassign}
88
- disabled={saving}
90
+ disabled={saving || isLocked}
91
+ title={isLocked ? "Managed by GitOps" : undefined}
89
92
  className="text-destructive hover:text-destructive hover:bg-destructive/10"
90
93
  >
91
94
  <Trash2 className="h-3.5 w-3.5 mr-1.5" />
@@ -14,6 +14,7 @@ interface RetentionPanelProps {
14
14
  onSave: () => void;
15
15
  onReset: () => void;
16
16
  saving: boolean;
17
+ isLocked?: boolean;
17
18
  }
18
19
 
19
20
  /**
@@ -25,6 +26,7 @@ export const RetentionPanel: React.FC<RetentionPanelProps> = ({
25
26
  onSave,
26
27
  onReset,
27
28
  saving,
29
+ isLocked,
28
30
  }) => {
29
31
  if (!data) {
30
32
  return (
@@ -68,6 +70,7 @@ export const RetentionPanel: React.FC<RetentionPanelProps> = ({
68
70
  min={1}
69
71
  max={30}
70
72
  onChange={(v) => onFieldChange("rawRetentionDays", v)}
73
+ disabled={saving || isLocked}
71
74
  />
72
75
 
73
76
  {/* Hourly Aggregates */}
@@ -78,6 +81,7 @@ export const RetentionPanel: React.FC<RetentionPanelProps> = ({
78
81
  min={7}
79
82
  max={365}
80
83
  onChange={(v) => onFieldChange("hourlyRetentionDays", v)}
84
+ disabled={saving || isLocked}
81
85
  />
82
86
 
83
87
  {/* Daily Aggregates */}
@@ -88,6 +92,7 @@ export const RetentionPanel: React.FC<RetentionPanelProps> = ({
88
92
  min={30}
89
93
  max={1095}
90
94
  onChange={(v) => onFieldChange("dailyRetentionDays", v)}
95
+ disabled={saving || isLocked}
91
96
  />
92
97
 
93
98
  {/* Actions */}
@@ -96,14 +101,14 @@ export const RetentionPanel: React.FC<RetentionPanelProps> = ({
96
101
  variant="ghost"
97
102
  size="sm"
98
103
  onClick={onReset}
99
- disabled={saving || !data.isCustom}
104
+ disabled={saving || isLocked || !data.isCustom}
100
105
  >
101
106
  Reset to Defaults
102
107
  </Button>
103
108
  <Button
104
109
  size="sm"
105
110
  onClick={onSave}
106
- disabled={saving || !isValidHierarchy}
111
+ disabled={saving || isLocked || !isValidHierarchy}
107
112
  >
108
113
  {saving ? "Saving..." : "Save Retention"}
109
114
  </Button>
@@ -123,6 +128,7 @@ function RetentionTier({
123
128
  min,
124
129
  max,
125
130
  onChange,
131
+ disabled,
126
132
  }: {
127
133
  label: string;
128
134
  description: string;
@@ -130,6 +136,7 @@ function RetentionTier({
130
136
  min: number;
131
137
  max: number;
132
138
  onChange: (value: number) => void;
139
+ disabled?: boolean;
133
140
  }) {
134
141
  return (
135
142
  <div className="p-3 rounded-lg border bg-muted/30">
@@ -145,6 +152,7 @@ function RetentionTier({
145
152
  max={max}
146
153
  value={value}
147
154
  onChange={(e) => onChange(Number(e.target.value))}
155
+ disabled={disabled}
148
156
  className="h-8 w-20 text-center"
149
157
  />
150
158
  <span className="text-sm text-muted-foreground w-10">days</span>
@@ -18,6 +18,7 @@ interface ThresholdsPanelProps {
18
18
  onChange: (thresholds: StateThresholds) => void;
19
19
  onSave: () => void;
20
20
  saving: boolean;
21
+ isLocked?: boolean;
21
22
  }
22
23
 
23
24
  /**
@@ -29,6 +30,7 @@ export const ThresholdsPanel: React.FC<ThresholdsPanelProps> = ({
29
30
  onChange,
30
31
  onSave,
31
32
  saving,
33
+ isLocked,
32
34
  }) => {
33
35
  return (
34
36
  <div className="p-6 space-y-4">
@@ -64,6 +66,7 @@ export const ThresholdsPanel: React.FC<ThresholdsPanelProps> = ({
64
66
  });
65
67
  }
66
68
  }}
69
+ disabled={saving || isLocked}
67
70
  >
68
71
  <SelectTrigger className="w-full">
69
72
  <SelectValue />
@@ -96,6 +99,7 @@ export const ThresholdsPanel: React.FC<ThresholdsPanelProps> = ({
96
99
  onChange={(v) =>
97
100
  onChange({ ...thresholds, healthy: { minSuccessCount: v } })
98
101
  }
102
+ disabled={saving || isLocked}
99
103
  />
100
104
  <ThresholdCard
101
105
  status="warning"
@@ -106,6 +110,7 @@ export const ThresholdsPanel: React.FC<ThresholdsPanelProps> = ({
106
110
  onChange={(v) =>
107
111
  onChange({ ...thresholds, degraded: { minFailureCount: v } })
108
112
  }
113
+ disabled={saving || isLocked}
109
114
  />
110
115
  <ThresholdCard
111
116
  status="destructive"
@@ -116,6 +121,7 @@ export const ThresholdsPanel: React.FC<ThresholdsPanelProps> = ({
116
121
  onChange={(v) =>
117
122
  onChange({ ...thresholds, unhealthy: { minFailureCount: v } })
118
123
  }
124
+ disabled={saving || isLocked}
119
125
  />
120
126
  </div>
121
127
  ) : (
@@ -139,6 +145,7 @@ export const ThresholdsPanel: React.FC<ThresholdsPanelProps> = ({
139
145
  windowSize: Number.parseInt(e.target.value) || 10,
140
146
  })
141
147
  }
148
+ disabled={saving || isLocked}
142
149
  className="h-8 w-16 text-center"
143
150
  />
144
151
  <span className="text-xs text-muted-foreground">runs</span>
@@ -156,6 +163,7 @@ export const ThresholdsPanel: React.FC<ThresholdsPanelProps> = ({
156
163
  onChange={(v) =>
157
164
  onChange({ ...thresholds, degraded: { minFailureCount: v } })
158
165
  }
166
+ disabled={saving || isLocked}
159
167
  />
160
168
  <ThresholdCard
161
169
  status="destructive"
@@ -167,13 +175,14 @@ export const ThresholdsPanel: React.FC<ThresholdsPanelProps> = ({
167
175
  onChange={(v) =>
168
176
  onChange({ ...thresholds, unhealthy: { minFailureCount: v } })
169
177
  }
178
+ disabled={saving || isLocked}
170
179
  />
171
180
  </div>
172
181
  )}
173
182
 
174
183
  {/* Save Button */}
175
184
  <div className="flex justify-end pt-2 border-t">
176
- <Button size="sm" onClick={onSave} disabled={saving}>
185
+ <Button size="sm" onClick={onSave} disabled={saving || isLocked}>
177
186
  {saving ? "Saving..." : "Save Thresholds"}
178
187
  </Button>
179
188
  </div>
@@ -214,6 +223,7 @@ function ThresholdCard({
214
223
  prefix,
215
224
  suffix,
216
225
  onChange,
226
+ disabled,
217
227
  }: {
218
228
  status: keyof typeof STATUS_STYLES;
219
229
  label: string;
@@ -222,6 +232,7 @@ function ThresholdCard({
222
232
  prefix?: string;
223
233
  suffix: string;
224
234
  onChange: (value: number) => void;
235
+ disabled?: boolean;
225
236
  }) {
226
237
  const styles = STATUS_STYLES[status];
227
238
  return (
@@ -241,6 +252,7 @@ function ThresholdCard({
241
252
  min={1}
242
253
  value={value}
243
254
  onChange={(e) => onChange(Number.parseInt(e.target.value) || 1)}
255
+ disabled={disabled}
244
256
  className="h-8 w-16 text-center"
245
257
  />
246
258
  <span className="text-xs text-muted-foreground w-20">{suffix}</span>
@@ -18,6 +18,7 @@ import {
18
18
  } from "../components/assignments/AssignmentTree";
19
19
  import { GeneralPanel } from "../components/assignments/GeneralPanel";
20
20
  import { ThresholdsPanel } from "../components/assignments/ThresholdsPanel";
21
+ import { useProvenanceLock, GitOpsLockBanner } from "@checkstack/gitops-frontend";
21
22
  import {
22
23
  RetentionPanel,
23
24
  type RetentionData,
@@ -61,6 +62,11 @@ const AssignmentIDEPageContent = () => {
61
62
  { enabled: !!systemId },
62
63
  );
63
64
 
65
+ const { isLocked, provenance } = useProvenanceLock({
66
+ kind: "System",
67
+ entityId: systemId,
68
+ });
69
+
64
70
  const { data: satellitesData } = satelliteClient.listSatellites.useQuery({});
65
71
 
66
72
  // --- UI State ---
@@ -390,6 +396,7 @@ const AssignmentIDEPageContent = () => {
390
396
  onToggleEnabled={() => handleToggleEnabled(configId, assoc.enabled)}
391
397
  onUnassign={() => handleToggleAssignment(configId, true)}
392
398
  saving={saving}
399
+ isLocked={isLocked}
393
400
  />
394
401
  );
395
402
  }
@@ -404,6 +411,7 @@ const AssignmentIDEPageContent = () => {
404
411
  onChange={(t) => handleThresholdChange(configId, t)}
405
412
  onSave={() => handleSaveThresholds(configId)}
406
413
  saving={saving}
414
+ isLocked={isLocked}
407
415
  />
408
416
  );
409
417
  }
@@ -417,6 +425,7 @@ const AssignmentIDEPageContent = () => {
417
425
  onSave={() => handleSaveRetention(configId)}
418
426
  onReset={() => handleResetRetention(configId)}
419
427
  saving={saving}
428
+ isLocked={isLocked}
420
429
  />
421
430
  );
422
431
  }
@@ -431,6 +440,7 @@ const AssignmentIDEPageContent = () => {
431
440
  handleToggleSatellite(configId, satId)
432
441
  }
433
442
  saving={saving}
443
+ isLocked={isLocked}
434
444
  />
435
445
  );
436
446
  }
@@ -458,6 +468,11 @@ const AssignmentIDEPageContent = () => {
458
468
  </BackLink>
459
469
  }
460
470
  >
471
+ {isLocked && provenance && (
472
+ <div className="mb-4">
473
+ <GitOpsLockBanner provenance={provenance} />
474
+ </div>
475
+ )}
461
476
  <IDELayout
462
477
  tree={
463
478
  <AssignmentTree
@@ -466,6 +481,7 @@ const AssignmentIDEPageContent = () => {
466
481
  selectedNode={selectedNode}
467
482
  onSelectNode={setSelectedNode}
468
483
  onToggleAssignment={handleToggleAssignment}
484
+ isLocked={isLocked}
469
485
  />
470
486
  }
471
487
  panel={renderPanel()}