@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.
- package/CHANGELOG.md +72 -0
- package/package.json +11 -10
- package/src/components/HealthCheckRunsTable.tsx +19 -1
- package/src/components/HealthCheckSystemOverview.tsx +79 -6
- package/src/components/SystemHealthCheckAssignment.tsx +34 -825
- package/src/components/assignments/AssignmentTree.tsx +123 -0
- package/src/components/assignments/ExecutionPanel.tsx +135 -0
- package/src/components/assignments/GeneralPanel.tsx +100 -0
- package/src/components/assignments/RetentionPanel.tsx +157 -0
- package/src/components/assignments/ThresholdsPanel.tsx +254 -0
- package/src/components/editor/EditorTree.tsx +10 -75
- package/src/components/editor/IDEStatusBar.tsx +5 -58
- package/src/hooks/useHealthCheckData.ts +4 -0
- package/src/index.tsx +7 -0
- package/src/pages/AssignmentIDEPage.tsx +477 -0
- package/src/pages/HealthCheckHistoryDetailPage.tsx +53 -6
- package/src/pages/HealthCheckIDEPage.tsx +7 -15
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Settings, Gauge, Database, Radio, Plus, Check } from "lucide-react";
|
|
3
|
+
import { IDETreeNode, IDETreeSection } from "@checkstack/ui";
|
|
4
|
+
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// TYPES
|
|
7
|
+
// =============================================================================
|
|
8
|
+
|
|
9
|
+
export type AssignmentNodeId =
|
|
10
|
+
| `general:${string}`
|
|
11
|
+
| `thresholds:${string}`
|
|
12
|
+
| `retention:${string}`
|
|
13
|
+
| `execution:${string}`;
|
|
14
|
+
|
|
15
|
+
interface AssignmentConfig {
|
|
16
|
+
configurationId: string;
|
|
17
|
+
configurationName: string;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
satelliteCount: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AssignmentTreeProps {
|
|
23
|
+
assigned: AssignmentConfig[];
|
|
24
|
+
available: Array<{ id: string; name: string; strategyId: string }>;
|
|
25
|
+
selectedNode: AssignmentNodeId | undefined;
|
|
26
|
+
onSelectNode: (nodeId: AssignmentNodeId) => void;
|
|
27
|
+
onToggleAssignment: (configId: string, assigned: boolean) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// ASSIGNMENT TREE
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
export const AssignmentTree: React.FC<AssignmentTreeProps> = ({
|
|
35
|
+
assigned,
|
|
36
|
+
available,
|
|
37
|
+
selectedNode,
|
|
38
|
+
onSelectNode,
|
|
39
|
+
onToggleAssignment,
|
|
40
|
+
}) => {
|
|
41
|
+
return (
|
|
42
|
+
<div className="py-2">
|
|
43
|
+
<IDETreeSection label="Assigned Health Checks" />
|
|
44
|
+
|
|
45
|
+
{assigned.length === 0 && (
|
|
46
|
+
<p className="px-3 py-2 text-xs text-muted-foreground italic">
|
|
47
|
+
No health checks assigned
|
|
48
|
+
</p>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
{assigned.map((assoc) => (
|
|
52
|
+
<div key={assoc.configurationId}>
|
|
53
|
+
{/* Config header — not clickable as a node, just a label */}
|
|
54
|
+
<div className="px-3 py-1.5 text-xs font-medium text-foreground flex items-center gap-2 mt-1">
|
|
55
|
+
<Check className="h-3 w-3 text-primary shrink-0" />
|
|
56
|
+
<span className="truncate">{assoc.configurationName}</span>
|
|
57
|
+
{!assoc.enabled && (
|
|
58
|
+
<span className="text-[9px] text-muted-foreground bg-muted px-1 rounded">
|
|
59
|
+
off
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{/* Sub-nodes */}
|
|
65
|
+
<IDETreeNode
|
|
66
|
+
nodeId={`general:${assoc.configurationId}`}
|
|
67
|
+
label="General"
|
|
68
|
+
icon={Settings}
|
|
69
|
+
selected={selectedNode === `general:${assoc.configurationId}`}
|
|
70
|
+
onClick={() => onSelectNode(`general:${assoc.configurationId}`)}
|
|
71
|
+
indent
|
|
72
|
+
/>
|
|
73
|
+
<IDETreeNode
|
|
74
|
+
nodeId={`thresholds:${assoc.configurationId}`}
|
|
75
|
+
label="Thresholds"
|
|
76
|
+
icon={Gauge}
|
|
77
|
+
selected={selectedNode === `thresholds:${assoc.configurationId}`}
|
|
78
|
+
onClick={() => onSelectNode(`thresholds:${assoc.configurationId}`)}
|
|
79
|
+
indent
|
|
80
|
+
/>
|
|
81
|
+
<IDETreeNode
|
|
82
|
+
nodeId={`retention:${assoc.configurationId}`}
|
|
83
|
+
label="Retention"
|
|
84
|
+
icon={Database}
|
|
85
|
+
selected={selectedNode === `retention:${assoc.configurationId}`}
|
|
86
|
+
onClick={() => onSelectNode(`retention:${assoc.configurationId}`)}
|
|
87
|
+
indent
|
|
88
|
+
/>
|
|
89
|
+
<IDETreeNode
|
|
90
|
+
nodeId={`execution:${assoc.configurationId}`}
|
|
91
|
+
label="Execution"
|
|
92
|
+
icon={Radio}
|
|
93
|
+
selected={selectedNode === `execution:${assoc.configurationId}`}
|
|
94
|
+
onClick={() => onSelectNode(`execution:${assoc.configurationId}`)}
|
|
95
|
+
indent
|
|
96
|
+
badge={assoc.satelliteCount > 0 ? `${assoc.satelliteCount}` : undefined}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
))}
|
|
100
|
+
|
|
101
|
+
{/* Available (unassigned) health checks */}
|
|
102
|
+
{available.length > 0 && (
|
|
103
|
+
<>
|
|
104
|
+
<IDETreeSection label="Available" />
|
|
105
|
+
{available.map((config) => (
|
|
106
|
+
<button
|
|
107
|
+
key={config.id}
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => onToggleAssignment(config.id, false)}
|
|
110
|
+
className="flex items-center gap-2 w-full px-3 py-2 pl-7 text-sm text-left transition-colors text-muted-foreground hover:text-foreground hover:bg-muted/50 border-l-2 border-transparent"
|
|
111
|
+
>
|
|
112
|
+
<Plus className="h-4 w-4 shrink-0" />
|
|
113
|
+
<span className="truncate flex-1">{config.name}</span>
|
|
114
|
+
<span className="text-[10px] text-muted-foreground shrink-0">
|
|
115
|
+
{config.strategyId.split(".").pop()}
|
|
116
|
+
</span>
|
|
117
|
+
</button>
|
|
118
|
+
))}
|
|
119
|
+
</>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Checkbox, Label, Tooltip } from "@checkstack/ui";
|
|
3
|
+
import { Satellite } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
interface SatelliteDto {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
region: string;
|
|
9
|
+
status: "online" | "offline";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ExecutionPanelProps {
|
|
13
|
+
includeLocal: boolean;
|
|
14
|
+
satelliteIds: string[];
|
|
15
|
+
satellites: SatelliteDto[];
|
|
16
|
+
onToggleLocal: () => void;
|
|
17
|
+
onToggleSatellite: (satelliteId: string) => void;
|
|
18
|
+
saving: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Panel for configuring where a health check executes:
|
|
23
|
+
* core server (local) and/or remote satellites.
|
|
24
|
+
*/
|
|
25
|
+
export const ExecutionPanel: React.FC<ExecutionPanelProps> = ({
|
|
26
|
+
includeLocal,
|
|
27
|
+
satelliteIds,
|
|
28
|
+
satellites,
|
|
29
|
+
onToggleLocal,
|
|
30
|
+
onToggleSatellite,
|
|
31
|
+
saving,
|
|
32
|
+
}) => {
|
|
33
|
+
const hasSatellites = satelliteIds.length > 0;
|
|
34
|
+
const willRunAnywhere = includeLocal || hasSatellites;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="p-6 space-y-4">
|
|
38
|
+
<div>
|
|
39
|
+
<h3 className="text-sm font-semibold">Execution Sources</h3>
|
|
40
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
41
|
+
Choose where this health check runs. You can combine local
|
|
42
|
+
execution with remote satellites.
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Include Local Toggle */}
|
|
47
|
+
<div className="p-4 bg-muted/50 rounded-lg border">
|
|
48
|
+
<div className="flex items-center gap-3">
|
|
49
|
+
<Checkbox
|
|
50
|
+
checked={includeLocal}
|
|
51
|
+
onCheckedChange={onToggleLocal}
|
|
52
|
+
disabled={saving || (!hasSatellites && includeLocal)}
|
|
53
|
+
/>
|
|
54
|
+
<div>
|
|
55
|
+
<Label className="text-sm font-medium">Run Locally</Label>
|
|
56
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
57
|
+
Execute this health check on the core server
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
{!includeLocal && !hasSatellites && (
|
|
62
|
+
<p className="text-xs text-warning mt-2">
|
|
63
|
+
⚠ Enable at least one execution source (local or satellite)
|
|
64
|
+
</p>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Satellite Picker */}
|
|
69
|
+
<div className="space-y-2">
|
|
70
|
+
<div className="flex items-center gap-2">
|
|
71
|
+
<Label className="text-sm font-medium">Assigned Satellites</Label>
|
|
72
|
+
<Tooltip content="Select which satellites should execute this health check remotely" />
|
|
73
|
+
</div>
|
|
74
|
+
{satellites.length === 0 ? (
|
|
75
|
+
<p className="text-sm text-muted-foreground italic py-2">
|
|
76
|
+
No satellites registered. Create one in the Satellites settings.
|
|
77
|
+
</p>
|
|
78
|
+
) : (
|
|
79
|
+
<div className="space-y-1.5">
|
|
80
|
+
{satellites.map((sat) => {
|
|
81
|
+
const isChecked = satelliteIds.includes(sat.id);
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
key={sat.id}
|
|
85
|
+
className="flex items-center gap-3 p-2.5 rounded-md border hover:bg-muted/30 transition-colors"
|
|
86
|
+
>
|
|
87
|
+
<Checkbox
|
|
88
|
+
checked={isChecked}
|
|
89
|
+
onCheckedChange={() => onToggleSatellite(sat.id)}
|
|
90
|
+
disabled={saving}
|
|
91
|
+
/>
|
|
92
|
+
<div className="flex-1 min-w-0">
|
|
93
|
+
<div className="flex items-center gap-2">
|
|
94
|
+
<Satellite className="h-3.5 w-3.5 text-muted-foreground" />
|
|
95
|
+
<span className="text-sm font-medium truncate">
|
|
96
|
+
{sat.name}
|
|
97
|
+
</span>
|
|
98
|
+
</div>
|
|
99
|
+
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
|
100
|
+
{sat.region}
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
<span
|
|
104
|
+
className={`text-xs px-1.5 py-0.5 rounded-full ${
|
|
105
|
+
sat.status === "online"
|
|
106
|
+
? "bg-success/10 text-success"
|
|
107
|
+
: "bg-muted text-muted-foreground"
|
|
108
|
+
}`}
|
|
109
|
+
>
|
|
110
|
+
{sat.status === "online" ? "Online" : "Offline"}
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
})}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Execution Summary */}
|
|
120
|
+
<div className="p-3 bg-muted/30 rounded-lg border text-xs text-muted-foreground">
|
|
121
|
+
<span className="font-medium">Execution: </span>
|
|
122
|
+
{willRunAnywhere ? (
|
|
123
|
+
<>
|
|
124
|
+
{includeLocal && "Core server"}
|
|
125
|
+
{includeLocal && hasSatellites && " + "}
|
|
126
|
+
{hasSatellites &&
|
|
127
|
+
`${satelliteIds.length} satellite${satelliteIds.length > 1 ? "s" : ""}`}
|
|
128
|
+
</>
|
|
129
|
+
) : (
|
|
130
|
+
<span className="text-warning">No execution sources configured</span>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button, Checkbox, Label } from "@checkstack/ui";
|
|
3
|
+
import { ExternalLink, Trash2 } from "lucide-react";
|
|
4
|
+
import { Link } from "react-router-dom";
|
|
5
|
+
import { resolveRoute } from "@checkstack/common";
|
|
6
|
+
import { healthcheckRoutes } from "@checkstack/healthcheck-common";
|
|
7
|
+
|
|
8
|
+
interface GeneralPanelProps {
|
|
9
|
+
configurationName: string;
|
|
10
|
+
strategyId: string;
|
|
11
|
+
configurationId: string;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
onToggleEnabled: () => void;
|
|
14
|
+
onUnassign: () => void;
|
|
15
|
+
saving: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Panel showing general assignment info: toggle enabled + link to config editor.
|
|
20
|
+
*/
|
|
21
|
+
export const GeneralPanel: React.FC<GeneralPanelProps> = ({
|
|
22
|
+
configurationName,
|
|
23
|
+
strategyId,
|
|
24
|
+
configurationId,
|
|
25
|
+
enabled,
|
|
26
|
+
onToggleEnabled,
|
|
27
|
+
onUnassign,
|
|
28
|
+
saving,
|
|
29
|
+
}) => {
|
|
30
|
+
const editUrl = resolveRoute(healthcheckRoutes.routes.edit, {
|
|
31
|
+
configId: configurationId,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="p-6 space-y-4">
|
|
36
|
+
<div>
|
|
37
|
+
<h3 className="text-sm font-semibold">General</h3>
|
|
38
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
39
|
+
Basic assignment settings for this health check on this system.
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Enabled Toggle */}
|
|
44
|
+
<div className="p-4 bg-muted/50 rounded-lg border">
|
|
45
|
+
<div className="flex items-center gap-3">
|
|
46
|
+
<Checkbox
|
|
47
|
+
checked={enabled}
|
|
48
|
+
onCheckedChange={onToggleEnabled}
|
|
49
|
+
disabled={saving}
|
|
50
|
+
/>
|
|
51
|
+
<div>
|
|
52
|
+
<Label className="text-sm font-medium">Enabled</Label>
|
|
53
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
54
|
+
When disabled, this health check will not run for this system
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{/* Config Info */}
|
|
61
|
+
<div className="p-4 bg-muted/50 rounded-lg border space-y-2">
|
|
62
|
+
<div className="flex items-center justify-between">
|
|
63
|
+
<div>
|
|
64
|
+
<Label className="text-sm font-medium">Configuration</Label>
|
|
65
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
66
|
+
{configurationName}
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
<Link
|
|
70
|
+
to={editUrl}
|
|
71
|
+
className="text-xs text-primary hover:underline flex items-center gap-1"
|
|
72
|
+
>
|
|
73
|
+
Edit configuration
|
|
74
|
+
<ExternalLink className="h-3 w-3" />
|
|
75
|
+
</Link>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="text-xs text-muted-foreground">
|
|
78
|
+
Strategy: <code className="bg-muted px-1 py-0.5 rounded text-foreground">{strategyId}</code>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Unassign */}
|
|
83
|
+
<div className="pt-2 border-t">
|
|
84
|
+
<Button
|
|
85
|
+
variant="ghost"
|
|
86
|
+
size="sm"
|
|
87
|
+
onClick={onUnassign}
|
|
88
|
+
disabled={saving}
|
|
89
|
+
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
|
90
|
+
>
|
|
91
|
+
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
|
|
92
|
+
Remove Assignment
|
|
93
|
+
</Button>
|
|
94
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
95
|
+
This will unassign the health check from this system entirely.
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button, Input, LoadingSpinner } from "@checkstack/ui";
|
|
3
|
+
|
|
4
|
+
export interface RetentionData {
|
|
5
|
+
rawRetentionDays: number;
|
|
6
|
+
hourlyRetentionDays: number;
|
|
7
|
+
dailyRetentionDays: number;
|
|
8
|
+
isCustom: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface RetentionPanelProps {
|
|
12
|
+
data: RetentionData | undefined;
|
|
13
|
+
onFieldChange: (field: string, value: number) => void;
|
|
14
|
+
onSave: () => void;
|
|
15
|
+
onReset: () => void;
|
|
16
|
+
saving: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Panel for configuring tiered retention periods (raw → hourly → daily).
|
|
21
|
+
*/
|
|
22
|
+
export const RetentionPanel: React.FC<RetentionPanelProps> = ({
|
|
23
|
+
data,
|
|
24
|
+
onFieldChange,
|
|
25
|
+
onSave,
|
|
26
|
+
onReset,
|
|
27
|
+
saving,
|
|
28
|
+
}) => {
|
|
29
|
+
if (!data) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex justify-center py-12">
|
|
32
|
+
<LoadingSpinner />
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isValidHierarchy =
|
|
38
|
+
data.rawRetentionDays < data.hourlyRetentionDays &&
|
|
39
|
+
data.hourlyRetentionDays < data.dailyRetentionDays;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="p-6 space-y-3">
|
|
43
|
+
<div>
|
|
44
|
+
<h3 className="text-sm font-semibold">Data Retention</h3>
|
|
45
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
46
|
+
Configure how long health check run data is retained at each
|
|
47
|
+
aggregation tier.
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{!data.isCustom && (
|
|
52
|
+
<div className="rounded-md bg-muted p-3 text-sm text-muted-foreground">
|
|
53
|
+
Using default retention settings. Customize below to override.
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
{!isValidHierarchy && (
|
|
58
|
+
<div className="rounded-md bg-destructive/10 border border-destructive/30 p-3 text-sm text-destructive">
|
|
59
|
+
Retention periods must increase: Raw < Hourly < Daily
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{/* Raw Data */}
|
|
64
|
+
<RetentionTier
|
|
65
|
+
label="Raw Data Retention"
|
|
66
|
+
description="Individual run data before hourly aggregation"
|
|
67
|
+
value={data.rawRetentionDays}
|
|
68
|
+
min={1}
|
|
69
|
+
max={30}
|
|
70
|
+
onChange={(v) => onFieldChange("rawRetentionDays", v)}
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
{/* Hourly Aggregates */}
|
|
74
|
+
<RetentionTier
|
|
75
|
+
label="Hourly Aggregates"
|
|
76
|
+
description="Hourly stats before daily rollup"
|
|
77
|
+
value={data.hourlyRetentionDays}
|
|
78
|
+
min={7}
|
|
79
|
+
max={365}
|
|
80
|
+
onChange={(v) => onFieldChange("hourlyRetentionDays", v)}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
{/* Daily Aggregates */}
|
|
84
|
+
<RetentionTier
|
|
85
|
+
label="Daily Aggregates"
|
|
86
|
+
description="Long-term storage before deletion"
|
|
87
|
+
value={data.dailyRetentionDays}
|
|
88
|
+
min={30}
|
|
89
|
+
max={1095}
|
|
90
|
+
onChange={(v) => onFieldChange("dailyRetentionDays", v)}
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
{/* Actions */}
|
|
94
|
+
<div className="flex justify-between pt-2 border-t">
|
|
95
|
+
<Button
|
|
96
|
+
variant="ghost"
|
|
97
|
+
size="sm"
|
|
98
|
+
onClick={onReset}
|
|
99
|
+
disabled={saving || !data.isCustom}
|
|
100
|
+
>
|
|
101
|
+
Reset to Defaults
|
|
102
|
+
</Button>
|
|
103
|
+
<Button
|
|
104
|
+
size="sm"
|
|
105
|
+
onClick={onSave}
|
|
106
|
+
disabled={saving || !isValidHierarchy}
|
|
107
|
+
>
|
|
108
|
+
{saving ? "Saving..." : "Save Retention"}
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// =============================================================================
|
|
116
|
+
// RETENTION TIER — Shared sub-component
|
|
117
|
+
// =============================================================================
|
|
118
|
+
|
|
119
|
+
function RetentionTier({
|
|
120
|
+
label,
|
|
121
|
+
description,
|
|
122
|
+
value,
|
|
123
|
+
min,
|
|
124
|
+
max,
|
|
125
|
+
onChange,
|
|
126
|
+
}: {
|
|
127
|
+
label: string;
|
|
128
|
+
description: string;
|
|
129
|
+
value: number;
|
|
130
|
+
min: number;
|
|
131
|
+
max: number;
|
|
132
|
+
onChange: (value: number) => void;
|
|
133
|
+
}) {
|
|
134
|
+
return (
|
|
135
|
+
<div className="p-3 rounded-lg border bg-muted/30">
|
|
136
|
+
<div className="flex items-center justify-between">
|
|
137
|
+
<div>
|
|
138
|
+
<span className="text-sm font-medium">{label}</span>
|
|
139
|
+
<p className="text-xs text-muted-foreground">{description}</p>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="flex items-center gap-2">
|
|
142
|
+
<Input
|
|
143
|
+
type="number"
|
|
144
|
+
min={min}
|
|
145
|
+
max={max}
|
|
146
|
+
value={value}
|
|
147
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
148
|
+
className="h-8 w-20 text-center"
|
|
149
|
+
/>
|
|
150
|
+
<span className="text-sm text-muted-foreground w-10">days</span>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { DEFAULT_RETENTION_CONFIG } from "@checkstack/healthcheck-common";
|