@checkstack/healthcheck-frontend 0.4.10 → 0.6.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 +123 -0
- package/package.json +1 -1
- package/src/auto-charts/useStrategySchemas.ts +18 -6
- package/src/components/AggregatedDataBanner.tsx +51 -8
- package/src/components/CollectorList.tsx +63 -56
- package/src/components/HealthCheckDiagram.tsx +9 -12
- package/src/components/HealthCheckLatencyChart.tsx +6 -3
- package/src/components/HealthCheckList.tsx +53 -11
- package/src/components/HealthCheckStatusTimeline.tsx +6 -3
- package/src/components/HealthCheckSystemOverview.tsx +27 -32
- package/src/hooks/useHealthCheckData.ts +10 -7
- package/src/pages/HealthCheckConfigPage.tsx +27 -5
- package/src/pages/HealthCheckHistoryDetailPage.tsx +5 -3
- package/src/pages/HealthCheckHistoryPage.tsx +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,128 @@
|
|
|
1
1
|
# @checkstack/healthcheck-frontend
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 11d2679: Add ability to pause health check configurations globally. When paused, health checks continue to be scheduled but execution is skipped for all systems using that configuration. Users with manage access can pause/resume from the Health Checks config page.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 223081d: Add icon support to PageLayout and improve mobile responsiveness
|
|
12
|
+
|
|
13
|
+
**PageLayout Icons:**
|
|
14
|
+
|
|
15
|
+
- Added required `icon` prop to `PageLayout` and `PageHeader` components that accepts a Lucide icon component reference
|
|
16
|
+
- Icons are rendered with consistent `h-6 w-6 text-primary` styling
|
|
17
|
+
- Updated all page components to include appropriate icons in their headers
|
|
18
|
+
|
|
19
|
+
**Mobile Layout Improvements:**
|
|
20
|
+
|
|
21
|
+
- Standardized responsive padding in main app shell (`p-3` on mobile, `p-6` on desktop)
|
|
22
|
+
- Added `CardHeaderRow` component for mobile-safe card headers with proper wrapping
|
|
23
|
+
- Improved `DateRangeFilter` responsive behavior with vertical stacking on mobile
|
|
24
|
+
- Migrated pages to use `PageLayout` for consistent responsive behavior
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [11d2679]
|
|
27
|
+
- Updated dependencies [223081d]
|
|
28
|
+
- @checkstack/healthcheck-common@0.6.0
|
|
29
|
+
- @checkstack/ui@0.5.0
|
|
30
|
+
- @checkstack/auth-frontend@0.5.5
|
|
31
|
+
- @checkstack/dashboard-frontend@0.3.10
|
|
32
|
+
|
|
33
|
+
## 0.5.0
|
|
34
|
+
|
|
35
|
+
### Minor Changes
|
|
36
|
+
|
|
37
|
+
- ac3a4cf: ### Dynamic Bucket Sizing for Health Check Visualization
|
|
38
|
+
|
|
39
|
+
Implements industry-standard dynamic bucket sizing for health check data aggregation, following patterns from Grafana/VictoriaMetrics.
|
|
40
|
+
|
|
41
|
+
**What changed:**
|
|
42
|
+
|
|
43
|
+
- Replaced fixed `bucketSize: "hourly" | "daily" | "auto"` with dynamic `targetPoints` parameter (default: 500)
|
|
44
|
+
- Bucket interval is now calculated as `(endDate - startDate) / targetPoints` with a minimum of 1 second
|
|
45
|
+
- Added `bucketIntervalSeconds` to aggregated response and individual buckets
|
|
46
|
+
- Updated chart components to use dynamic time formatting based on bucket interval
|
|
47
|
+
|
|
48
|
+
**Why:**
|
|
49
|
+
|
|
50
|
+
- A 24-hour view with 1-second health checks previously returned 86,400+ data points, causing lag
|
|
51
|
+
- Now returns ~500 data points regardless of timeframe, ensuring consistent chart performance
|
|
52
|
+
- Charts still preserve visual fidelity through proper aggregation
|
|
53
|
+
|
|
54
|
+
**Breaking Change:**
|
|
55
|
+
|
|
56
|
+
- `bucketSize` parameter removed from `getAggregatedHistory` and `getDetailedAggregatedHistory` endpoints
|
|
57
|
+
- Use `targetPoints` instead (defaults to 500 if not specified)
|
|
58
|
+
|
|
59
|
+
***
|
|
60
|
+
|
|
61
|
+
### Collector Aggregated Charts Fix
|
|
62
|
+
|
|
63
|
+
Fixed issue where collector auto-charts (like HTTP request response time charts) were not showing in aggregated data mode.
|
|
64
|
+
|
|
65
|
+
**What changed:**
|
|
66
|
+
|
|
67
|
+
- Added `aggregatedResultSchema` to `CollectorDtoSchema`
|
|
68
|
+
- Backend now returns collector aggregated schemas via `getCollectors` endpoint
|
|
69
|
+
- Frontend `useStrategySchemas` hook now merges collector aggregated schemas
|
|
70
|
+
- Service now calls each collector's `aggregateResult()` when building buckets
|
|
71
|
+
- Aggregated collector data stored in `aggregatedResult.collectors[uuid]`
|
|
72
|
+
|
|
73
|
+
**Why:**
|
|
74
|
+
|
|
75
|
+
- Previously only strategy-level aggregated results were computed
|
|
76
|
+
- Collectors like HTTP Request Collector have their own `aggregateResult` method
|
|
77
|
+
- Without calling these, fields like `avgResponseTimeMs` and `successRate` were missing from aggregated buckets
|
|
78
|
+
|
|
79
|
+
### Patch Changes
|
|
80
|
+
|
|
81
|
+
- 095cf4e: ### Cross-Tier Data Aggregation
|
|
82
|
+
|
|
83
|
+
Implements intelligent cross-tier querying for health check history, enabling seamless data retrieval across raw, hourly, and daily storage tiers.
|
|
84
|
+
|
|
85
|
+
**What changed:**
|
|
86
|
+
|
|
87
|
+
- `getAggregatedHistory` now queries all three tiers (raw, hourly, daily) in parallel
|
|
88
|
+
- Added `NormalizedBucket` type for unified bucket format across tiers
|
|
89
|
+
- Added `mergeTieredBuckets()` to merge data with priority (raw > hourly > daily)
|
|
90
|
+
- Added `combineBuckets()` and `reaggregateBuckets()` for re-aggregation to target bucket size
|
|
91
|
+
- Raw data preserves full granularity when available (uses target bucket interval)
|
|
92
|
+
|
|
93
|
+
**Why:**
|
|
94
|
+
|
|
95
|
+
- Previously, the API only queried raw runs, which are retained for a limited period (default 7 days)
|
|
96
|
+
- For longer time ranges, data was missing because hourly/daily aggregates weren't queried
|
|
97
|
+
- The retention job only runs periodically, so we can't assume tier boundaries based on config
|
|
98
|
+
- Querying all tiers ensures no gaps in data coverage
|
|
99
|
+
|
|
100
|
+
**Technical details:**
|
|
101
|
+
|
|
102
|
+
- Additive metrics (counts, latencySum) are summed correctly for accurate averages
|
|
103
|
+
- p95 latency uses max of source p95s as conservative upper-bound approximation
|
|
104
|
+
- `aggregatedResult` (strategy-specific) is preserved for raw-only buckets
|
|
105
|
+
|
|
106
|
+
- 538e45d: Fixed 24-hour date range not returning correct data and improved chart display
|
|
107
|
+
|
|
108
|
+
- Fixed missing `endDate` parameter in raw data queries causing data to extend beyond selected time range
|
|
109
|
+
- Fixed incorrect 24-hour date calculation using `setHours()` - now uses `date-fns` `subHours()` for correct date math
|
|
110
|
+
- Refactored `DateRangePreset` from string union to enum for improved type safety and IDE support
|
|
111
|
+
- Exported `getPresetRange` function for reuse across components
|
|
112
|
+
- Changed chart x-axis domain from `["auto", "auto"]` to `["dataMin", "dataMax"]` to remove padding gaps
|
|
113
|
+
|
|
114
|
+
- Updated dependencies [ac3a4cf]
|
|
115
|
+
- Updated dependencies [db1f56f]
|
|
116
|
+
- Updated dependencies [538e45d]
|
|
117
|
+
- @checkstack/healthcheck-common@0.5.0
|
|
118
|
+
- @checkstack/common@0.6.0
|
|
119
|
+
- @checkstack/ui@0.4.1
|
|
120
|
+
- @checkstack/dashboard-frontend@0.3.9
|
|
121
|
+
- @checkstack/auth-frontend@0.5.4
|
|
122
|
+
- @checkstack/catalog-common@1.2.4
|
|
123
|
+
- @checkstack/frontend-api@0.3.3
|
|
124
|
+
- @checkstack/signal-frontend@0.0.10
|
|
125
|
+
|
|
3
126
|
## 0.4.10
|
|
4
127
|
|
|
5
128
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -38,7 +38,7 @@ export function useStrategySchemas(strategyId: string): {
|
|
|
38
38
|
// Fetch collectors with useQuery
|
|
39
39
|
const { data: collectors } = healthCheckClient.getCollectors.useQuery(
|
|
40
40
|
{ strategyId },
|
|
41
|
-
{ enabled: !!strategyId }
|
|
41
|
+
{ enabled: !!strategyId },
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
useEffect(() => {
|
|
@@ -51,22 +51,34 @@ export function useStrategySchemas(strategyId: string): {
|
|
|
51
51
|
if (strategy) {
|
|
52
52
|
// Build collector schemas object for nesting under resultSchema.properties.collectors
|
|
53
53
|
const collectorProperties: Record<string, unknown> = {};
|
|
54
|
+
const collectorAggregatedProperties: Record<string, unknown> = {};
|
|
55
|
+
|
|
54
56
|
for (const collector of collectors) {
|
|
55
57
|
// Use full ID so it matches stored data keys like "healthcheck-http.request"
|
|
56
58
|
collectorProperties[collector.id] = collector.resultSchema;
|
|
59
|
+
|
|
60
|
+
// Also collect aggregated schemas if available
|
|
61
|
+
if (collector.aggregatedResultSchema) {
|
|
62
|
+
collectorAggregatedProperties[collector.id] =
|
|
63
|
+
collector.aggregatedResultSchema;
|
|
64
|
+
}
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
// Merge collector schemas into strategy result schema
|
|
60
68
|
const mergedResultSchema = mergeCollectorSchemas(
|
|
61
69
|
strategy.resultSchema as Record<string, unknown> | undefined,
|
|
62
|
-
collectorProperties
|
|
70
|
+
collectorProperties,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Merge collector aggregated schemas into strategy aggregated schema
|
|
74
|
+
const mergedAggregatedSchema = mergeCollectorSchemas(
|
|
75
|
+
strategy.aggregatedResultSchema as Record<string, unknown> | undefined,
|
|
76
|
+
collectorAggregatedProperties,
|
|
63
77
|
);
|
|
64
78
|
|
|
65
79
|
setSchemas({
|
|
66
80
|
resultSchema: mergedResultSchema,
|
|
67
|
-
aggregatedResultSchema:
|
|
68
|
-
(strategy.aggregatedResultSchema as Record<string, unknown>) ??
|
|
69
|
-
undefined,
|
|
81
|
+
aggregatedResultSchema: mergedAggregatedSchema,
|
|
70
82
|
});
|
|
71
83
|
}
|
|
72
84
|
|
|
@@ -85,7 +97,7 @@ export function useStrategySchemas(strategyId: string): {
|
|
|
85
97
|
*/
|
|
86
98
|
function mergeCollectorSchemas(
|
|
87
99
|
strategySchema: Record<string, unknown> | undefined,
|
|
88
|
-
collectorProperties: Record<string, unknown
|
|
100
|
+
collectorProperties: Record<string, unknown>,
|
|
89
101
|
): Record<string, unknown> | undefined {
|
|
90
102
|
// If no collectors, return original schema
|
|
91
103
|
if (Object.keys(collectorProperties).length === 0) {
|
|
@@ -1,24 +1,67 @@
|
|
|
1
1
|
import { InfoBanner } from "@checkstack/ui";
|
|
2
2
|
|
|
3
3
|
interface AggregatedDataBannerProps {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/** Bucket interval in seconds */
|
|
5
|
+
bucketIntervalSeconds: number;
|
|
6
|
+
/** The configured check interval in seconds (optional, for comparison) */
|
|
7
|
+
checkIntervalSeconds?: number;
|
|
6
8
|
}
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Format seconds into a human-readable duration string.
|
|
12
|
+
*/
|
|
13
|
+
function formatDuration(seconds: number): string {
|
|
14
|
+
if (seconds < 60) {
|
|
15
|
+
return `${seconds}s`;
|
|
16
|
+
}
|
|
17
|
+
if (seconds < 3600) {
|
|
18
|
+
const mins = Math.round(seconds / 60);
|
|
19
|
+
return `${mins}min`;
|
|
20
|
+
}
|
|
21
|
+
const hours = Math.round(seconds / 3600);
|
|
22
|
+
return `${hours}h`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get resolution tier label based on bucket interval.
|
|
27
|
+
*/
|
|
28
|
+
function getResolutionTier(
|
|
29
|
+
bucketIntervalSeconds: number,
|
|
30
|
+
): "high" | "medium" | "low" {
|
|
31
|
+
if (bucketIntervalSeconds >= 86_400) {
|
|
32
|
+
return "low"; // Daily aggregates
|
|
33
|
+
}
|
|
34
|
+
if (bucketIntervalSeconds >= 3600) {
|
|
35
|
+
return "medium"; // Hourly aggregates
|
|
36
|
+
}
|
|
37
|
+
return "high"; // Raw data
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TIER_LABELS: Record<"high" | "medium" | "low", string> = {
|
|
41
|
+
high: "High resolution",
|
|
42
|
+
medium: "Medium resolution",
|
|
43
|
+
low: "Low resolution",
|
|
44
|
+
};
|
|
45
|
+
|
|
8
46
|
/**
|
|
9
47
|
* Banner shown when viewing aggregated health check data.
|
|
10
|
-
* Informs users about the aggregation level
|
|
48
|
+
* Informs users about the aggregation level due to high data volume.
|
|
11
49
|
*/
|
|
12
50
|
export function AggregatedDataBanner({
|
|
13
|
-
|
|
14
|
-
|
|
51
|
+
bucketIntervalSeconds,
|
|
52
|
+
checkIntervalSeconds,
|
|
15
53
|
}: AggregatedDataBannerProps) {
|
|
16
|
-
|
|
54
|
+
// Only show if bucket interval is larger than check interval (data is being aggregated)
|
|
55
|
+
if (checkIntervalSeconds && bucketIntervalSeconds <= checkIntervalSeconds) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tier = getResolutionTier(bucketIntervalSeconds);
|
|
17
60
|
|
|
18
61
|
return (
|
|
19
62
|
<InfoBanner variant="info">
|
|
20
|
-
|
|
21
|
-
{
|
|
63
|
+
{TIER_LABELS[tier]} • Data aggregated into{" "}
|
|
64
|
+
{formatDuration(bucketIntervalSeconds)} intervals
|
|
22
65
|
</InfoBanner>
|
|
23
66
|
);
|
|
24
67
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Card,
|
|
8
8
|
CardContent,
|
|
9
9
|
CardHeader,
|
|
10
|
+
CardHeaderRow,
|
|
10
11
|
CardTitle,
|
|
11
12
|
Label,
|
|
12
13
|
Select,
|
|
@@ -157,62 +158,68 @@ export const CollectorList: React.FC<CollectorListProps> = ({
|
|
|
157
158
|
|
|
158
159
|
return (
|
|
159
160
|
<Card>
|
|
160
|
-
<CardHeader
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
161
|
+
<CardHeader>
|
|
162
|
+
<CardHeaderRow>
|
|
163
|
+
<CardTitle className="text-base">Check Items</CardTitle>
|
|
164
|
+
{addableCollectors.length > 0 && (
|
|
165
|
+
<Select value="" onValueChange={handleAdd}>
|
|
166
|
+
<SelectTrigger className="w-[200px]">
|
|
167
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
168
|
+
<SelectValue placeholder="Add collector..." />
|
|
169
|
+
</SelectTrigger>
|
|
170
|
+
<SelectContent>
|
|
171
|
+
{/* Built-in collectors first */}
|
|
172
|
+
{builtInCollectors.length > 0 && (
|
|
173
|
+
<>
|
|
174
|
+
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
|
175
|
+
Built-in
|
|
176
|
+
</div>
|
|
177
|
+
{builtInCollectors
|
|
178
|
+
.filter((c) =>
|
|
179
|
+
addableCollectors.some((a) => a.id === c.id),
|
|
180
|
+
)
|
|
181
|
+
.map((collector) => (
|
|
182
|
+
<SelectItem key={collector.id} value={collector.id}>
|
|
183
|
+
<div className="flex items-center gap-2">
|
|
184
|
+
<span>{collector.displayName}</span>
|
|
185
|
+
{collector.allowMultiple && (
|
|
186
|
+
<Badge variant="outline" className="text-xs">
|
|
187
|
+
Multiple
|
|
188
|
+
</Badge>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</SelectItem>
|
|
192
|
+
))}
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
{/* External collectors */}
|
|
196
|
+
{externalCollectors.length > 0 && (
|
|
197
|
+
<>
|
|
198
|
+
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
|
199
|
+
External
|
|
200
|
+
</div>
|
|
201
|
+
{externalCollectors
|
|
202
|
+
.filter((c) =>
|
|
203
|
+
addableCollectors.some((a) => a.id === c.id),
|
|
204
|
+
)
|
|
205
|
+
.map((collector) => (
|
|
206
|
+
<SelectItem key={collector.id} value={collector.id}>
|
|
207
|
+
<div className="flex items-center gap-2">
|
|
208
|
+
<span>{collector.displayName}</span>
|
|
209
|
+
{collector.allowMultiple && (
|
|
210
|
+
<Badge variant="outline" className="text-xs">
|
|
211
|
+
Multiple
|
|
212
|
+
</Badge>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
</SelectItem>
|
|
216
|
+
))}
|
|
217
|
+
</>
|
|
218
|
+
)}
|
|
219
|
+
</SelectContent>
|
|
220
|
+
</Select>
|
|
221
|
+
)}
|
|
222
|
+
</CardHeaderRow>
|
|
216
223
|
</CardHeader>
|
|
217
224
|
<CardContent>
|
|
218
225
|
{configuredCollectors.length === 0 ? (
|
|
@@ -11,8 +11,10 @@ interface HealthCheckDiagramProps {
|
|
|
11
11
|
context: HealthCheckDiagramSlotContext;
|
|
12
12
|
/** Whether the data is aggregated (for showing the info banner) */
|
|
13
13
|
isAggregated?: boolean;
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/** The bucket interval in seconds (from aggregated response) */
|
|
15
|
+
bucketIntervalSeconds?: number;
|
|
16
|
+
/** The check interval in seconds (for comparison in banner) */
|
|
17
|
+
checkIntervalSeconds?: number;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -22,20 +24,15 @@ interface HealthCheckDiagramProps {
|
|
|
22
24
|
export function HealthCheckDiagram({
|
|
23
25
|
context,
|
|
24
26
|
isAggregated = false,
|
|
25
|
-
|
|
27
|
+
bucketIntervalSeconds,
|
|
28
|
+
checkIntervalSeconds,
|
|
26
29
|
}: HealthCheckDiagramProps) {
|
|
27
|
-
// Determine bucket size from context for aggregated data info banner
|
|
28
|
-
const bucketSize =
|
|
29
|
-
context.type === "aggregated" && context.buckets.length > 0
|
|
30
|
-
? context.buckets[0].bucketSize
|
|
31
|
-
: "hourly";
|
|
32
|
-
|
|
33
30
|
return (
|
|
34
31
|
<>
|
|
35
|
-
{isAggregated && (
|
|
32
|
+
{isAggregated && bucketIntervalSeconds && (
|
|
36
33
|
<AggregatedDataBanner
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
bucketIntervalSeconds={bucketIntervalSeconds}
|
|
35
|
+
checkIntervalSeconds={checkIntervalSeconds}
|
|
39
36
|
/>
|
|
40
37
|
)}
|
|
41
38
|
<ExtensionSlot slot={HealthCheckDiagramSlot} context={context} />
|
|
@@ -50,8 +50,11 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
50
50
|
? chartData.reduce((sum, d) => sum + d.latencyMs, 0) / chartData.length
|
|
51
51
|
: 0;
|
|
52
52
|
|
|
53
|
+
// Use daily format for intervals >= 6 hours, otherwise include time
|
|
53
54
|
const timeFormat =
|
|
54
|
-
buckets[0]?.
|
|
55
|
+
(buckets[0]?.bucketIntervalSeconds ?? 3600) >= 21_600
|
|
56
|
+
? "MMM d"
|
|
57
|
+
: "MMM d HH:mm";
|
|
55
58
|
|
|
56
59
|
return (
|
|
57
60
|
<ResponsiveContainer width="100%" height={height}>
|
|
@@ -73,7 +76,7 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
73
76
|
<XAxis
|
|
74
77
|
dataKey="timestamp"
|
|
75
78
|
type="number"
|
|
76
|
-
domain={["
|
|
79
|
+
domain={["dataMin", "dataMax"]}
|
|
77
80
|
tickFormatter={(ts: number) => format(new Date(ts), timeFormat)}
|
|
78
81
|
stroke="hsl(var(--muted-foreground))"
|
|
79
82
|
fontSize={12}
|
|
@@ -172,7 +175,7 @@ export const HealthCheckLatencyChart: React.FC<
|
|
|
172
175
|
<XAxis
|
|
173
176
|
dataKey="timestamp"
|
|
174
177
|
type="number"
|
|
175
|
-
domain={["
|
|
178
|
+
domain={["dataMin", "dataMax"]}
|
|
176
179
|
tickFormatter={(ts: number) => format(new Date(ts), "HH:mm")}
|
|
177
180
|
stroke="hsl(var(--muted-foreground))"
|
|
178
181
|
fontSize={12}
|
|
@@ -11,14 +11,18 @@ import {
|
|
|
11
11
|
TableHeader,
|
|
12
12
|
TableRow,
|
|
13
13
|
Button,
|
|
14
|
+
Badge,
|
|
14
15
|
} from "@checkstack/ui";
|
|
15
|
-
import { Trash2, Edit } from "lucide-react";
|
|
16
|
+
import { Trash2, Edit, Pause, Play } from "lucide-react";
|
|
16
17
|
|
|
17
18
|
interface HealthCheckListProps {
|
|
18
19
|
configurations: HealthCheckConfiguration[];
|
|
19
20
|
strategies: HealthCheckStrategyDto[];
|
|
20
21
|
onEdit: (config: HealthCheckConfiguration) => void;
|
|
21
22
|
onDelete: (id: string) => void;
|
|
23
|
+
onPause?: (id: string) => void;
|
|
24
|
+
onResume?: (id: string) => void;
|
|
25
|
+
canManage?: boolean;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export const HealthCheckList: React.FC<HealthCheckListProps> = ({
|
|
@@ -26,6 +30,9 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
|
|
|
26
30
|
strategies,
|
|
27
31
|
onEdit,
|
|
28
32
|
onDelete,
|
|
33
|
+
onPause,
|
|
34
|
+
onResume,
|
|
35
|
+
canManage = true,
|
|
29
36
|
}) => {
|
|
30
37
|
const getStrategyName = (id: string) => {
|
|
31
38
|
return strategies.find((s) => s.id === id)?.displayName || id;
|
|
@@ -39,24 +46,57 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
|
|
|
39
46
|
<TableHead>Name</TableHead>
|
|
40
47
|
<TableHead>Strategy</TableHead>
|
|
41
48
|
<TableHead>Interval (s)</TableHead>
|
|
49
|
+
<TableHead>Status</TableHead>
|
|
42
50
|
<TableHead className="text-right">Actions</TableHead>
|
|
43
51
|
</TableRow>
|
|
44
52
|
</TableHeader>
|
|
45
53
|
<TableBody>
|
|
46
54
|
{configurations.length === 0 ? (
|
|
47
55
|
<TableRow>
|
|
48
|
-
<TableCell colSpan={
|
|
56
|
+
<TableCell colSpan={5} className="h-24 text-center">
|
|
49
57
|
No health checks configured.
|
|
50
58
|
</TableCell>
|
|
51
59
|
</TableRow>
|
|
52
60
|
) : (
|
|
53
61
|
configurations.map((config) => (
|
|
54
|
-
<TableRow
|
|
62
|
+
<TableRow
|
|
63
|
+
key={config.id}
|
|
64
|
+
className={config.paused ? "opacity-60" : ""}
|
|
65
|
+
>
|
|
55
66
|
<TableCell className="font-medium">{config.name}</TableCell>
|
|
56
67
|
<TableCell>{getStrategyName(config.strategyId)}</TableCell>
|
|
57
68
|
<TableCell>{config.intervalSeconds}</TableCell>
|
|
69
|
+
<TableCell>
|
|
70
|
+
{config.paused ? (
|
|
71
|
+
<Badge variant="secondary">Paused</Badge>
|
|
72
|
+
) : (
|
|
73
|
+
<Badge variant="default">Active</Badge>
|
|
74
|
+
)}
|
|
75
|
+
</TableCell>
|
|
58
76
|
<TableCell className="text-right">
|
|
59
77
|
<div className="flex justify-end gap-2">
|
|
78
|
+
{canManage &&
|
|
79
|
+
onPause &&
|
|
80
|
+
onResume &&
|
|
81
|
+
(config.paused ? (
|
|
82
|
+
<Button
|
|
83
|
+
variant="ghost"
|
|
84
|
+
size="icon"
|
|
85
|
+
onClick={() => onResume(config.id)}
|
|
86
|
+
title="Resume health check"
|
|
87
|
+
>
|
|
88
|
+
<Play className="h-4 w-4" />
|
|
89
|
+
</Button>
|
|
90
|
+
) : (
|
|
91
|
+
<Button
|
|
92
|
+
variant="ghost"
|
|
93
|
+
size="icon"
|
|
94
|
+
onClick={() => onPause(config.id)}
|
|
95
|
+
title="Pause health check"
|
|
96
|
+
>
|
|
97
|
+
<Pause className="h-4 w-4" />
|
|
98
|
+
</Button>
|
|
99
|
+
))}
|
|
60
100
|
<Button
|
|
61
101
|
variant="ghost"
|
|
62
102
|
size="icon"
|
|
@@ -64,14 +104,16 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
|
|
|
64
104
|
>
|
|
65
105
|
<Edit className="h-4 w-4" />
|
|
66
106
|
</Button>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
107
|
+
{canManage && (
|
|
108
|
+
<Button
|
|
109
|
+
variant="ghost"
|
|
110
|
+
size="icon"
|
|
111
|
+
className="text-destructive hover:text-destructive"
|
|
112
|
+
onClick={() => onDelete(config.id)}
|
|
113
|
+
>
|
|
114
|
+
<Trash2 className="h-4 w-4" />
|
|
115
|
+
</Button>
|
|
116
|
+
)}
|
|
75
117
|
</div>
|
|
76
118
|
</TableCell>
|
|
77
119
|
</TableRow>
|
|
@@ -50,8 +50,11 @@ export const HealthCheckStatusTimeline: React.FC<
|
|
|
50
50
|
total: d.runCount,
|
|
51
51
|
}));
|
|
52
52
|
|
|
53
|
+
// Use daily format for intervals >= 6 hours, otherwise include time
|
|
53
54
|
const timeFormat =
|
|
54
|
-
buckets[0]?.
|
|
55
|
+
(buckets[0]?.bucketIntervalSeconds ?? 3600) >= 21_600
|
|
56
|
+
? "MMM d"
|
|
57
|
+
: "MMM d HH:mm";
|
|
55
58
|
|
|
56
59
|
return (
|
|
57
60
|
<ResponsiveContainer width="100%" height={height}>
|
|
@@ -59,7 +62,7 @@ export const HealthCheckStatusTimeline: React.FC<
|
|
|
59
62
|
<XAxis
|
|
60
63
|
dataKey="timestamp"
|
|
61
64
|
type="number"
|
|
62
|
-
domain={["
|
|
65
|
+
domain={["dataMin", "dataMax"]}
|
|
63
66
|
tickFormatter={(ts: number) => format(new Date(ts), timeFormat)}
|
|
64
67
|
stroke="hsl(var(--muted-foreground))"
|
|
65
68
|
fontSize={10}
|
|
@@ -133,7 +136,7 @@ export const HealthCheckStatusTimeline: React.FC<
|
|
|
133
136
|
<XAxis
|
|
134
137
|
dataKey="timestamp"
|
|
135
138
|
type="number"
|
|
136
|
-
domain={["
|
|
139
|
+
domain={["dataMin", "dataMax"]}
|
|
137
140
|
tickFormatter={(ts: number) => format(new Date(ts), "HH:mm")}
|
|
138
141
|
stroke="hsl(var(--muted-foreground))"
|
|
139
142
|
fontSize={10}
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
usePagination,
|
|
21
21
|
usePaginationSync,
|
|
22
22
|
DateRangeFilter,
|
|
23
|
+
getPresetRange,
|
|
24
|
+
DateRangePreset,
|
|
23
25
|
} from "@checkstack/ui";
|
|
24
26
|
import { formatDistanceToNow } from "date-fns";
|
|
25
27
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
@@ -55,17 +57,10 @@ interface ExpandedRowProps {
|
|
|
55
57
|
const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
56
58
|
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
57
59
|
|
|
58
|
-
// Date range state for filtering
|
|
59
|
-
const [dateRange, setDateRange] = useState
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
});
|
|
60
|
+
// Date range state for filtering - default to last 24 hours
|
|
61
|
+
const [dateRange, setDateRange] = useState(() =>
|
|
62
|
+
getPresetRange(DateRangePreset.Last24Hours),
|
|
63
|
+
);
|
|
69
64
|
|
|
70
65
|
// Use shared hook for chart data - handles both raw and aggregated modes
|
|
71
66
|
// and includes signal handling for automatic refresh
|
|
@@ -73,7 +68,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
73
68
|
context: chartContext,
|
|
74
69
|
loading: chartLoading,
|
|
75
70
|
isAggregated,
|
|
76
|
-
|
|
71
|
+
bucketIntervalSeconds,
|
|
77
72
|
} = useHealthCheckData({
|
|
78
73
|
systemId,
|
|
79
74
|
configurationId: item.configurationId,
|
|
@@ -160,7 +155,8 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
160
155
|
<HealthCheckDiagram
|
|
161
156
|
context={chartContext}
|
|
162
157
|
isAggregated={isAggregated}
|
|
163
|
-
|
|
158
|
+
bucketIntervalSeconds={bucketIntervalSeconds}
|
|
159
|
+
checkIntervalSeconds={item.intervalSeconds}
|
|
164
160
|
/>
|
|
165
161
|
</div>
|
|
166
162
|
);
|
|
@@ -184,10 +180,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
184
180
|
</div>
|
|
185
181
|
|
|
186
182
|
{/* Date Range Filter */}
|
|
187
|
-
<
|
|
188
|
-
<span className="text-sm text-muted-foreground">Time Range:</span>
|
|
189
|
-
<DateRangeFilter value={dateRange} onChange={setDateRange} />
|
|
190
|
-
</div>
|
|
183
|
+
<DateRangeFilter value={dateRange} onChange={setDateRange} />
|
|
191
184
|
|
|
192
185
|
{/* Charts Section */}
|
|
193
186
|
{renderCharts()}
|
|
@@ -281,8 +274,8 @@ export function HealthCheckSystemOverview(props: SlotProps) {
|
|
|
281
274
|
void refetch();
|
|
282
275
|
}
|
|
283
276
|
},
|
|
284
|
-
[systemId, refetch]
|
|
285
|
-
)
|
|
277
|
+
[systemId, refetch],
|
|
278
|
+
),
|
|
286
279
|
);
|
|
287
280
|
|
|
288
281
|
if (initialLoading) {
|
|
@@ -305,28 +298,31 @@ export function HealthCheckSystemOverview(props: SlotProps) {
|
|
|
305
298
|
return (
|
|
306
299
|
<div key={item.configurationId} className="rounded-md border bg-card">
|
|
307
300
|
<button
|
|
308
|
-
className="w-full
|
|
301
|
+
className="w-full p-4 text-left hover:bg-muted/50 transition-colors"
|
|
309
302
|
onClick={() =>
|
|
310
303
|
setExpandedRow(isExpanded ? undefined : item.configurationId)
|
|
311
304
|
}
|
|
312
305
|
>
|
|
306
|
+
{/* Header row: chevron, name, badge */}
|
|
313
307
|
<div className="flex items-center gap-3">
|
|
314
308
|
{isExpanded ? (
|
|
315
|
-
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
309
|
+
<ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
316
310
|
) : (
|
|
317
|
-
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
311
|
+
<ChevronRight className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
318
312
|
)}
|
|
319
|
-
<div>
|
|
320
|
-
<
|
|
321
|
-
<
|
|
322
|
-
Last run:{" "}
|
|
323
|
-
{item.lastRunAt
|
|
324
|
-
? formatDistanceToNow(item.lastRunAt, { addSuffix: true })
|
|
325
|
-
: "never"}
|
|
326
|
-
</div>
|
|
313
|
+
<div className="flex-1 min-w-0 flex items-center justify-between gap-2">
|
|
314
|
+
<span className="font-medium truncate">{item.name}</span>
|
|
315
|
+
<HealthBadge status={item.state} />
|
|
327
316
|
</div>
|
|
328
317
|
</div>
|
|
329
|
-
|
|
318
|
+
{/* Details row: last run + sparkline */}
|
|
319
|
+
<div className="ml-7 mt-1 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
|
320
|
+
<span className="text-sm text-muted-foreground">
|
|
321
|
+
Last run:{" "}
|
|
322
|
+
{item.lastRunAt
|
|
323
|
+
? formatDistanceToNow(item.lastRunAt, { addSuffix: true })
|
|
324
|
+
: "never"}
|
|
325
|
+
</span>
|
|
330
326
|
{item.recentStatusHistory.length > 0 && (
|
|
331
327
|
<HealthCheckSparkline
|
|
332
328
|
runs={item.recentStatusHistory.map((status) => ({
|
|
@@ -334,7 +330,6 @@ export function HealthCheckSystemOverview(props: SlotProps) {
|
|
|
334
330
|
}))}
|
|
335
331
|
/>
|
|
336
332
|
)}
|
|
337
|
-
<HealthBadge status={item.state} />
|
|
338
333
|
</div>
|
|
339
334
|
</button>
|
|
340
335
|
{isExpanded && <ExpandedDetails item={item} systemId={systemId} />}
|
|
@@ -44,6 +44,8 @@ interface UseHealthCheckDataResult {
|
|
|
44
44
|
hasAccess: boolean;
|
|
45
45
|
/** Whether access is still loading */
|
|
46
46
|
accessLoading: boolean;
|
|
47
|
+
/** Bucket interval in seconds (only for aggregated mode) */
|
|
48
|
+
bucketIntervalSeconds: number | undefined;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
/**
|
|
@@ -67,14 +69,14 @@ export function useHealthCheckData({
|
|
|
67
69
|
|
|
68
70
|
// Access state
|
|
69
71
|
const { allowed: hasAccess, loading: accessLoading } = accessApi.useAccess(
|
|
70
|
-
healthCheckAccess.details
|
|
72
|
+
healthCheckAccess.details,
|
|
71
73
|
);
|
|
72
74
|
|
|
73
75
|
// Calculate date range in days
|
|
74
76
|
const dateRangeDays = useMemo(() => {
|
|
75
77
|
return Math.ceil(
|
|
76
78
|
(dateRange.endDate.getTime() - dateRange.startDate.getTime()) /
|
|
77
|
-
(1000 * 60 * 60 * 24)
|
|
79
|
+
(1000 * 60 * 60 * 24),
|
|
78
80
|
);
|
|
79
81
|
}, [dateRange.startDate, dateRange.endDate]);
|
|
80
82
|
|
|
@@ -82,7 +84,7 @@ export function useHealthCheckData({
|
|
|
82
84
|
const { data: retentionData, isLoading: retentionLoading } =
|
|
83
85
|
healthCheckClient.getRetentionConfig.useQuery(
|
|
84
86
|
{ systemId, configurationId },
|
|
85
|
-
{ enabled: !!systemId && !!configurationId && hasAccess }
|
|
87
|
+
{ enabled: !!systemId && !!configurationId && hasAccess },
|
|
86
88
|
);
|
|
87
89
|
|
|
88
90
|
const retentionConfig =
|
|
@@ -101,6 +103,7 @@ export function useHealthCheckData({
|
|
|
101
103
|
systemId,
|
|
102
104
|
configurationId,
|
|
103
105
|
startDate: dateRange.startDate,
|
|
106
|
+
endDate: dateRange.endDate,
|
|
104
107
|
limit,
|
|
105
108
|
offset,
|
|
106
109
|
},
|
|
@@ -112,11 +115,10 @@ export function useHealthCheckData({
|
|
|
112
115
|
!accessLoading &&
|
|
113
116
|
!retentionLoading &&
|
|
114
117
|
!isAggregated,
|
|
115
|
-
}
|
|
118
|
+
},
|
|
116
119
|
);
|
|
117
120
|
|
|
118
121
|
// Query: Fetch aggregated data (when in aggregated mode)
|
|
119
|
-
const bucketSize = dateRangeDays > 30 ? "daily" : "hourly";
|
|
120
122
|
const { data: aggregatedData, isLoading: aggregatedLoading } =
|
|
121
123
|
healthCheckClient.getDetailedAggregatedHistory.useQuery(
|
|
122
124
|
{
|
|
@@ -124,7 +126,7 @@ export function useHealthCheckData({
|
|
|
124
126
|
configurationId,
|
|
125
127
|
startDate: dateRange.startDate,
|
|
126
128
|
endDate: dateRange.endDate,
|
|
127
|
-
|
|
129
|
+
targetPoints: 500,
|
|
128
130
|
},
|
|
129
131
|
{
|
|
130
132
|
enabled:
|
|
@@ -134,7 +136,7 @@ export function useHealthCheckData({
|
|
|
134
136
|
!accessLoading &&
|
|
135
137
|
!retentionLoading &&
|
|
136
138
|
isAggregated,
|
|
137
|
-
}
|
|
139
|
+
},
|
|
138
140
|
);
|
|
139
141
|
|
|
140
142
|
// Listen for realtime health check updates to refresh data silently
|
|
@@ -223,5 +225,6 @@ export function useHealthCheckData({
|
|
|
223
225
|
retentionConfig,
|
|
224
226
|
hasAccess,
|
|
225
227
|
accessLoading,
|
|
228
|
+
bucketIntervalSeconds: aggregatedData?.bucketIntervalSeconds,
|
|
226
229
|
};
|
|
227
230
|
}
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
PageLayout,
|
|
22
22
|
useToast,
|
|
23
23
|
} from "@checkstack/ui";
|
|
24
|
-
import { Plus, History } from "lucide-react";
|
|
24
|
+
import { Plus, History, Activity } from "lucide-react";
|
|
25
25
|
import { Link } from "react-router-dom";
|
|
26
26
|
import { resolveRoute } from "@checkstack/common";
|
|
27
27
|
|
|
@@ -31,10 +31,10 @@ const HealthCheckConfigPageContent = () => {
|
|
|
31
31
|
const toast = useToast();
|
|
32
32
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
33
33
|
const { allowed: canRead, loading: accessLoading } = accessApi.useAccess(
|
|
34
|
-
healthCheckAccess.configuration.read
|
|
34
|
+
healthCheckAccess.configuration.read,
|
|
35
35
|
);
|
|
36
36
|
const { allowed: canManage } = accessApi.useAccess(
|
|
37
|
-
healthCheckAccess.configuration.manage
|
|
37
|
+
healthCheckAccess.configuration.manage,
|
|
38
38
|
);
|
|
39
39
|
|
|
40
40
|
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
|
@@ -52,7 +52,7 @@ const HealthCheckConfigPageContent = () => {
|
|
|
52
52
|
|
|
53
53
|
// Fetch strategies with useQuery
|
|
54
54
|
const { data: strategies = [] } = healthCheckClient.getStrategies.useQuery(
|
|
55
|
-
{}
|
|
55
|
+
{},
|
|
56
56
|
);
|
|
57
57
|
|
|
58
58
|
const configurations = configurationsData?.configurations ?? [];
|
|
@@ -100,6 +100,24 @@ const HealthCheckConfigPageContent = () => {
|
|
|
100
100
|
},
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
const pauseMutation = healthCheckClient.pauseConfiguration.useMutation({
|
|
104
|
+
onSuccess: () => {
|
|
105
|
+
void refetchConfigurations();
|
|
106
|
+
},
|
|
107
|
+
onError: (error) => {
|
|
108
|
+
toast.error(error instanceof Error ? error.message : "Failed to pause");
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const resumeMutation = healthCheckClient.resumeConfiguration.useMutation({
|
|
113
|
+
onSuccess: () => {
|
|
114
|
+
void refetchConfigurations();
|
|
115
|
+
},
|
|
116
|
+
onError: (error) => {
|
|
117
|
+
toast.error(error instanceof Error ? error.message : "Failed to resume");
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
103
121
|
const handleCreate = () => {
|
|
104
122
|
setEditingConfig(undefined);
|
|
105
123
|
setIsEditorOpen(true);
|
|
@@ -137,6 +155,7 @@ const HealthCheckConfigPageContent = () => {
|
|
|
137
155
|
<PageLayout
|
|
138
156
|
title="Health Checks"
|
|
139
157
|
subtitle="Manage health check configurations"
|
|
158
|
+
icon={Activity}
|
|
140
159
|
loading={accessLoading}
|
|
141
160
|
allowed={canRead}
|
|
142
161
|
actions={
|
|
@@ -159,6 +178,9 @@ const HealthCheckConfigPageContent = () => {
|
|
|
159
178
|
strategies={strategies}
|
|
160
179
|
onEdit={handleEdit}
|
|
161
180
|
onDelete={handleDelete}
|
|
181
|
+
onPause={(id) => pauseMutation.mutate(id)}
|
|
182
|
+
onResume={(id) => resumeMutation.mutate(id)}
|
|
183
|
+
canManage={canManage}
|
|
162
184
|
/>
|
|
163
185
|
|
|
164
186
|
<HealthCheckEditor
|
|
@@ -184,5 +206,5 @@ const HealthCheckConfigPageContent = () => {
|
|
|
184
206
|
};
|
|
185
207
|
|
|
186
208
|
export const HealthCheckConfigPage = wrapInSuspense(
|
|
187
|
-
HealthCheckConfigPageContent
|
|
209
|
+
HealthCheckConfigPageContent,
|
|
188
210
|
);
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type DateRange,
|
|
26
26
|
} from "@checkstack/ui";
|
|
27
27
|
import { useParams } from "react-router-dom";
|
|
28
|
+
import { History } from "lucide-react";
|
|
28
29
|
import {
|
|
29
30
|
HealthCheckRunsTable,
|
|
30
31
|
type HealthCheckRunDetailed,
|
|
@@ -39,7 +40,7 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
39
40
|
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
40
41
|
const accessApi = useApi(accessApiRef);
|
|
41
42
|
const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
|
|
42
|
-
healthCheckAccess.configuration.manage
|
|
43
|
+
healthCheckAccess.configuration.manage,
|
|
43
44
|
);
|
|
44
45
|
|
|
45
46
|
const [dateRange, setDateRange] = useState<DateRange>(getDefaultDateRange);
|
|
@@ -67,8 +68,9 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
67
68
|
title="Health Check Run History"
|
|
68
69
|
subtitle={`System: ${systemId} • Configuration: ${configurationId?.slice(
|
|
69
70
|
0,
|
|
70
|
-
8
|
|
71
|
+
8,
|
|
71
72
|
)}...`}
|
|
73
|
+
icon={History}
|
|
72
74
|
loading={accessLoading}
|
|
73
75
|
allowed={canManage}
|
|
74
76
|
actions={
|
|
@@ -100,5 +102,5 @@ const HealthCheckHistoryDetailPageContent = () => {
|
|
|
100
102
|
};
|
|
101
103
|
|
|
102
104
|
export const HealthCheckHistoryDetailPage = wrapInSuspense(
|
|
103
|
-
HealthCheckHistoryDetailPageContent
|
|
105
|
+
HealthCheckHistoryDetailPageContent,
|
|
104
106
|
);
|
|
@@ -21,12 +21,13 @@ import {
|
|
|
21
21
|
healthCheckAccess,
|
|
22
22
|
HealthCheckApi,
|
|
23
23
|
} from "@checkstack/healthcheck-common";
|
|
24
|
+
import { History } from "lucide-react";
|
|
24
25
|
|
|
25
26
|
const HealthCheckHistoryPageContent = () => {
|
|
26
27
|
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
27
28
|
const accessApi = useApi(accessApiRef);
|
|
28
29
|
const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
|
|
29
|
-
healthCheckAccess.configuration.manage
|
|
30
|
+
healthCheckAccess.configuration.manage,
|
|
30
31
|
);
|
|
31
32
|
|
|
32
33
|
// Pagination state
|
|
@@ -47,6 +48,7 @@ const HealthCheckHistoryPageContent = () => {
|
|
|
47
48
|
<PageLayout
|
|
48
49
|
title="Health Check History"
|
|
49
50
|
subtitle="Detailed run history with full result data"
|
|
51
|
+
icon={History}
|
|
50
52
|
loading={accessLoading}
|
|
51
53
|
allowed={canManage}
|
|
52
54
|
>
|
|
@@ -68,5 +70,5 @@ const HealthCheckHistoryPageContent = () => {
|
|
|
68
70
|
};
|
|
69
71
|
|
|
70
72
|
export const HealthCheckHistoryPage = wrapInSuspense(
|
|
71
|
-
HealthCheckHistoryPageContent
|
|
73
|
+
HealthCheckHistoryPageContent,
|
|
72
74
|
);
|