@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.
@@ -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
+ };