@checkstack/healthcheck-frontend 0.12.1 → 0.13.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.
@@ -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 &lt; Hourly &lt; 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";