@checkstack/healthcheck-frontend 0.8.1 → 0.9.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 +45 -0
- package/package.json +11 -11
- package/src/auto-charts/AutoChartGrid.tsx +94 -208
- package/src/auto-charts/useStrategySchemas.ts +5 -17
- package/src/components/HealthCheckEditor.tsx +8 -0
- package/src/components/HealthCheckLatencyChart.tsx +20 -128
- package/src/components/HealthCheckStatusTimeline.tsx +60 -150
- package/src/components/HealthCheckSystemOverview.tsx +3 -10
- package/src/hooks/useHealthCheckData.ts +22 -138
- package/src/index.tsx +0 -4
- package/src/slots.tsx +24 -113
|
@@ -18,133 +18,15 @@ interface HealthCheckLatencyChartProps {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Area chart showing health check latency over time.
|
|
21
|
-
*
|
|
21
|
+
* Uses aggregated bucket data with average latency per bucket.
|
|
22
22
|
* Uses HSL CSS variables for theming consistency.
|
|
23
23
|
*/
|
|
24
24
|
export const HealthCheckLatencyChart: React.FC<
|
|
25
25
|
HealthCheckLatencyChartProps
|
|
26
26
|
> = ({ context, height = 200, showAverage = true }) => {
|
|
27
|
-
|
|
28
|
-
const buckets = context.buckets.filter((b) => b.avgLatencyMs !== undefined);
|
|
27
|
+
const buckets = context.buckets.filter((b) => b.avgLatencyMs !== undefined);
|
|
29
28
|
|
|
30
|
-
|
|
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
|
-
bucketEndTimestamp: new Date(d.bucketEnd).getTime(),
|
|
44
|
-
latencyMs: d.avgLatencyMs!,
|
|
45
|
-
minLatencyMs: d.minLatencyMs,
|
|
46
|
-
maxLatencyMs: d.maxLatencyMs,
|
|
47
|
-
}));
|
|
48
|
-
|
|
49
|
-
const avgLatency =
|
|
50
|
-
chartData.length > 0
|
|
51
|
-
? chartData.reduce((sum, d) => sum + d.latencyMs, 0) / chartData.length
|
|
52
|
-
: 0;
|
|
53
|
-
|
|
54
|
-
// Use daily format for intervals >= 6 hours, otherwise include time
|
|
55
|
-
const timeFormat =
|
|
56
|
-
(buckets[0]?.bucketIntervalSeconds ?? 3600) >= 21_600
|
|
57
|
-
? "MMM d"
|
|
58
|
-
: "MMM d, HH:mm";
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<ResponsiveContainer width="100%" height={height}>
|
|
62
|
-
<AreaChart data={chartData}>
|
|
63
|
-
<defs>
|
|
64
|
-
<linearGradient id="latencyGradient" x1="0" y1="0" x2="0" y2="1">
|
|
65
|
-
<stop
|
|
66
|
-
offset="5%"
|
|
67
|
-
stopColor="hsl(var(--primary))"
|
|
68
|
-
stopOpacity={0.3}
|
|
69
|
-
/>
|
|
70
|
-
<stop
|
|
71
|
-
offset="95%"
|
|
72
|
-
stopColor="hsl(var(--primary))"
|
|
73
|
-
stopOpacity={0}
|
|
74
|
-
/>
|
|
75
|
-
</linearGradient>
|
|
76
|
-
</defs>
|
|
77
|
-
<XAxis
|
|
78
|
-
dataKey="timestamp"
|
|
79
|
-
type="number"
|
|
80
|
-
domain={["dataMin", "dataMax"]}
|
|
81
|
-
tickFormatter={(ts: number) => format(new Date(ts), timeFormat)}
|
|
82
|
-
stroke="hsl(var(--muted-foreground))"
|
|
83
|
-
fontSize={12}
|
|
84
|
-
/>
|
|
85
|
-
<YAxis
|
|
86
|
-
stroke="hsl(var(--muted-foreground))"
|
|
87
|
-
fontSize={12}
|
|
88
|
-
tickFormatter={(v: number) => `${v}ms`}
|
|
89
|
-
/>
|
|
90
|
-
<Tooltip<number, "latencyMs">
|
|
91
|
-
content={({ active, payload }) => {
|
|
92
|
-
// eslint-disable-next-line unicorn/no-null -- recharts requires null return, not undefined
|
|
93
|
-
if (!active || !payload?.length) return null;
|
|
94
|
-
const data = payload[0].payload as (typeof chartData)[number];
|
|
95
|
-
const startTime = format(
|
|
96
|
-
new Date(data.timestamp),
|
|
97
|
-
"MMM d, HH:mm",
|
|
98
|
-
);
|
|
99
|
-
const endTime = format(
|
|
100
|
-
new Date(data.bucketEndTimestamp),
|
|
101
|
-
"HH:mm",
|
|
102
|
-
);
|
|
103
|
-
return (
|
|
104
|
-
<div
|
|
105
|
-
className="rounded-md border bg-popover p-2 text-sm shadow-md"
|
|
106
|
-
style={{
|
|
107
|
-
backgroundColor: "hsl(var(--popover))",
|
|
108
|
-
border: "1px solid hsl(var(--border))",
|
|
109
|
-
}}
|
|
110
|
-
>
|
|
111
|
-
<p className="text-muted-foreground">
|
|
112
|
-
{startTime} - {endTime}
|
|
113
|
-
</p>
|
|
114
|
-
<p className="font-medium">{data.latencyMs}ms (avg)</p>
|
|
115
|
-
</div>
|
|
116
|
-
);
|
|
117
|
-
}}
|
|
118
|
-
/>
|
|
119
|
-
{showAverage && (
|
|
120
|
-
<ReferenceLine
|
|
121
|
-
y={avgLatency}
|
|
122
|
-
stroke="hsl(var(--muted-foreground))"
|
|
123
|
-
strokeDasharray="3 3"
|
|
124
|
-
label={{
|
|
125
|
-
value: `Avg: ${avgLatency.toFixed(0)}ms`,
|
|
126
|
-
position: "right",
|
|
127
|
-
fill: "hsl(var(--muted-foreground))",
|
|
128
|
-
fontSize: 12,
|
|
129
|
-
}}
|
|
130
|
-
/>
|
|
131
|
-
)}
|
|
132
|
-
<Area
|
|
133
|
-
type="monotone"
|
|
134
|
-
dataKey="latencyMs"
|
|
135
|
-
stroke="hsl(var(--primary))"
|
|
136
|
-
fill="url(#latencyGradient)"
|
|
137
|
-
strokeWidth={2}
|
|
138
|
-
/>
|
|
139
|
-
</AreaChart>
|
|
140
|
-
</ResponsiveContainer>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Raw data path
|
|
145
|
-
const runs = context.runs.filter((r) => r.latencyMs !== undefined);
|
|
146
|
-
|
|
147
|
-
if (runs.length === 0) {
|
|
29
|
+
if (buckets.length === 0) {
|
|
148
30
|
return (
|
|
149
31
|
<div
|
|
150
32
|
className="flex items-center justify-center text-muted-foreground"
|
|
@@ -155,10 +37,12 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
155
37
|
);
|
|
156
38
|
}
|
|
157
39
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
latencyMs: d.
|
|
40
|
+
const chartData = buckets.map((d) => ({
|
|
41
|
+
timestamp: new Date(d.bucketStart).getTime(),
|
|
42
|
+
bucketEndTimestamp: new Date(d.bucketEnd).getTime(),
|
|
43
|
+
latencyMs: d.avgLatencyMs!,
|
|
44
|
+
minLatencyMs: d.minLatencyMs,
|
|
45
|
+
maxLatencyMs: d.maxLatencyMs,
|
|
162
46
|
}));
|
|
163
47
|
|
|
164
48
|
const avgLatency =
|
|
@@ -166,6 +50,12 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
166
50
|
? chartData.reduce((sum, d) => sum + d.latencyMs, 0) / chartData.length
|
|
167
51
|
: 0;
|
|
168
52
|
|
|
53
|
+
// Use daily format for intervals >= 6 hours, otherwise include time
|
|
54
|
+
const timeFormat =
|
|
55
|
+
(buckets[0]?.bucketIntervalSeconds ?? 3600) >= 21_600
|
|
56
|
+
? "MMM d"
|
|
57
|
+
: "MMM d, HH:mm";
|
|
58
|
+
|
|
169
59
|
return (
|
|
170
60
|
<ResponsiveContainer width="100%" height={height}>
|
|
171
61
|
<AreaChart data={chartData}>
|
|
@@ -187,7 +77,7 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
187
77
|
dataKey="timestamp"
|
|
188
78
|
type="number"
|
|
189
79
|
domain={["dataMin", "dataMax"]}
|
|
190
|
-
tickFormatter={(ts: number) => format(new Date(ts),
|
|
80
|
+
tickFormatter={(ts: number) => format(new Date(ts), timeFormat)}
|
|
191
81
|
stroke="hsl(var(--muted-foreground))"
|
|
192
82
|
fontSize={12}
|
|
193
83
|
/>
|
|
@@ -201,6 +91,8 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
201
91
|
// eslint-disable-next-line unicorn/no-null -- recharts requires null return, not undefined
|
|
202
92
|
if (!active || !payload?.length) return null;
|
|
203
93
|
const data = payload[0].payload as (typeof chartData)[number];
|
|
94
|
+
const startTime = format(new Date(data.timestamp), "MMM d, HH:mm");
|
|
95
|
+
const endTime = format(new Date(data.bucketEndTimestamp), "HH:mm");
|
|
204
96
|
return (
|
|
205
97
|
<div
|
|
206
98
|
className="rounded-md border bg-popover p-2 text-sm shadow-md"
|
|
@@ -210,9 +102,9 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
210
102
|
}}
|
|
211
103
|
>
|
|
212
104
|
<p className="text-muted-foreground">
|
|
213
|
-
{
|
|
105
|
+
{startTime} - {endTime}
|
|
214
106
|
</p>
|
|
215
|
-
<p className="font-medium">{data.latencyMs}ms</p>
|
|
107
|
+
<p className="font-medium">{data.latencyMs}ms (avg)</p>
|
|
216
108
|
</div>
|
|
217
109
|
);
|
|
218
110
|
}}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { format } from "date-fns";
|
|
2
2
|
import type { HealthCheckDiagramSlotContext } from "../slots";
|
|
3
3
|
import { SparklineTooltip } from "./SparklineTooltip";
|
|
4
|
-
import { downsampleSparkline } from "../utils/sparkline-downsampling";
|
|
5
4
|
|
|
6
5
|
interface HealthCheckStatusTimelineProps {
|
|
7
6
|
context: HealthCheckDiagramSlotContext;
|
|
@@ -15,104 +14,15 @@ const statusColors = {
|
|
|
15
14
|
};
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
|
-
* Timeline bar chart showing health check status
|
|
19
|
-
*
|
|
20
|
-
* For aggregated data: each bar shows the distribution of statuses in that bucket.
|
|
17
|
+
* Timeline bar chart showing health check status distribution over time.
|
|
18
|
+
* Each bar shows the distribution of statuses in that aggregated bucket.
|
|
21
19
|
*/
|
|
22
20
|
export const HealthCheckStatusTimeline: React.FC<
|
|
23
21
|
HealthCheckStatusTimelineProps
|
|
24
22
|
> = ({ context, height = 60 }) => {
|
|
25
|
-
|
|
26
|
-
const buckets = context.buckets;
|
|
23
|
+
const buckets = context.buckets;
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div
|
|
31
|
-
className="flex items-center justify-center text-muted-foreground"
|
|
32
|
-
style={{ height }}
|
|
33
|
-
>
|
|
34
|
-
No status data available
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Use daily format for intervals >= 6 hours, otherwise include time
|
|
40
|
-
const timeFormat =
|
|
41
|
-
(buckets[0]?.bucketIntervalSeconds ?? 3600) >= 21_600
|
|
42
|
-
? "MMM d"
|
|
43
|
-
: "MMM d HH:mm";
|
|
44
|
-
|
|
45
|
-
// Calculate time range for labels
|
|
46
|
-
const firstTime = new Date(buckets[0].bucketStart).getTime();
|
|
47
|
-
const lastTime = new Date(buckets.at(-1)!.bucketStart).getTime();
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<div style={{ height }} className="flex flex-col justify-between">
|
|
51
|
-
{/* Status strip - equal width stacked segments for each bucket */}
|
|
52
|
-
<div className="flex h-4 gap-px rounded-md bg-muted/30">
|
|
53
|
-
{buckets.map((bucket, index) => {
|
|
54
|
-
const total = bucket.runCount || 1;
|
|
55
|
-
const healthyPct = (bucket.healthyCount / total) * 100;
|
|
56
|
-
const degradedPct = (bucket.degradedCount / total) * 100;
|
|
57
|
-
const unhealthyPct = (bucket.unhealthyCount / total) * 100;
|
|
58
|
-
|
|
59
|
-
// Use actual bucket end time from response (critical for last bucket which extends to query end)
|
|
60
|
-
const bucketStart = new Date(bucket.bucketStart);
|
|
61
|
-
const bucketEnd = new Date(bucket.bucketEnd);
|
|
62
|
-
const timeSpan = `${format(bucketStart, "MMM d, HH:mm")} - ${format(bucketEnd, "HH:mm")}`;
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<SparklineTooltip
|
|
66
|
-
key={index}
|
|
67
|
-
content={`${timeSpan}\nHealthy: ${bucket.healthyCount}\nDegraded: ${bucket.degradedCount}\nUnhealthy: ${bucket.unhealthyCount}`}
|
|
68
|
-
>
|
|
69
|
-
<div className="flex-1 h-full flex flex-col cursor-pointer group">
|
|
70
|
-
{bucket.healthyCount > 0 && (
|
|
71
|
-
<div
|
|
72
|
-
className="w-full transition-opacity group-hover:opacity-80"
|
|
73
|
-
style={{
|
|
74
|
-
height: `${healthyPct}%`,
|
|
75
|
-
backgroundColor: statusColors.healthy,
|
|
76
|
-
}}
|
|
77
|
-
/>
|
|
78
|
-
)}
|
|
79
|
-
{bucket.degradedCount > 0 && (
|
|
80
|
-
<div
|
|
81
|
-
className="w-full transition-opacity group-hover:opacity-80"
|
|
82
|
-
style={{
|
|
83
|
-
height: `${degradedPct}%`,
|
|
84
|
-
backgroundColor: statusColors.degraded,
|
|
85
|
-
}}
|
|
86
|
-
/>
|
|
87
|
-
)}
|
|
88
|
-
{bucket.unhealthyCount > 0 && (
|
|
89
|
-
<div
|
|
90
|
-
className="w-full transition-opacity group-hover:opacity-80"
|
|
91
|
-
style={{
|
|
92
|
-
height: `${unhealthyPct}%`,
|
|
93
|
-
backgroundColor: statusColors.unhealthy,
|
|
94
|
-
}}
|
|
95
|
-
/>
|
|
96
|
-
)}
|
|
97
|
-
</div>
|
|
98
|
-
</SparklineTooltip>
|
|
99
|
-
);
|
|
100
|
-
})}
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
{/* Time axis labels */}
|
|
104
|
-
<div className="flex justify-between text-xs text-muted-foreground mt-1">
|
|
105
|
-
<span>{format(new Date(firstTime), timeFormat)}</span>
|
|
106
|
-
<span>{format(new Date(lastTime), timeFormat)}</span>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Raw data path - use a continuous strip visualization
|
|
113
|
-
const runs = context.runs;
|
|
114
|
-
|
|
115
|
-
if (runs.length === 0) {
|
|
25
|
+
if (buckets.length === 0) {
|
|
116
26
|
return (
|
|
117
27
|
<div
|
|
118
28
|
className="flex items-center justify-center text-muted-foreground"
|
|
@@ -123,74 +33,74 @@ export const HealthCheckStatusTimeline: React.FC<
|
|
|
123
33
|
);
|
|
124
34
|
}
|
|
125
35
|
|
|
126
|
-
//
|
|
127
|
-
|
|
36
|
+
// Use daily format for intervals >= 6 hours, otherwise include time
|
|
37
|
+
const timeFormat =
|
|
38
|
+
(buckets[0]?.bucketIntervalSeconds ?? 3600) >= 21_600
|
|
39
|
+
? "MMM d"
|
|
40
|
+
: "MMM d HH:mm";
|
|
128
41
|
|
|
129
42
|
// Calculate time range for labels
|
|
130
|
-
const firstTime = new Date(
|
|
131
|
-
const lastTime = new Date(
|
|
132
|
-
const totalRange = lastTime - firstTime;
|
|
43
|
+
const firstTime = new Date(buckets[0].bucketStart).getTime();
|
|
44
|
+
const lastTime = new Date(buckets.at(-1)!.bucketStart).getTime();
|
|
133
45
|
|
|
134
46
|
return (
|
|
135
47
|
<div style={{ height }} className="flex flex-col justify-between">
|
|
136
|
-
{/* Status strip - equal width segments for
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
48
|
+
{/* Status strip - equal width stacked segments for each bucket */}
|
|
49
|
+
<div className="flex h-4 gap-px rounded-md bg-muted/30">
|
|
50
|
+
{buckets.map((bucket, index) => {
|
|
51
|
+
const total = bucket.runCount || 1;
|
|
52
|
+
const healthyPct = (bucket.healthyCount / total) * 100;
|
|
53
|
+
const degradedPct = (bucket.degradedCount / total) * 100;
|
|
54
|
+
const unhealthyPct = (bucket.unhealthyCount / total) * 100;
|
|
55
|
+
|
|
56
|
+
// Use actual bucket end time from response (critical for last bucket which extends to query end)
|
|
57
|
+
const bucketStart = new Date(bucket.bucketStart);
|
|
58
|
+
const bucketEnd = new Date(bucket.bucketEnd);
|
|
59
|
+
const timeSpan = `${format(bucketStart, "MMM d, HH:mm")} - ${format(bucketEnd, "HH:mm")}`;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<SparklineTooltip
|
|
63
|
+
key={index}
|
|
64
|
+
content={`${timeSpan}\nHealthy: ${bucket.healthyCount}\nDegraded: ${bucket.degradedCount}\nUnhealthy: ${bucket.unhealthyCount}`}
|
|
65
|
+
>
|
|
66
|
+
<div className="flex-1 h-full flex flex-col cursor-pointer group">
|
|
67
|
+
{bucket.healthyCount > 0 && (
|
|
68
|
+
<div
|
|
69
|
+
className="w-full transition-opacity group-hover:opacity-80"
|
|
70
|
+
style={{
|
|
71
|
+
height: `${healthyPct}%`,
|
|
72
|
+
backgroundColor: statusColors.healthy,
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
{bucket.degradedCount > 0 && (
|
|
77
|
+
<div
|
|
78
|
+
className="w-full transition-opacity group-hover:opacity-80"
|
|
79
|
+
style={{
|
|
80
|
+
height: `${degradedPct}%`,
|
|
81
|
+
backgroundColor: statusColors.degraded,
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
{bucket.unhealthyCount > 0 && (
|
|
174
86
|
<div
|
|
175
|
-
className="
|
|
87
|
+
className="w-full transition-opacity group-hover:opacity-80"
|
|
176
88
|
style={{
|
|
177
|
-
|
|
89
|
+
height: `${unhealthyPct}%`,
|
|
90
|
+
backgroundColor: statusColors.unhealthy,
|
|
178
91
|
}}
|
|
179
92
|
/>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</SparklineTooltip>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</div>
|
|
186
99
|
|
|
187
100
|
{/* Time axis labels */}
|
|
188
101
|
<div className="flex justify-between text-xs text-muted-foreground mt-1">
|
|
189
|
-
<span>{format(new Date(firstTime),
|
|
190
|
-
{
|
|
191
|
-
<span>{format(new Date(firstTime + totalRange / 2), "HH:mm")}</span>
|
|
192
|
-
)}
|
|
193
|
-
<span>{format(new Date(lastTime), "HH:mm")}</span>
|
|
102
|
+
<span>{format(new Date(firstTime), timeFormat)}</span>
|
|
103
|
+
<span>{format(new Date(lastTime), timeFormat)}</span>
|
|
194
104
|
</div>
|
|
195
105
|
</div>
|
|
196
106
|
);
|
|
@@ -155,7 +155,6 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
155
155
|
context: chartContext,
|
|
156
156
|
loading: chartLoading,
|
|
157
157
|
isFetching: chartFetching,
|
|
158
|
-
isAggregated,
|
|
159
158
|
bucketIntervalSeconds,
|
|
160
159
|
} = useHealthCheckData({
|
|
161
160
|
systemId,
|
|
@@ -163,7 +162,6 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
163
162
|
strategyId: item.strategyId,
|
|
164
163
|
dateRange,
|
|
165
164
|
isRollingPreset,
|
|
166
|
-
limit: 1000,
|
|
167
165
|
// Update endDate to current time when new runs are detected (only for rolling presets)
|
|
168
166
|
onDateRangeRefresh: (newEndDate) => {
|
|
169
167
|
setDateRange((prev) => ({ ...prev, endDate: newEndDate }));
|
|
@@ -233,10 +231,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
233
231
|
}
|
|
234
232
|
|
|
235
233
|
// Check if we have data to show
|
|
236
|
-
const hasData =
|
|
237
|
-
chartContext.type === "raw"
|
|
238
|
-
? chartContext.runs.length > 0
|
|
239
|
-
: chartContext.buckets.length > 0;
|
|
234
|
+
const hasData = chartContext.buckets.length > 0;
|
|
240
235
|
|
|
241
236
|
if (!hasData) {
|
|
242
237
|
return;
|
|
@@ -244,7 +239,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
244
239
|
|
|
245
240
|
return (
|
|
246
241
|
<div className="space-y-4">
|
|
247
|
-
{
|
|
242
|
+
{bucketIntervalSeconds && (
|
|
248
243
|
<AggregatedDataBanner
|
|
249
244
|
bucketIntervalSeconds={bucketIntervalSeconds}
|
|
250
245
|
checkIntervalSeconds={item.intervalSeconds}
|
|
@@ -265,9 +260,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
265
260
|
<Card>
|
|
266
261
|
<CardHeader className="pb-2">
|
|
267
262
|
<CardTitle className="text-sm font-medium">
|
|
268
|
-
|
|
269
|
-
? "Average Execution Duration"
|
|
270
|
-
: "Execution Duration"}
|
|
263
|
+
Average Execution Duration
|
|
271
264
|
</CardTitle>
|
|
272
265
|
</CardHeader>
|
|
273
266
|
<CardContent>
|