@checkmate-monitor/healthcheck-frontend 0.1.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 +63 -0
- package/package.json +36 -0
- package/src/api.ts +17 -0
- package/src/auto-charts/AutoChartGrid.tsx +383 -0
- package/src/auto-charts/extension.tsx +27 -0
- package/src/auto-charts/index.ts +12 -0
- package/src/auto-charts/schema-parser.ts +121 -0
- package/src/auto-charts/useStrategySchemas.ts +62 -0
- package/src/components/AggregatedDataBanner.tsx +24 -0
- package/src/components/HealthCheckDiagram.tsx +88 -0
- package/src/components/HealthCheckEditor.tsx +136 -0
- package/src/components/HealthCheckHistory.tsx +79 -0
- package/src/components/HealthCheckLatencyChart.tsx +168 -0
- package/src/components/HealthCheckList.tsx +84 -0
- package/src/components/HealthCheckMenuItems.tsx +27 -0
- package/src/components/HealthCheckRunsTable.tsx +187 -0
- package/src/components/HealthCheckSparkline.tsx +46 -0
- package/src/components/HealthCheckStatusTimeline.tsx +190 -0
- package/src/components/HealthCheckSystemOverview.tsx +328 -0
- package/src/components/SystemHealthBadge.tsx +46 -0
- package/src/components/SystemHealthCheckAssignment.tsx +869 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useHealthCheckData.ts +257 -0
- package/src/index.tsx +100 -0
- package/src/pages/HealthCheckConfigPage.tsx +151 -0
- package/src/pages/HealthCheckHistoryDetailPage.tsx +100 -0
- package/src/pages/HealthCheckHistoryPage.tsx +67 -0
- package/src/slots.tsx +185 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { useApi, type SlotContext } from "@checkmate-monitor/frontend-api";
|
|
3
|
+
import { useSignal } from "@checkmate-monitor/signal-frontend";
|
|
4
|
+
import { healthCheckApiRef } from "../api";
|
|
5
|
+
import { SystemDetailsSlot } from "@checkmate-monitor/catalog-common";
|
|
6
|
+
import { HEALTH_CHECK_RUN_COMPLETED } from "@checkmate-monitor/healthcheck-common";
|
|
7
|
+
import {
|
|
8
|
+
HealthBadge,
|
|
9
|
+
LoadingSpinner,
|
|
10
|
+
Table,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
TableHead,
|
|
14
|
+
TableBody,
|
|
15
|
+
TableCell,
|
|
16
|
+
Tooltip,
|
|
17
|
+
Pagination,
|
|
18
|
+
usePagination,
|
|
19
|
+
DateRangeFilter,
|
|
20
|
+
} from "@checkmate-monitor/ui";
|
|
21
|
+
import { formatDistanceToNow } from "date-fns";
|
|
22
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
23
|
+
import { HealthCheckSparkline } from "./HealthCheckSparkline";
|
|
24
|
+
import { HealthCheckLatencyChart } from "./HealthCheckLatencyChart";
|
|
25
|
+
import { HealthCheckStatusTimeline } from "./HealthCheckStatusTimeline";
|
|
26
|
+
|
|
27
|
+
import type {
|
|
28
|
+
StateThresholds,
|
|
29
|
+
HealthCheckStatus,
|
|
30
|
+
} from "@checkmate-monitor/healthcheck-common";
|
|
31
|
+
import { HealthCheckDiagram } from "./HealthCheckDiagram";
|
|
32
|
+
|
|
33
|
+
type SlotProps = SlotContext<typeof SystemDetailsSlot>;
|
|
34
|
+
|
|
35
|
+
interface HealthCheckOverviewItem {
|
|
36
|
+
configurationId: string;
|
|
37
|
+
configurationName: string;
|
|
38
|
+
strategyId: string;
|
|
39
|
+
intervalSeconds: number;
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
status: HealthCheckStatus;
|
|
42
|
+
stateThresholds?: StateThresholds;
|
|
43
|
+
recentRuns: Array<{
|
|
44
|
+
id: string;
|
|
45
|
+
status: HealthCheckStatus;
|
|
46
|
+
timestamp: Date;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ExpandedRowProps {
|
|
51
|
+
item: HealthCheckOverviewItem;
|
|
52
|
+
systemId: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
56
|
+
const api = useApi(healthCheckApiRef);
|
|
57
|
+
|
|
58
|
+
// Date range state for filtering
|
|
59
|
+
const [dateRange, setDateRange] = useState<{
|
|
60
|
+
startDate: Date;
|
|
61
|
+
endDate: Date;
|
|
62
|
+
}>(() => {
|
|
63
|
+
// Default to last 24 hours
|
|
64
|
+
const end = new Date();
|
|
65
|
+
const start = new Date();
|
|
66
|
+
start.setHours(start.getHours() - 24);
|
|
67
|
+
return { startDate: start, endDate: end };
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// usePagination now uses refs internally - no memoization needed
|
|
71
|
+
const {
|
|
72
|
+
items: runs,
|
|
73
|
+
loading,
|
|
74
|
+
pagination,
|
|
75
|
+
} = usePagination({
|
|
76
|
+
fetchFn: (params: {
|
|
77
|
+
limit: number;
|
|
78
|
+
offset: number;
|
|
79
|
+
systemId: string;
|
|
80
|
+
configurationId: string;
|
|
81
|
+
startDate?: Date;
|
|
82
|
+
endDate?: Date;
|
|
83
|
+
}) =>
|
|
84
|
+
api.getHistory({
|
|
85
|
+
systemId: params.systemId,
|
|
86
|
+
configurationId: params.configurationId,
|
|
87
|
+
limit: params.limit,
|
|
88
|
+
offset: params.offset,
|
|
89
|
+
startDate: params.startDate,
|
|
90
|
+
endDate: params.endDate,
|
|
91
|
+
}),
|
|
92
|
+
getItems: (response) => response.runs,
|
|
93
|
+
getTotal: (response) => response.total,
|
|
94
|
+
extraParams: {
|
|
95
|
+
systemId,
|
|
96
|
+
configurationId: item.configurationId,
|
|
97
|
+
startDate: dateRange.startDate,
|
|
98
|
+
endDate: dateRange.endDate,
|
|
99
|
+
},
|
|
100
|
+
defaultLimit: 10,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const thresholdDescription = item.stateThresholds
|
|
104
|
+
? item.stateThresholds.mode === "consecutive"
|
|
105
|
+
? `Consecutive mode: Healthy after ${item.stateThresholds.healthy.minSuccessCount} success(es), Degraded after ${item.stateThresholds.degraded.minFailureCount} failure(s), Unhealthy after ${item.stateThresholds.unhealthy.minFailureCount} failure(s)`
|
|
106
|
+
: `Window mode (${item.stateThresholds.windowSize} runs): Degraded at ${item.stateThresholds.degraded.minFailureCount}+ failures, Unhealthy at ${item.stateThresholds.unhealthy.minFailureCount}+ failures`
|
|
107
|
+
: "Using default thresholds";
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="p-4 bg-muted/30 border-t space-y-4">
|
|
111
|
+
<div className="flex flex-wrap gap-4 text-sm">
|
|
112
|
+
<div>
|
|
113
|
+
<span className="text-muted-foreground">Strategy:</span>{" "}
|
|
114
|
+
<span className="font-medium">{item.strategyId}</span>
|
|
115
|
+
</div>
|
|
116
|
+
<div>
|
|
117
|
+
<span className="text-muted-foreground">Interval:</span>{" "}
|
|
118
|
+
<span className="font-medium">{item.intervalSeconds}s</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div className="flex items-center gap-1">
|
|
121
|
+
<span className="text-muted-foreground">Thresholds:</span>{" "}
|
|
122
|
+
<Tooltip content={thresholdDescription} />
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Date Range Filter */}
|
|
127
|
+
<div className="flex items-center gap-2">
|
|
128
|
+
<span className="text-sm text-muted-foreground">Time Range:</span>
|
|
129
|
+
<DateRangeFilter value={dateRange} onChange={setDateRange} />
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Charts Section - always render if we have run data */}
|
|
133
|
+
{runs.length > 0 && (
|
|
134
|
+
<div className="space-y-4">
|
|
135
|
+
{/* Status Timeline */}
|
|
136
|
+
<div>
|
|
137
|
+
<h4 className="text-sm font-medium mb-2">Status Timeline</h4>
|
|
138
|
+
<HealthCheckStatusTimeline
|
|
139
|
+
type="raw"
|
|
140
|
+
data={runs.map((r) => ({
|
|
141
|
+
timestamp: new Date(r.timestamp),
|
|
142
|
+
status: r.status,
|
|
143
|
+
}))}
|
|
144
|
+
height={50}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{/* Latency Chart - only if any run has latency data */}
|
|
149
|
+
{runs.some((r) => r.latencyMs !== undefined) && (
|
|
150
|
+
<div>
|
|
151
|
+
<h4 className="text-sm font-medium mb-2">Response Latency</h4>
|
|
152
|
+
<HealthCheckLatencyChart
|
|
153
|
+
type="raw"
|
|
154
|
+
data={runs
|
|
155
|
+
.filter((r) => r.latencyMs !== undefined)
|
|
156
|
+
.map((r) => ({
|
|
157
|
+
timestamp: new Date(r.timestamp),
|
|
158
|
+
latencyMs: r.latencyMs!,
|
|
159
|
+
status: r.status,
|
|
160
|
+
}))}
|
|
161
|
+
height={150}
|
|
162
|
+
showAverage
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{/* Extension Slot for custom strategy-specific diagrams */}
|
|
168
|
+
<HealthCheckDiagram
|
|
169
|
+
systemId={systemId}
|
|
170
|
+
configurationId={item.configurationId}
|
|
171
|
+
strategyId={item.strategyId}
|
|
172
|
+
dateRange={dateRange}
|
|
173
|
+
limit={pagination.limit}
|
|
174
|
+
offset={pagination.page * pagination.limit - pagination.limit}
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{loading ? (
|
|
180
|
+
<LoadingSpinner />
|
|
181
|
+
) : runs.length > 0 ? (
|
|
182
|
+
<>
|
|
183
|
+
<div className="rounded-md border">
|
|
184
|
+
<Table>
|
|
185
|
+
<TableHeader>
|
|
186
|
+
<TableRow>
|
|
187
|
+
<TableHead className="w-24">Status</TableHead>
|
|
188
|
+
<TableHead>Time</TableHead>
|
|
189
|
+
</TableRow>
|
|
190
|
+
</TableHeader>
|
|
191
|
+
<TableBody>
|
|
192
|
+
{runs.map((run) => (
|
|
193
|
+
<TableRow key={run.id}>
|
|
194
|
+
<TableCell>
|
|
195
|
+
<HealthBadge status={run.status} />
|
|
196
|
+
</TableCell>
|
|
197
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
198
|
+
{formatDistanceToNow(new Date(run.timestamp), {
|
|
199
|
+
addSuffix: true,
|
|
200
|
+
})}
|
|
201
|
+
</TableCell>
|
|
202
|
+
</TableRow>
|
|
203
|
+
))}
|
|
204
|
+
</TableBody>
|
|
205
|
+
</Table>
|
|
206
|
+
</div>
|
|
207
|
+
{pagination.totalPages > 1 && (
|
|
208
|
+
<Pagination
|
|
209
|
+
page={pagination.page}
|
|
210
|
+
totalPages={pagination.totalPages}
|
|
211
|
+
onPageChange={pagination.setPage}
|
|
212
|
+
total={pagination.total}
|
|
213
|
+
limit={pagination.limit}
|
|
214
|
+
onPageSizeChange={pagination.setLimit}
|
|
215
|
+
showTotal
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
</>
|
|
219
|
+
) : (
|
|
220
|
+
<p className="text-sm text-muted-foreground italic">No runs yet</p>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const HealthCheckSystemOverview: React.FC<SlotProps> = (props) => {
|
|
227
|
+
const { system } = props;
|
|
228
|
+
const systemId = system?.id;
|
|
229
|
+
|
|
230
|
+
const api = useApi(healthCheckApiRef);
|
|
231
|
+
const [overview, setOverview] = useState<HealthCheckOverviewItem[]>([]);
|
|
232
|
+
const [loading, setLoading] = useState(true);
|
|
233
|
+
const [expandedId, setExpandedId] = useState<string>();
|
|
234
|
+
|
|
235
|
+
const refetch = useCallback(() => {
|
|
236
|
+
if (!systemId) return;
|
|
237
|
+
|
|
238
|
+
api
|
|
239
|
+
.getSystemHealthOverview({ systemId })
|
|
240
|
+
.then((data) => setOverview(data.checks))
|
|
241
|
+
.finally(() => setLoading(false));
|
|
242
|
+
}, [api, systemId]);
|
|
243
|
+
|
|
244
|
+
// Initial fetch
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
refetch();
|
|
247
|
+
}, [refetch]);
|
|
248
|
+
|
|
249
|
+
// Listen for realtime health check updates
|
|
250
|
+
useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
|
|
251
|
+
if (changedId === systemId) {
|
|
252
|
+
refetch();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (loading) return <LoadingSpinner />;
|
|
257
|
+
|
|
258
|
+
if (overview.length === 0) {
|
|
259
|
+
return (
|
|
260
|
+
<p className="text-muted-foreground text-sm">
|
|
261
|
+
No health checks assigned to this system.
|
|
262
|
+
</p>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div className="space-y-2">
|
|
268
|
+
{overview.map((item) => {
|
|
269
|
+
const isExpanded = expandedId === item.configurationId;
|
|
270
|
+
const lastRun = item.recentRuns[0];
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div
|
|
274
|
+
key={item.configurationId}
|
|
275
|
+
className="rounded-lg border bg-card transition-shadow hover:shadow-sm"
|
|
276
|
+
>
|
|
277
|
+
<div
|
|
278
|
+
className="flex items-center gap-4 p-3 cursor-pointer"
|
|
279
|
+
onClick={() =>
|
|
280
|
+
setExpandedId(isExpanded ? undefined : item.configurationId)
|
|
281
|
+
}
|
|
282
|
+
>
|
|
283
|
+
<div className="text-muted-foreground">
|
|
284
|
+
{isExpanded ? (
|
|
285
|
+
<ChevronDown className="h-4 w-4" />
|
|
286
|
+
) : (
|
|
287
|
+
<ChevronRight className="h-4 w-4" />
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div className="flex-1 min-w-0">
|
|
292
|
+
<div className="flex items-center gap-2">
|
|
293
|
+
<span className="font-medium text-sm truncate">
|
|
294
|
+
{item.configurationName}
|
|
295
|
+
</span>
|
|
296
|
+
{!item.enabled && (
|
|
297
|
+
<span className="text-xs text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
|
|
298
|
+
Disabled
|
|
299
|
+
</span>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
{lastRun && (
|
|
303
|
+
<span className="text-xs text-muted-foreground">
|
|
304
|
+
Last run:{" "}
|
|
305
|
+
{formatDistanceToNow(new Date(lastRun.timestamp), {
|
|
306
|
+
addSuffix: true,
|
|
307
|
+
})}
|
|
308
|
+
</span>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<HealthCheckSparkline
|
|
313
|
+
runs={item.recentRuns}
|
|
314
|
+
className="hidden sm:flex"
|
|
315
|
+
/>
|
|
316
|
+
|
|
317
|
+
<HealthBadge status={item.status} />
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
{isExpanded && systemId && (
|
|
321
|
+
<ExpandedDetails item={item} systemId={systemId} />
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
})}
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { useApi, type SlotContext } from "@checkmate-monitor/frontend-api";
|
|
3
|
+
import { useSignal } from "@checkmate-monitor/signal-frontend";
|
|
4
|
+
import { SystemStateBadgesSlot } from "@checkmate-monitor/catalog-common";
|
|
5
|
+
import { HEALTH_CHECK_RUN_COMPLETED } from "@checkmate-monitor/healthcheck-common";
|
|
6
|
+
import { healthCheckApiRef } from "../api";
|
|
7
|
+
import { HealthBadge, type HealthStatus } from "@checkmate-monitor/ui";
|
|
8
|
+
|
|
9
|
+
type Props = SlotContext<typeof SystemStateBadgesSlot>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Displays a health badge for a system based on its health check results.
|
|
13
|
+
* Uses the backend's getSystemHealthStatus endpoint which evaluates
|
|
14
|
+
* health status based on configured state thresholds.
|
|
15
|
+
* Listens for realtime updates via signals.
|
|
16
|
+
*/
|
|
17
|
+
export const SystemHealthBadge: React.FC<Props> = ({ system }) => {
|
|
18
|
+
const api = useApi(healthCheckApiRef);
|
|
19
|
+
const [status, setStatus] = useState<HealthStatus>();
|
|
20
|
+
|
|
21
|
+
const refetch = useCallback(() => {
|
|
22
|
+
if (!system?.id) return;
|
|
23
|
+
|
|
24
|
+
api
|
|
25
|
+
.getSystemHealthStatus({ systemId: system.id })
|
|
26
|
+
.then((result) => {
|
|
27
|
+
setStatus(result.status);
|
|
28
|
+
})
|
|
29
|
+
.catch(console.error);
|
|
30
|
+
}, [system?.id, api]);
|
|
31
|
+
|
|
32
|
+
// Initial fetch
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
refetch();
|
|
35
|
+
}, [refetch]);
|
|
36
|
+
|
|
37
|
+
// Listen for realtime health check updates
|
|
38
|
+
useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
|
|
39
|
+
if (changedId === system?.id) {
|
|
40
|
+
refetch();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!status) return;
|
|
45
|
+
return <HealthBadge status={status} />;
|
|
46
|
+
};
|