@checkstack/healthcheck-frontend 0.5.0 → 0.7.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 +113 -0
- package/package.json +1 -1
- package/src/auto-charts/AutoChartGrid.tsx +447 -99
- package/src/components/AggregatedDataBanner.tsx +3 -3
- package/src/components/CollectorList.tsx +63 -56
- package/src/components/ExpandedResultView.tsx +140 -0
- package/src/components/HealthCheckDiagram.tsx +0 -40
- package/src/components/HealthCheckHistory.tsx +3 -3
- package/src/components/HealthCheckLatencyChart.tsx +14 -4
- package/src/components/HealthCheckList.tsx +53 -11
- package/src/components/HealthCheckRunsTable.tsx +59 -230
- package/src/components/HealthCheckSparkline.tsx +12 -11
- package/src/components/HealthCheckStatusTimeline.tsx +133 -112
- package/src/components/HealthCheckSystemOverview.tsx +188 -47
- package/src/components/SparklineTooltip.tsx +51 -0
- package/src/hooks/useHealthCheckData.ts +78 -28
- package/src/index.tsx +6 -0
- package/src/pages/HealthCheckConfigPage.tsx +27 -5
- package/src/pages/HealthCheckHistoryDetailPage.tsx +62 -6
- package/src/pages/HealthCheckHistoryPage.tsx +6 -3
- package/src/utils/sparkline-downsampling.ts +88 -0
|
@@ -12,14 +12,14 @@ interface AggregatedDataBannerProps {
|
|
|
12
12
|
*/
|
|
13
13
|
function formatDuration(seconds: number): string {
|
|
14
14
|
if (seconds < 60) {
|
|
15
|
-
return
|
|
15
|
+
return `~${seconds}s`;
|
|
16
16
|
}
|
|
17
17
|
if (seconds < 3600) {
|
|
18
18
|
const mins = Math.round(seconds / 60);
|
|
19
|
-
return
|
|
19
|
+
return `~${mins}min`;
|
|
20
20
|
}
|
|
21
21
|
const hours = Math.round(seconds / 3600);
|
|
22
|
-
return
|
|
22
|
+
return `~${hours}h`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Card,
|
|
8
8
|
CardContent,
|
|
9
9
|
CardHeader,
|
|
10
|
+
CardHeaderRow,
|
|
10
11
|
CardTitle,
|
|
11
12
|
Label,
|
|
12
13
|
Select,
|
|
@@ -157,62 +158,68 @@ export const CollectorList: React.FC<CollectorListProps> = ({
|
|
|
157
158
|
|
|
158
159
|
return (
|
|
159
160
|
<Card>
|
|
160
|
-
<CardHeader
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
161
|
+
<CardHeader>
|
|
162
|
+
<CardHeaderRow>
|
|
163
|
+
<CardTitle className="text-base">Check Items</CardTitle>
|
|
164
|
+
{addableCollectors.length > 0 && (
|
|
165
|
+
<Select value="" onValueChange={handleAdd}>
|
|
166
|
+
<SelectTrigger className="w-[200px]">
|
|
167
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
168
|
+
<SelectValue placeholder="Add collector..." />
|
|
169
|
+
</SelectTrigger>
|
|
170
|
+
<SelectContent>
|
|
171
|
+
{/* Built-in collectors first */}
|
|
172
|
+
{builtInCollectors.length > 0 && (
|
|
173
|
+
<>
|
|
174
|
+
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
|
175
|
+
Built-in
|
|
176
|
+
</div>
|
|
177
|
+
{builtInCollectors
|
|
178
|
+
.filter((c) =>
|
|
179
|
+
addableCollectors.some((a) => a.id === c.id),
|
|
180
|
+
)
|
|
181
|
+
.map((collector) => (
|
|
182
|
+
<SelectItem key={collector.id} value={collector.id}>
|
|
183
|
+
<div className="flex items-center gap-2">
|
|
184
|
+
<span>{collector.displayName}</span>
|
|
185
|
+
{collector.allowMultiple && (
|
|
186
|
+
<Badge variant="outline" className="text-xs">
|
|
187
|
+
Multiple
|
|
188
|
+
</Badge>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</SelectItem>
|
|
192
|
+
))}
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
{/* External collectors */}
|
|
196
|
+
{externalCollectors.length > 0 && (
|
|
197
|
+
<>
|
|
198
|
+
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
|
199
|
+
External
|
|
200
|
+
</div>
|
|
201
|
+
{externalCollectors
|
|
202
|
+
.filter((c) =>
|
|
203
|
+
addableCollectors.some((a) => a.id === c.id),
|
|
204
|
+
)
|
|
205
|
+
.map((collector) => (
|
|
206
|
+
<SelectItem key={collector.id} value={collector.id}>
|
|
207
|
+
<div className="flex items-center gap-2">
|
|
208
|
+
<span>{collector.displayName}</span>
|
|
209
|
+
{collector.allowMultiple && (
|
|
210
|
+
<Badge variant="outline" className="text-xs">
|
|
211
|
+
Multiple
|
|
212
|
+
</Badge>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
</SelectItem>
|
|
216
|
+
))}
|
|
217
|
+
</>
|
|
218
|
+
)}
|
|
219
|
+
</SelectContent>
|
|
220
|
+
</Select>
|
|
221
|
+
)}
|
|
222
|
+
</CardHeaderRow>
|
|
216
223
|
</CardHeader>
|
|
217
224
|
<CardContent>
|
|
218
225
|
{configuredCollectors.length === 0 ? (
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExpandedResultView - Displays health check result data in a structured format.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
interface ExpandedResultViewProps {
|
|
6
|
+
result: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Displays the result data in a structured format.
|
|
11
|
+
* Shows collector results as cards with key-value pairs.
|
|
12
|
+
*/
|
|
13
|
+
export function ExpandedResultView({ result }: ExpandedResultViewProps) {
|
|
14
|
+
const metadata = result.metadata as Record<string, unknown> | undefined;
|
|
15
|
+
const rawCollectors = metadata?.collectors;
|
|
16
|
+
|
|
17
|
+
// Type guard for collectors object
|
|
18
|
+
const collectors: Record<string, Record<string, unknown>> | undefined =
|
|
19
|
+
rawCollectors &&
|
|
20
|
+
typeof rawCollectors === "object" &&
|
|
21
|
+
!Array.isArray(rawCollectors)
|
|
22
|
+
? (rawCollectors as Record<string, Record<string, unknown>>)
|
|
23
|
+
: undefined;
|
|
24
|
+
|
|
25
|
+
// Check if we have collectors to display
|
|
26
|
+
const collectorEntries = collectors ? Object.entries(collectors) : [];
|
|
27
|
+
|
|
28
|
+
// Extract connection time as typed value
|
|
29
|
+
const connectionTimeMs = metadata?.connectionTimeMs as number | undefined;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-4">
|
|
33
|
+
<div className="flex gap-4 text-sm">
|
|
34
|
+
<div>
|
|
35
|
+
<span className="text-muted-foreground">Status: </span>
|
|
36
|
+
<span className="font-medium">{String(result.status)}</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div>
|
|
39
|
+
<span className="text-muted-foreground">Latency: </span>
|
|
40
|
+
<span className="font-medium">{String(result.latencyMs)}ms</span>
|
|
41
|
+
</div>
|
|
42
|
+
{connectionTimeMs !== undefined && (
|
|
43
|
+
<div>
|
|
44
|
+
<span className="text-muted-foreground">Connection: </span>
|
|
45
|
+
<span className="font-medium">{connectionTimeMs}ms</span>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{collectorEntries.length > 0 && (
|
|
51
|
+
<div className="space-y-3">
|
|
52
|
+
<h4 className="text-sm font-medium">Collector Results</h4>
|
|
53
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
54
|
+
{collectorEntries.map(([collectorId, collectorResult]) => (
|
|
55
|
+
<CollectorResultCard
|
|
56
|
+
key={collectorId}
|
|
57
|
+
collectorId={collectorId}
|
|
58
|
+
result={collectorResult}
|
|
59
|
+
/>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{result.message ? (
|
|
66
|
+
<div className="text-sm text-muted-foreground">
|
|
67
|
+
{String(result.message)}
|
|
68
|
+
</div>
|
|
69
|
+
) : undefined}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface CollectorResultCardProps {
|
|
75
|
+
collectorId: string;
|
|
76
|
+
result: Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Card displaying a single collector's result values.
|
|
81
|
+
*/
|
|
82
|
+
function CollectorResultCard({
|
|
83
|
+
collectorId,
|
|
84
|
+
result,
|
|
85
|
+
}: CollectorResultCardProps) {
|
|
86
|
+
if (!result || typeof result !== "object") {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Filter out null/undefined values
|
|
91
|
+
const entries = Object.entries(result).filter(
|
|
92
|
+
([, value]) => value !== null && value !== undefined,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="rounded-md border bg-card p-3 space-y-2">
|
|
97
|
+
<h5 className="text-sm font-medium text-primary">{collectorId}</h5>
|
|
98
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
|
|
99
|
+
{entries.map(([key, value]) => (
|
|
100
|
+
<div key={key} className="contents">
|
|
101
|
+
<span className="text-muted-foreground truncate">
|
|
102
|
+
{formatKey(key)}
|
|
103
|
+
</span>
|
|
104
|
+
<span className="font-mono text-xs truncate" title={String(value)}>
|
|
105
|
+
{formatValue(value)}
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Format a camelCase key to a readable label.
|
|
116
|
+
*/
|
|
117
|
+
function formatKey(key: string): string {
|
|
118
|
+
return key
|
|
119
|
+
.replaceAll(/([a-z])([A-Z])/g, "$1 $2")
|
|
120
|
+
.replace(/^./, (c) => c.toUpperCase());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Format a value for display.
|
|
125
|
+
*/
|
|
126
|
+
function formatValue(value: unknown): string {
|
|
127
|
+
if (value === null || value === undefined) return "—";
|
|
128
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
129
|
+
if (typeof value === "number") {
|
|
130
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
return value.length > 3
|
|
134
|
+
? `[${value.slice(0, 3).join(", ")}…]`
|
|
135
|
+
: `[${value.join(", ")}]`;
|
|
136
|
+
}
|
|
137
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
138
|
+
const str = String(value);
|
|
139
|
+
return str.length > 50 ? `${str.slice(0, 47)}…` : str;
|
|
140
|
+
}
|
|
@@ -1,44 +1,4 @@
|
|
|
1
|
-
import { ExtensionSlot } from "@checkstack/frontend-api";
|
|
2
1
|
import { InfoBanner } from "@checkstack/ui";
|
|
3
|
-
import {
|
|
4
|
-
HealthCheckDiagramSlot,
|
|
5
|
-
type HealthCheckDiagramSlotContext,
|
|
6
|
-
} from "../slots";
|
|
7
|
-
import { AggregatedDataBanner } from "./AggregatedDataBanner";
|
|
8
|
-
|
|
9
|
-
interface HealthCheckDiagramProps {
|
|
10
|
-
/** The context from useHealthCheckData - handles both raw and aggregated modes */
|
|
11
|
-
context: HealthCheckDiagramSlotContext;
|
|
12
|
-
/** Whether the data is aggregated (for showing the info banner) */
|
|
13
|
-
isAggregated?: boolean;
|
|
14
|
-
/** The bucket interval in seconds (from aggregated response) */
|
|
15
|
-
bucketIntervalSeconds?: number;
|
|
16
|
-
/** The check interval in seconds (for comparison in banner) */
|
|
17
|
-
checkIntervalSeconds?: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Component that renders the diagram extension slot with the provided context.
|
|
22
|
-
* Expects parent component to fetch data via useHealthCheckData and pass context.
|
|
23
|
-
*/
|
|
24
|
-
export function HealthCheckDiagram({
|
|
25
|
-
context,
|
|
26
|
-
isAggregated = false,
|
|
27
|
-
bucketIntervalSeconds,
|
|
28
|
-
checkIntervalSeconds,
|
|
29
|
-
}: HealthCheckDiagramProps) {
|
|
30
|
-
return (
|
|
31
|
-
<>
|
|
32
|
-
{isAggregated && bucketIntervalSeconds && (
|
|
33
|
-
<AggregatedDataBanner
|
|
34
|
-
bucketIntervalSeconds={bucketIntervalSeconds}
|
|
35
|
-
checkIntervalSeconds={checkIntervalSeconds}
|
|
36
|
-
/>
|
|
37
|
-
)}
|
|
38
|
-
<ExtensionSlot slot={HealthCheckDiagramSlot} context={context} />
|
|
39
|
-
</>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
2
|
|
|
43
3
|
/**
|
|
44
4
|
* Wrapper that shows access message when user lacks access.
|
|
@@ -26,10 +26,10 @@ export const HealthCheckHistory: React.FC<SlotProps> = (props) => {
|
|
|
26
26
|
|
|
27
27
|
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
28
28
|
|
|
29
|
-
// Fetch history with useQuery
|
|
29
|
+
// Fetch history with useQuery - newest first for table view
|
|
30
30
|
const { data, isLoading: loading } = healthCheckClient.getHistory.useQuery(
|
|
31
|
-
{ systemId, configurationId, limit },
|
|
32
|
-
{ enabled: true }
|
|
31
|
+
{ systemId, configurationId, limit, sortOrder: "desc" },
|
|
32
|
+
{ enabled: true },
|
|
33
33
|
);
|
|
34
34
|
|
|
35
35
|
const history = data?.runs ?? [];
|
|
@@ -40,6 +40,7 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
40
40
|
|
|
41
41
|
const chartData = buckets.map((d) => ({
|
|
42
42
|
timestamp: new Date(d.bucketStart).getTime(),
|
|
43
|
+
bucketEndTimestamp: new Date(d.bucketEnd).getTime(),
|
|
43
44
|
latencyMs: d.avgLatencyMs!,
|
|
44
45
|
minLatencyMs: d.minLatencyMs,
|
|
45
46
|
maxLatencyMs: d.maxLatencyMs,
|
|
@@ -54,7 +55,7 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
54
55
|
const timeFormat =
|
|
55
56
|
(buckets[0]?.bucketIntervalSeconds ?? 3600) >= 21_600
|
|
56
57
|
? "MMM d"
|
|
57
|
-
: "MMM d HH:mm";
|
|
58
|
+
: "MMM d, HH:mm";
|
|
58
59
|
|
|
59
60
|
return (
|
|
60
61
|
<ResponsiveContainer width="100%" height={height}>
|
|
@@ -90,6 +91,14 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
90
91
|
content={({ active, payload }) => {
|
|
91
92
|
if (!active || !payload?.length) return;
|
|
92
93
|
const data = payload[0].payload as (typeof chartData)[number];
|
|
94
|
+
const startTime = format(
|
|
95
|
+
new Date(data.timestamp),
|
|
96
|
+
"MMM d, HH:mm",
|
|
97
|
+
);
|
|
98
|
+
const endTime = format(
|
|
99
|
+
new Date(data.bucketEndTimestamp),
|
|
100
|
+
"HH:mm",
|
|
101
|
+
);
|
|
93
102
|
return (
|
|
94
103
|
<div
|
|
95
104
|
className="rounded-md border bg-popover p-2 text-sm shadow-md"
|
|
@@ -99,9 +108,9 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
99
108
|
}}
|
|
100
109
|
>
|
|
101
110
|
<p className="text-muted-foreground">
|
|
102
|
-
{
|
|
111
|
+
{startTime} - {endTime}
|
|
103
112
|
</p>
|
|
104
|
-
<p className="font-medium">{data.latencyMs}ms</p>
|
|
113
|
+
<p className="font-medium">{data.latencyMs}ms (avg)</p>
|
|
105
114
|
</div>
|
|
106
115
|
);
|
|
107
116
|
}}
|
|
@@ -145,7 +154,8 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
145
154
|
);
|
|
146
155
|
}
|
|
147
156
|
|
|
148
|
-
|
|
157
|
+
// Runs come in chronological order from API (oldest first, newest last)
|
|
158
|
+
const chartData = runs.map((d) => ({
|
|
149
159
|
timestamp: new Date(d.timestamp).getTime(),
|
|
150
160
|
latencyMs: d.latencyMs!,
|
|
151
161
|
}));
|
|
@@ -11,14 +11,18 @@ import {
|
|
|
11
11
|
TableHeader,
|
|
12
12
|
TableRow,
|
|
13
13
|
Button,
|
|
14
|
+
Badge,
|
|
14
15
|
} from "@checkstack/ui";
|
|
15
|
-
import { Trash2, Edit } from "lucide-react";
|
|
16
|
+
import { Trash2, Edit, Pause, Play } from "lucide-react";
|
|
16
17
|
|
|
17
18
|
interface HealthCheckListProps {
|
|
18
19
|
configurations: HealthCheckConfiguration[];
|
|
19
20
|
strategies: HealthCheckStrategyDto[];
|
|
20
21
|
onEdit: (config: HealthCheckConfiguration) => void;
|
|
21
22
|
onDelete: (id: string) => void;
|
|
23
|
+
onPause?: (id: string) => void;
|
|
24
|
+
onResume?: (id: string) => void;
|
|
25
|
+
canManage?: boolean;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export const HealthCheckList: React.FC<HealthCheckListProps> = ({
|
|
@@ -26,6 +30,9 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
|
|
|
26
30
|
strategies,
|
|
27
31
|
onEdit,
|
|
28
32
|
onDelete,
|
|
33
|
+
onPause,
|
|
34
|
+
onResume,
|
|
35
|
+
canManage = true,
|
|
29
36
|
}) => {
|
|
30
37
|
const getStrategyName = (id: string) => {
|
|
31
38
|
return strategies.find((s) => s.id === id)?.displayName || id;
|
|
@@ -39,24 +46,57 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
|
|
|
39
46
|
<TableHead>Name</TableHead>
|
|
40
47
|
<TableHead>Strategy</TableHead>
|
|
41
48
|
<TableHead>Interval (s)</TableHead>
|
|
49
|
+
<TableHead>Status</TableHead>
|
|
42
50
|
<TableHead className="text-right">Actions</TableHead>
|
|
43
51
|
</TableRow>
|
|
44
52
|
</TableHeader>
|
|
45
53
|
<TableBody>
|
|
46
54
|
{configurations.length === 0 ? (
|
|
47
55
|
<TableRow>
|
|
48
|
-
<TableCell colSpan={
|
|
56
|
+
<TableCell colSpan={5} className="h-24 text-center">
|
|
49
57
|
No health checks configured.
|
|
50
58
|
</TableCell>
|
|
51
59
|
</TableRow>
|
|
52
60
|
) : (
|
|
53
61
|
configurations.map((config) => (
|
|
54
|
-
<TableRow
|
|
62
|
+
<TableRow
|
|
63
|
+
key={config.id}
|
|
64
|
+
className={config.paused ? "opacity-60" : ""}
|
|
65
|
+
>
|
|
55
66
|
<TableCell className="font-medium">{config.name}</TableCell>
|
|
56
67
|
<TableCell>{getStrategyName(config.strategyId)}</TableCell>
|
|
57
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>
|
|
58
76
|
<TableCell className="text-right">
|
|
59
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
|
+
))}
|
|
60
100
|
<Button
|
|
61
101
|
variant="ghost"
|
|
62
102
|
size="icon"
|
|
@@ -64,14 +104,16 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
|
|
|
64
104
|
>
|
|
65
105
|
<Edit className="h-4 w-4" />
|
|
66
106
|
</Button>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
)}
|
|
75
117
|
</div>
|
|
76
118
|
</TableCell>
|
|
77
119
|
</TableRow>
|