@checkstack/healthcheck-frontend 0.0.2 → 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 +73 -0
- package/package.json +1 -1
- package/src/auto-charts/AutoChartGrid.tsx +377 -78
- package/src/auto-charts/index.ts +1 -1
- package/src/auto-charts/schema-parser.ts +122 -51
- package/src/components/AssertionBuilder.tsx +425 -0
- package/src/components/CollectorList.tsx +303 -0
- package/src/components/HealthCheckDiagram.tsx +39 -62
- package/src/components/HealthCheckEditor.tsx +37 -8
- package/src/components/HealthCheckLatencyChart.tsx +119 -59
- package/src/components/HealthCheckRunsTable.tsx +142 -6
- package/src/components/HealthCheckStatusTimeline.tsx +35 -52
- package/src/components/HealthCheckSystemOverview.tsx +208 -185
- package/src/hooks/useCollectors.ts +63 -0
- package/src/hooks/useHealthCheckData.ts +52 -33
|
@@ -8,50 +8,130 @@ import {
|
|
|
8
8
|
ReferenceLine,
|
|
9
9
|
} from "recharts";
|
|
10
10
|
import { format } from "date-fns";
|
|
11
|
+
import type { HealthCheckDiagramSlotContext } from "../slots";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
latencyMs: number;
|
|
15
|
-
status: "healthy" | "degraded" | "unhealthy";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface AggregatedLatencyDataPoint {
|
|
19
|
-
bucketStart: Date;
|
|
20
|
-
avgLatencyMs: number;
|
|
21
|
-
minLatencyMs?: number;
|
|
22
|
-
maxLatencyMs?: number;
|
|
23
|
-
bucketSize: "hourly" | "daily";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type RawLatencyChartProps = {
|
|
27
|
-
type: "raw";
|
|
28
|
-
data: LatencyDataPoint[];
|
|
29
|
-
height?: number;
|
|
30
|
-
showAverage?: boolean;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
type AggregatedLatencyChartProps = {
|
|
34
|
-
type: "aggregated";
|
|
35
|
-
data: AggregatedLatencyDataPoint[];
|
|
13
|
+
interface HealthCheckLatencyChartProps {
|
|
14
|
+
context: HealthCheckDiagramSlotContext;
|
|
36
15
|
height?: number;
|
|
37
16
|
showAverage?: boolean;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
type HealthCheckLatencyChartProps =
|
|
41
|
-
| RawLatencyChartProps
|
|
42
|
-
| AggregatedLatencyChartProps;
|
|
17
|
+
}
|
|
43
18
|
|
|
44
19
|
/**
|
|
45
20
|
* Area chart showing health check latency over time.
|
|
46
21
|
* Supports both raw per-run data and aggregated bucket data.
|
|
47
22
|
* Uses HSL CSS variables for theming consistency.
|
|
48
23
|
*/
|
|
49
|
-
export const HealthCheckLatencyChart: React.FC<
|
|
50
|
-
|
|
51
|
-
) => {
|
|
52
|
-
|
|
24
|
+
export const HealthCheckLatencyChart: React.FC<
|
|
25
|
+
HealthCheckLatencyChartProps
|
|
26
|
+
> = ({ context, height = 200, showAverage = true }) => {
|
|
27
|
+
if (context.type === "aggregated") {
|
|
28
|
+
const buckets = context.buckets.filter((b) => b.avgLatencyMs !== undefined);
|
|
29
|
+
|
|
30
|
+
if (buckets.length === 0) {
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className="flex items-center justify-center text-muted-foreground"
|
|
34
|
+
style={{ height }}
|
|
35
|
+
>
|
|
36
|
+
No latency data available
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const chartData = buckets.map((d) => ({
|
|
42
|
+
timestamp: new Date(d.bucketStart).getTime(),
|
|
43
|
+
latencyMs: d.avgLatencyMs!,
|
|
44
|
+
minLatencyMs: d.minLatencyMs,
|
|
45
|
+
maxLatencyMs: d.maxLatencyMs,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
const avgLatency =
|
|
49
|
+
chartData.length > 0
|
|
50
|
+
? chartData.reduce((sum, d) => sum + d.latencyMs, 0) / chartData.length
|
|
51
|
+
: 0;
|
|
52
|
+
|
|
53
|
+
const timeFormat =
|
|
54
|
+
buckets[0]?.bucketSize === "daily" ? "MMM d" : "MMM d HH:mm";
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
return (
|
|
57
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
58
|
+
<AreaChart data={chartData}>
|
|
59
|
+
<defs>
|
|
60
|
+
<linearGradient id="latencyGradient" x1="0" y1="0" x2="0" y2="1">
|
|
61
|
+
<stop
|
|
62
|
+
offset="5%"
|
|
63
|
+
stopColor="hsl(var(--primary))"
|
|
64
|
+
stopOpacity={0.3}
|
|
65
|
+
/>
|
|
66
|
+
<stop
|
|
67
|
+
offset="95%"
|
|
68
|
+
stopColor="hsl(var(--primary))"
|
|
69
|
+
stopOpacity={0}
|
|
70
|
+
/>
|
|
71
|
+
</linearGradient>
|
|
72
|
+
</defs>
|
|
73
|
+
<XAxis
|
|
74
|
+
dataKey="timestamp"
|
|
75
|
+
type="number"
|
|
76
|
+
domain={["auto", "auto"]}
|
|
77
|
+
tickFormatter={(ts: number) => format(new Date(ts), timeFormat)}
|
|
78
|
+
stroke="hsl(var(--muted-foreground))"
|
|
79
|
+
fontSize={12}
|
|
80
|
+
/>
|
|
81
|
+
<YAxis
|
|
82
|
+
stroke="hsl(var(--muted-foreground))"
|
|
83
|
+
fontSize={12}
|
|
84
|
+
tickFormatter={(v: number) => `${v}ms`}
|
|
85
|
+
/>
|
|
86
|
+
<Tooltip<number, "latencyMs">
|
|
87
|
+
content={({ active, payload }) => {
|
|
88
|
+
if (!active || !payload?.length) return;
|
|
89
|
+
const data = payload[0].payload as (typeof chartData)[number];
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
className="rounded-md border bg-popover p-2 text-sm shadow-md"
|
|
93
|
+
style={{
|
|
94
|
+
backgroundColor: "hsl(var(--popover))",
|
|
95
|
+
border: "1px solid hsl(var(--border))",
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<p className="text-muted-foreground">
|
|
99
|
+
{format(new Date(data.timestamp), "MMM d, HH:mm:ss")}
|
|
100
|
+
</p>
|
|
101
|
+
<p className="font-medium">{data.latencyMs}ms</p>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
{showAverage && (
|
|
107
|
+
<ReferenceLine
|
|
108
|
+
y={avgLatency}
|
|
109
|
+
stroke="hsl(var(--muted-foreground))"
|
|
110
|
+
strokeDasharray="3 3"
|
|
111
|
+
label={{
|
|
112
|
+
value: `Avg: ${avgLatency.toFixed(0)}ms`,
|
|
113
|
+
position: "right",
|
|
114
|
+
fill: "hsl(var(--muted-foreground))",
|
|
115
|
+
fontSize: 12,
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
<Area
|
|
120
|
+
type="monotone"
|
|
121
|
+
dataKey="latencyMs"
|
|
122
|
+
stroke="hsl(var(--primary))"
|
|
123
|
+
fill="url(#latencyGradient)"
|
|
124
|
+
strokeWidth={2}
|
|
125
|
+
/>
|
|
126
|
+
</AreaChart>
|
|
127
|
+
</ResponsiveContainer>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Raw data path
|
|
132
|
+
const runs = context.runs.filter((r) => r.latencyMs !== undefined);
|
|
133
|
+
|
|
134
|
+
if (runs.length === 0) {
|
|
55
135
|
return (
|
|
56
136
|
<div
|
|
57
137
|
className="flex items-center justify-center text-muted-foreground"
|
|
@@ -62,34 +142,16 @@ export const HealthCheckLatencyChart: React.FC<HealthCheckLatencyChartProps> = (
|
|
|
62
142
|
);
|
|
63
143
|
}
|
|
64
144
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
? (props.data as AggregatedLatencyDataPoint[]).map((d) => ({
|
|
70
|
-
timestamp: d.bucketStart.getTime(),
|
|
71
|
-
latencyMs: d.avgLatencyMs,
|
|
72
|
-
minLatencyMs: d.minLatencyMs,
|
|
73
|
-
maxLatencyMs: d.maxLatencyMs,
|
|
74
|
-
}))
|
|
75
|
-
: (props.data as LatencyDataPoint[]).toReversed().map((d) => ({
|
|
76
|
-
timestamp: d.timestamp.getTime(),
|
|
77
|
-
latencyMs: d.latencyMs,
|
|
78
|
-
}));
|
|
145
|
+
const chartData = runs.toReversed().map((d) => ({
|
|
146
|
+
timestamp: new Date(d.timestamp).getTime(),
|
|
147
|
+
latencyMs: d.latencyMs!,
|
|
148
|
+
}));
|
|
79
149
|
|
|
80
|
-
// Calculate average latency
|
|
81
150
|
const avgLatency =
|
|
82
151
|
chartData.length > 0
|
|
83
152
|
? chartData.reduce((sum, d) => sum + d.latencyMs, 0) / chartData.length
|
|
84
153
|
: 0;
|
|
85
154
|
|
|
86
|
-
// Format based on bucket size for aggregated data
|
|
87
|
-
const timeFormat = isAggregated
|
|
88
|
-
? (props.data as AggregatedLatencyDataPoint[])[0]?.bucketSize === "daily"
|
|
89
|
-
? "MMM d"
|
|
90
|
-
: "MMM d HH:mm"
|
|
91
|
-
: "HH:mm";
|
|
92
|
-
|
|
93
155
|
return (
|
|
94
156
|
<ResponsiveContainer width="100%" height={height}>
|
|
95
157
|
<AreaChart data={chartData}>
|
|
@@ -111,7 +173,7 @@ export const HealthCheckLatencyChart: React.FC<HealthCheckLatencyChartProps> = (
|
|
|
111
173
|
dataKey="timestamp"
|
|
112
174
|
type="number"
|
|
113
175
|
domain={["auto", "auto"]}
|
|
114
|
-
tickFormatter={(ts: number) => format(new Date(ts),
|
|
176
|
+
tickFormatter={(ts: number) => format(new Date(ts), "HH:mm")}
|
|
115
177
|
stroke="hsl(var(--muted-foreground))"
|
|
116
178
|
fontSize={12}
|
|
117
179
|
/>
|
|
@@ -123,8 +185,6 @@ export const HealthCheckLatencyChart: React.FC<HealthCheckLatencyChartProps> = (
|
|
|
123
185
|
<Tooltip<number, "latencyMs">
|
|
124
186
|
content={({ active, payload }) => {
|
|
125
187
|
if (!active || !payload?.length) return;
|
|
126
|
-
// Note: payload[0].payload is typed as `any` in recharts - this is a recharts limitation.
|
|
127
|
-
// The Payload.payload property holds our data row but recharts can't infer its shape.
|
|
128
188
|
const data = payload[0].payload as (typeof chartData)[number];
|
|
129
189
|
return (
|
|
130
190
|
<div
|
|
@@ -153,12 +153,7 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
|
|
|
153
153
|
colSpan={calculatedColSpan}
|
|
154
154
|
className="bg-muted/30 p-4"
|
|
155
155
|
>
|
|
156
|
-
<
|
|
157
|
-
<h4 className="text-sm font-medium">Result Data</h4>
|
|
158
|
-
<pre className="text-xs bg-card rounded-md p-3 overflow-auto max-h-64 border">
|
|
159
|
-
{JSON.stringify(run.result, undefined, 2)}
|
|
160
|
-
</pre>
|
|
161
|
-
</div>
|
|
156
|
+
<ExpandedResultView result={run.result} />
|
|
162
157
|
</TableCell>
|
|
163
158
|
</TableRow>
|
|
164
159
|
)}
|
|
@@ -185,3 +180,144 @@ export const HealthCheckRunsTable: React.FC<HealthCheckRunsTableProps> = ({
|
|
|
185
180
|
</>
|
|
186
181
|
);
|
|
187
182
|
};
|
|
183
|
+
|
|
184
|
+
// =============================================================================
|
|
185
|
+
// EXPANDED RESULT VIEW
|
|
186
|
+
// =============================================================================
|
|
187
|
+
|
|
188
|
+
interface ExpandedResultViewProps {
|
|
189
|
+
result: Record<string, unknown>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Displays the result data in a structured format.
|
|
194
|
+
* Shows collector results as cards with key-value pairs.
|
|
195
|
+
*/
|
|
196
|
+
function ExpandedResultView({ result }: ExpandedResultViewProps) {
|
|
197
|
+
const metadata = result.metadata as Record<string, unknown> | undefined;
|
|
198
|
+
const rawCollectors = metadata?.collectors;
|
|
199
|
+
|
|
200
|
+
// Type guard for collectors object
|
|
201
|
+
const collectors: Record<string, Record<string, unknown>> | undefined =
|
|
202
|
+
rawCollectors &&
|
|
203
|
+
typeof rawCollectors === "object" &&
|
|
204
|
+
!Array.isArray(rawCollectors)
|
|
205
|
+
? (rawCollectors as Record<string, Record<string, unknown>>)
|
|
206
|
+
: undefined;
|
|
207
|
+
|
|
208
|
+
// Check if we have collectors to display
|
|
209
|
+
const collectorEntries = collectors ? Object.entries(collectors) : [];
|
|
210
|
+
|
|
211
|
+
// Extract connection time as typed value
|
|
212
|
+
const connectionTimeMs = metadata?.connectionTimeMs as number | undefined;
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div className="space-y-4">
|
|
216
|
+
<div className="flex gap-4 text-sm">
|
|
217
|
+
<div>
|
|
218
|
+
<span className="text-muted-foreground">Status: </span>
|
|
219
|
+
<span className="font-medium">{String(result.status)}</span>
|
|
220
|
+
</div>
|
|
221
|
+
<div>
|
|
222
|
+
<span className="text-muted-foreground">Latency: </span>
|
|
223
|
+
<span className="font-medium">{String(result.latencyMs)}ms</span>
|
|
224
|
+
</div>
|
|
225
|
+
{connectionTimeMs !== undefined && (
|
|
226
|
+
<div>
|
|
227
|
+
<span className="text-muted-foreground">Connection: </span>
|
|
228
|
+
<span className="font-medium">{connectionTimeMs}ms</span>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{collectorEntries.length > 0 && (
|
|
234
|
+
<div className="space-y-3">
|
|
235
|
+
<h4 className="text-sm font-medium">Collector Results</h4>
|
|
236
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
237
|
+
{collectorEntries.map(([collectorId, collectorResult]) => (
|
|
238
|
+
<CollectorResultCard
|
|
239
|
+
key={collectorId}
|
|
240
|
+
collectorId={collectorId}
|
|
241
|
+
result={collectorResult}
|
|
242
|
+
/>
|
|
243
|
+
))}
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{result.message ? (
|
|
249
|
+
<div className="text-sm text-muted-foreground">
|
|
250
|
+
{String(result.message)}
|
|
251
|
+
</div>
|
|
252
|
+
) : undefined}
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
interface CollectorResultCardProps {
|
|
258
|
+
collectorId: string;
|
|
259
|
+
result: Record<string, unknown>;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Card displaying a single collector's result values.
|
|
264
|
+
*/
|
|
265
|
+
function CollectorResultCard({
|
|
266
|
+
collectorId,
|
|
267
|
+
result,
|
|
268
|
+
}: CollectorResultCardProps) {
|
|
269
|
+
if (!result || typeof result !== "object") {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Filter out null/undefined values
|
|
274
|
+
const entries = Object.entries(result).filter(
|
|
275
|
+
([, value]) => value !== null && value !== undefined
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div className="rounded-md border bg-card p-3 space-y-2">
|
|
280
|
+
<h5 className="text-sm font-medium text-primary">{collectorId}</h5>
|
|
281
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
|
|
282
|
+
{entries.map(([key, value]) => (
|
|
283
|
+
<div key={key} className="contents">
|
|
284
|
+
<span className="text-muted-foreground truncate">
|
|
285
|
+
{formatKey(key)}
|
|
286
|
+
</span>
|
|
287
|
+
<span className="font-mono text-xs truncate" title={String(value)}>
|
|
288
|
+
{formatValue(value)}
|
|
289
|
+
</span>
|
|
290
|
+
</div>
|
|
291
|
+
))}
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Format a camelCase key to a readable label.
|
|
299
|
+
*/
|
|
300
|
+
function formatKey(key: string): string {
|
|
301
|
+
return key
|
|
302
|
+
.replaceAll(/([a-z])([A-Z])/g, "$1 $2")
|
|
303
|
+
.replaceAll(/^./, (c) => c.toUpperCase());
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Format a value for display.
|
|
308
|
+
*/
|
|
309
|
+
function formatValue(value: unknown): string {
|
|
310
|
+
if (value === null || value === undefined) return "—";
|
|
311
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
312
|
+
if (typeof value === "number") {
|
|
313
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
314
|
+
}
|
|
315
|
+
if (Array.isArray(value)) {
|
|
316
|
+
return value.length > 3
|
|
317
|
+
? `[${value.slice(0, 3).join(", ")}…]`
|
|
318
|
+
: `[${value.join(", ")}]`;
|
|
319
|
+
}
|
|
320
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
321
|
+
const str = String(value);
|
|
322
|
+
return str.length > 50 ? `${str.slice(0, 47)}…` : str;
|
|
323
|
+
}
|
|
@@ -7,36 +7,12 @@ import {
|
|
|
7
7
|
Cell,
|
|
8
8
|
} from "recharts";
|
|
9
9
|
import { format } from "date-fns";
|
|
10
|
+
import type { HealthCheckDiagramSlotContext } from "../slots";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
status: "healthy" | "degraded" | "unhealthy";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface AggregatedStatusDataPoint {
|
|
17
|
-
bucketStart: Date;
|
|
18
|
-
healthyCount: number;
|
|
19
|
-
degradedCount: number;
|
|
20
|
-
unhealthyCount: number;
|
|
21
|
-
runCount: number;
|
|
22
|
-
bucketSize: "hourly" | "daily";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type RawStatusTimelineProps = {
|
|
26
|
-
type: "raw";
|
|
27
|
-
data: StatusDataPoint[];
|
|
12
|
+
interface HealthCheckStatusTimelineProps {
|
|
13
|
+
context: HealthCheckDiagramSlotContext;
|
|
28
14
|
height?: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type AggregatedStatusTimelineProps = {
|
|
32
|
-
type: "aggregated";
|
|
33
|
-
data: AggregatedStatusDataPoint[];
|
|
34
|
-
height?: number;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
type HealthCheckStatusTimelineProps =
|
|
38
|
-
| RawStatusTimelineProps
|
|
39
|
-
| AggregatedStatusTimelineProps;
|
|
15
|
+
}
|
|
40
16
|
|
|
41
17
|
const statusColors = {
|
|
42
18
|
healthy: "hsl(var(--success))",
|
|
@@ -51,28 +27,23 @@ const statusColors = {
|
|
|
51
27
|
*/
|
|
52
28
|
export const HealthCheckStatusTimeline: React.FC<
|
|
53
29
|
HealthCheckStatusTimelineProps
|
|
54
|
-
> = (
|
|
55
|
-
|
|
30
|
+
> = ({ context, height = 60 }) => {
|
|
31
|
+
if (context.type === "aggregated") {
|
|
32
|
+
const buckets = context.buckets;
|
|
56
33
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const isAggregated = props.type === "aggregated";
|
|
34
|
+
if (buckets.length === 0) {
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className="flex items-center justify-center text-muted-foreground"
|
|
38
|
+
style={{ height }}
|
|
39
|
+
>
|
|
40
|
+
No status data available
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
69
44
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (isAggregated) {
|
|
73
|
-
const aggData = props.data as AggregatedStatusDataPoint[];
|
|
74
|
-
const chartData = aggData.map((d) => ({
|
|
75
|
-
timestamp: d.bucketStart.getTime(),
|
|
45
|
+
const chartData = buckets.map((d) => ({
|
|
46
|
+
timestamp: new Date(d.bucketStart).getTime(),
|
|
76
47
|
healthy: d.healthyCount,
|
|
77
48
|
degraded: d.degradedCount,
|
|
78
49
|
unhealthy: d.unhealthyCount,
|
|
@@ -80,7 +51,7 @@ export const HealthCheckStatusTimeline: React.FC<
|
|
|
80
51
|
}));
|
|
81
52
|
|
|
82
53
|
const timeFormat =
|
|
83
|
-
|
|
54
|
+
buckets[0]?.bucketSize === "daily" ? "MMM d" : "MMM d HH:mm";
|
|
84
55
|
|
|
85
56
|
return (
|
|
86
57
|
<ResponsiveContainer width="100%" height={height}>
|
|
@@ -137,9 +108,21 @@ export const HealthCheckStatusTimeline: React.FC<
|
|
|
137
108
|
}
|
|
138
109
|
|
|
139
110
|
// Raw data path
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
111
|
+
const runs = context.runs;
|
|
112
|
+
|
|
113
|
+
if (runs.length === 0) {
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
className="flex items-center justify-center text-muted-foreground"
|
|
117
|
+
style={{ height }}
|
|
118
|
+
>
|
|
119
|
+
No status data available
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const chartData = runs.toReversed().map((d) => ({
|
|
125
|
+
timestamp: new Date(d.timestamp).getTime(),
|
|
143
126
|
value: 1, // Fixed height for visibility
|
|
144
127
|
status: d.status,
|
|
145
128
|
}));
|