@checkstack/healthcheck-frontend 0.0.2

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,380 @@
1
+ import React, { useEffect, useState, useCallback } from "react";
2
+ import { useApi, type SlotContext } from "@checkstack/frontend-api";
3
+ import { useSignal } from "@checkstack/signal-frontend";
4
+ import { healthCheckApiRef } from "../api";
5
+ import { SystemDetailsSlot } from "@checkstack/catalog-common";
6
+ import { HEALTH_CHECK_RUN_COMPLETED } from "@checkstack/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 "@checkstack/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 "@checkstack/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
+ // Chart data - fetches all runs for the time range (unpaginated)
71
+ const [chartData, setChartData] = useState<
72
+ Array<{
73
+ id: string;
74
+ status: HealthCheckStatus;
75
+ timestamp: Date;
76
+ latencyMs?: number;
77
+ }>
78
+ >([]);
79
+ const [chartLoading, setChartLoading] = useState(true);
80
+
81
+ const fetchChartData = useCallback(() => {
82
+ setChartLoading(true);
83
+ api
84
+ .getHistory({
85
+ systemId,
86
+ configurationId: item.configurationId,
87
+ startDate: dateRange.startDate,
88
+ endDate: dateRange.endDate,
89
+ // Fetch up to 1000 data points for charts - enough for most time ranges
90
+ limit: 1000,
91
+ offset: 0,
92
+ })
93
+ .then((response) => {
94
+ setChartData(response.runs);
95
+ })
96
+ .finally(() => {
97
+ setChartLoading(false);
98
+ });
99
+ }, [
100
+ api,
101
+ systemId,
102
+ item.configurationId,
103
+ dateRange.startDate,
104
+ dateRange.endDate,
105
+ ]);
106
+
107
+ // Fetch chart data when date range changes
108
+ useEffect(() => {
109
+ fetchChartData();
110
+ }, [fetchChartData]);
111
+
112
+ // Paginated history for the table
113
+ const {
114
+ items: runs,
115
+ loading,
116
+ pagination,
117
+ } = usePagination({
118
+ fetchFn: (params: {
119
+ limit: number;
120
+ offset: number;
121
+ systemId: string;
122
+ configurationId: string;
123
+ startDate?: Date;
124
+ endDate?: Date;
125
+ }) =>
126
+ api.getHistory({
127
+ systemId: params.systemId,
128
+ configurationId: params.configurationId,
129
+ limit: params.limit,
130
+ offset: params.offset,
131
+ startDate: params.startDate,
132
+ endDate: params.endDate,
133
+ }),
134
+ getItems: (response) => response.runs,
135
+ getTotal: (response) => response.total,
136
+ extraParams: {
137
+ systemId,
138
+ configurationId: item.configurationId,
139
+ startDate: dateRange.startDate,
140
+ endDate: dateRange.endDate,
141
+ },
142
+ defaultLimit: 10,
143
+ });
144
+
145
+ // Listen for realtime health check updates to refresh charts and history
146
+ useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
147
+ if (changedId === systemId) {
148
+ fetchChartData();
149
+ pagination.refetch();
150
+ }
151
+ });
152
+
153
+ const thresholdDescription = item.stateThresholds
154
+ ? item.stateThresholds.mode === "consecutive"
155
+ ? `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)`
156
+ : `Window mode (${item.stateThresholds.windowSize} runs): Degraded at ${item.stateThresholds.degraded.minFailureCount}+ failures, Unhealthy at ${item.stateThresholds.unhealthy.minFailureCount}+ failures`
157
+ : "Using default thresholds";
158
+
159
+ return (
160
+ <div className="p-4 bg-muted/30 border-t space-y-4">
161
+ <div className="flex flex-wrap gap-4 text-sm">
162
+ <div>
163
+ <span className="text-muted-foreground">Strategy:</span>{" "}
164
+ <span className="font-medium">{item.strategyId}</span>
165
+ </div>
166
+ <div>
167
+ <span className="text-muted-foreground">Interval:</span>{" "}
168
+ <span className="font-medium">{item.intervalSeconds}s</span>
169
+ </div>
170
+ <div className="flex items-center gap-1">
171
+ <span className="text-muted-foreground">Thresholds:</span>{" "}
172
+ <Tooltip content={thresholdDescription} />
173
+ </div>
174
+ </div>
175
+
176
+ {/* Date Range Filter */}
177
+ <div className="flex items-center gap-2">
178
+ <span className="text-sm text-muted-foreground">Time Range:</span>
179
+ <DateRangeFilter value={dateRange} onChange={setDateRange} />
180
+ </div>
181
+
182
+ {/* Charts Section - uses full date range data, not paginated */}
183
+ {chartLoading ? (
184
+ <LoadingSpinner />
185
+ ) : chartData.length > 0 ? (
186
+ <div className="space-y-4">
187
+ {/* Status Timeline */}
188
+ <div>
189
+ <h4 className="text-sm font-medium mb-2">Status Timeline</h4>
190
+ <HealthCheckStatusTimeline
191
+ type="raw"
192
+ data={chartData.map((r) => ({
193
+ timestamp: new Date(r.timestamp),
194
+ status: r.status,
195
+ }))}
196
+ height={50}
197
+ />
198
+ </div>
199
+
200
+ {/* Latency Chart - only if any run has latency data */}
201
+ {chartData.some((r) => r.latencyMs !== undefined) && (
202
+ <div>
203
+ <h4 className="text-sm font-medium mb-2">Response Latency</h4>
204
+ <HealthCheckLatencyChart
205
+ type="raw"
206
+ data={chartData
207
+ .filter((r) => r.latencyMs !== undefined)
208
+ .map((r) => ({
209
+ timestamp: new Date(r.timestamp),
210
+ latencyMs: r.latencyMs!,
211
+ status: r.status,
212
+ }))}
213
+ height={150}
214
+ showAverage
215
+ />
216
+ </div>
217
+ )}
218
+
219
+ {/* Extension Slot for custom strategy-specific diagrams */}
220
+ <HealthCheckDiagram
221
+ systemId={systemId}
222
+ configurationId={item.configurationId}
223
+ strategyId={item.strategyId}
224
+ dateRange={dateRange}
225
+ limit={1000}
226
+ offset={0}
227
+ />
228
+ </div>
229
+ ) : undefined}
230
+
231
+ {loading ? (
232
+ <LoadingSpinner />
233
+ ) : runs.length > 0 ? (
234
+ <>
235
+ <div className="rounded-md border">
236
+ <Table>
237
+ <TableHeader>
238
+ <TableRow>
239
+ <TableHead className="w-24">Status</TableHead>
240
+ <TableHead>Time</TableHead>
241
+ </TableRow>
242
+ </TableHeader>
243
+ <TableBody>
244
+ {runs.map((run) => (
245
+ <TableRow key={run.id}>
246
+ <TableCell>
247
+ <HealthBadge status={run.status} />
248
+ </TableCell>
249
+ <TableCell className="text-sm text-muted-foreground">
250
+ {formatDistanceToNow(new Date(run.timestamp), {
251
+ addSuffix: true,
252
+ })}
253
+ </TableCell>
254
+ </TableRow>
255
+ ))}
256
+ </TableBody>
257
+ </Table>
258
+ </div>
259
+ {pagination.totalPages > 1 && (
260
+ <Pagination
261
+ page={pagination.page}
262
+ totalPages={pagination.totalPages}
263
+ onPageChange={pagination.setPage}
264
+ total={pagination.total}
265
+ limit={pagination.limit}
266
+ onPageSizeChange={pagination.setLimit}
267
+ showTotal
268
+ />
269
+ )}
270
+ </>
271
+ ) : (
272
+ <p className="text-sm text-muted-foreground italic">No runs yet</p>
273
+ )}
274
+ </div>
275
+ );
276
+ };
277
+
278
+ export const HealthCheckSystemOverview: React.FC<SlotProps> = (props) => {
279
+ const { system } = props;
280
+ const systemId = system?.id;
281
+
282
+ const api = useApi(healthCheckApiRef);
283
+ const [overview, setOverview] = useState<HealthCheckOverviewItem[]>([]);
284
+ const [loading, setLoading] = useState(true);
285
+ const [expandedId, setExpandedId] = useState<string>();
286
+
287
+ const refetch = useCallback(() => {
288
+ if (!systemId) return;
289
+
290
+ api
291
+ .getSystemHealthOverview({ systemId })
292
+ .then((data) => setOverview(data.checks))
293
+ .finally(() => setLoading(false));
294
+ }, [api, systemId]);
295
+
296
+ // Initial fetch
297
+ useEffect(() => {
298
+ refetch();
299
+ }, [refetch]);
300
+
301
+ // Listen for realtime health check updates
302
+ useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
303
+ if (changedId === systemId) {
304
+ refetch();
305
+ }
306
+ });
307
+
308
+ if (loading) return <LoadingSpinner />;
309
+
310
+ if (overview.length === 0) {
311
+ return (
312
+ <p className="text-muted-foreground text-sm">
313
+ No health checks assigned to this system.
314
+ </p>
315
+ );
316
+ }
317
+
318
+ return (
319
+ <div className="space-y-2">
320
+ {overview.map((item) => {
321
+ const isExpanded = expandedId === item.configurationId;
322
+ const lastRun = item.recentRuns[0];
323
+
324
+ return (
325
+ <div
326
+ key={item.configurationId}
327
+ className="rounded-lg border bg-card transition-shadow hover:shadow-sm"
328
+ >
329
+ <div
330
+ className="flex items-center gap-4 p-3 cursor-pointer"
331
+ onClick={() =>
332
+ setExpandedId(isExpanded ? undefined : item.configurationId)
333
+ }
334
+ >
335
+ <div className="text-muted-foreground">
336
+ {isExpanded ? (
337
+ <ChevronDown className="h-4 w-4" />
338
+ ) : (
339
+ <ChevronRight className="h-4 w-4" />
340
+ )}
341
+ </div>
342
+
343
+ <div className="flex-1 min-w-0">
344
+ <div className="flex items-center gap-2">
345
+ <span className="font-medium text-sm truncate">
346
+ {item.configurationName}
347
+ </span>
348
+ {!item.enabled && (
349
+ <span className="text-xs text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
350
+ Disabled
351
+ </span>
352
+ )}
353
+ </div>
354
+ {lastRun && (
355
+ <span className="text-xs text-muted-foreground">
356
+ Last run:{" "}
357
+ {formatDistanceToNow(new Date(lastRun.timestamp), {
358
+ addSuffix: true,
359
+ })}
360
+ </span>
361
+ )}
362
+ </div>
363
+
364
+ <HealthCheckSparkline
365
+ runs={item.recentRuns}
366
+ className="hidden sm:flex"
367
+ />
368
+
369
+ <HealthBadge status={item.status} />
370
+ </div>
371
+
372
+ {isExpanded && systemId && (
373
+ <ExpandedDetails item={item} systemId={systemId} />
374
+ )}
375
+ </div>
376
+ );
377
+ })}
378
+ </div>
379
+ );
380
+ };
@@ -0,0 +1,46 @@
1
+ import React, { useEffect, useState, useCallback } from "react";
2
+ import { useApi, type SlotContext } from "@checkstack/frontend-api";
3
+ import { useSignal } from "@checkstack/signal-frontend";
4
+ import { SystemStateBadgesSlot } from "@checkstack/catalog-common";
5
+ import { HEALTH_CHECK_RUN_COMPLETED } from "@checkstack/healthcheck-common";
6
+ import { healthCheckApiRef } from "../api";
7
+ import { HealthBadge, type HealthStatus } from "@checkstack/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
+ };