@checkstack/healthcheck-frontend 0.3.0 → 0.4.1
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 +78 -0
- package/package.json +2 -1
- package/src/api.ts +2 -10
- package/src/auto-charts/useStrategySchemas.ts +39 -48
- package/src/components/HealthCheckHistory.tsx +10 -14
- package/src/components/HealthCheckSystemOverview.tsx +66 -123
- package/src/components/SystemHealthBadge.tsx +36 -27
- package/src/components/SystemHealthCheckAssignment.tsx +191 -210
- package/src/hooks/useCollectors.ts +21 -34
- package/src/hooks/useHealthCheckData.ts +88 -137
- package/src/index.tsx +2 -16
- package/src/pages/HealthCheckConfigPage.tsx +64 -40
- package/src/pages/HealthCheckHistoryDetailPage.tsx +25 -24
- package/src/pages/HealthCheckHistoryPage.tsx +24 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# @checkstack/healthcheck-frontend
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [4eed42d]
|
|
8
|
+
- @checkstack/frontend-api@0.3.0
|
|
9
|
+
- @checkstack/dashboard-frontend@0.3.0
|
|
10
|
+
- @checkstack/auth-frontend@0.3.1
|
|
11
|
+
- @checkstack/catalog-common@1.2.1
|
|
12
|
+
- @checkstack/ui@0.2.2
|
|
13
|
+
|
|
14
|
+
## 0.4.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- 7a23261: ## TanStack Query Integration
|
|
19
|
+
|
|
20
|
+
Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
|
|
21
|
+
|
|
22
|
+
### New Features
|
|
23
|
+
|
|
24
|
+
- **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
|
|
25
|
+
- **Automatic request deduplication**: Multiple components requesting the same data share a single network request
|
|
26
|
+
- **Built-in caching**: Configurable stale time and cache duration per query
|
|
27
|
+
- **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
|
|
28
|
+
- **Background refetching**: Stale data is automatically refreshed when components mount
|
|
29
|
+
|
|
30
|
+
### Contract Changes
|
|
31
|
+
|
|
32
|
+
All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
const getItems = proc()
|
|
36
|
+
.meta({ operationType: "query", access: [access.read] })
|
|
37
|
+
.output(z.array(itemSchema))
|
|
38
|
+
.query();
|
|
39
|
+
|
|
40
|
+
const createItem = proc()
|
|
41
|
+
.meta({ operationType: "mutation", access: [access.manage] })
|
|
42
|
+
.input(createItemSchema)
|
|
43
|
+
.output(itemSchema)
|
|
44
|
+
.mutation();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Migration
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// Before (forPlugin pattern)
|
|
51
|
+
const api = useApi(myPluginApiRef);
|
|
52
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
api.getItems().then(setItems);
|
|
55
|
+
}, [api]);
|
|
56
|
+
|
|
57
|
+
// After (usePluginClient pattern)
|
|
58
|
+
const client = usePluginClient(MyPluginApi);
|
|
59
|
+
const { data: items, isLoading } = client.getItems.useQuery({});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Bug Fixes
|
|
63
|
+
|
|
64
|
+
- Fixed `rpc.test.ts` test setup for middleware type inference
|
|
65
|
+
- Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
|
|
66
|
+
- Fixed null→undefined warnings in notification and queue frontends
|
|
67
|
+
|
|
68
|
+
### Patch Changes
|
|
69
|
+
|
|
70
|
+
- Updated dependencies [180be38]
|
|
71
|
+
- Updated dependencies [7a23261]
|
|
72
|
+
- @checkstack/dashboard-frontend@0.2.0
|
|
73
|
+
- @checkstack/frontend-api@0.2.0
|
|
74
|
+
- @checkstack/common@0.3.0
|
|
75
|
+
- @checkstack/auth-frontend@0.3.0
|
|
76
|
+
- @checkstack/catalog-common@1.2.0
|
|
77
|
+
- @checkstack/healthcheck-common@0.4.0
|
|
78
|
+
- @checkstack/ui@0.2.1
|
|
79
|
+
- @checkstack/signal-frontend@0.0.7
|
|
80
|
+
|
|
3
81
|
## 0.3.0
|
|
4
82
|
|
|
5
83
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"scripts": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"@checkstack/auth-frontend": "workspace:*",
|
|
13
13
|
"@checkstack/catalog-common": "workspace:*",
|
|
14
14
|
"@checkstack/common": "workspace:*",
|
|
15
|
+
"@checkstack/dashboard-frontend": "workspace:*",
|
|
15
16
|
"@checkstack/frontend-api": "workspace:*",
|
|
16
17
|
"@checkstack/healthcheck-common": "workspace:*",
|
|
17
18
|
"@checkstack/signal-frontend": "workspace:*",
|
package/src/api.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { createApiRef } from "@checkstack/frontend-api";
|
|
2
|
-
import { HealthCheckApi } from "@checkstack/healthcheck-common";
|
|
3
|
-
import type { InferClient } from "@checkstack/common";
|
|
4
|
-
|
|
5
1
|
// Re-export types for convenience
|
|
6
2
|
export type {
|
|
7
3
|
HealthCheckConfiguration,
|
|
@@ -9,9 +5,5 @@ export type {
|
|
|
9
5
|
HealthCheckRun,
|
|
10
6
|
HealthCheckRunPublic,
|
|
11
7
|
} from "@checkstack/healthcheck-common";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export type HealthCheckApiClient = InferClient<typeof HealthCheckApi>;
|
|
15
|
-
|
|
16
|
-
export const healthCheckApiRef =
|
|
17
|
-
createApiRef<HealthCheckApiClient>("healthcheck-api");
|
|
8
|
+
// Client definition is in @checkstack/healthcheck-common - use with usePluginClient
|
|
9
|
+
export { HealthCheckApi } from "@checkstack/healthcheck-common";
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useEffect, useState } from "react";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
11
|
+
import { HealthCheckApi } from "../api";
|
|
12
12
|
|
|
13
13
|
interface StrategySchemas {
|
|
14
14
|
resultSchema: Record<string, unknown> | undefined;
|
|
@@ -28,59 +28,50 @@ export function useStrategySchemas(strategyId: string): {
|
|
|
28
28
|
schemas: StrategySchemas | undefined;
|
|
29
29
|
loading: boolean;
|
|
30
30
|
} {
|
|
31
|
-
const
|
|
31
|
+
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
32
32
|
const [schemas, setSchemas] = useState<StrategySchemas | undefined>();
|
|
33
33
|
const [loading, setLoading] = useState(true);
|
|
34
34
|
|
|
35
|
+
// Fetch strategies with useQuery
|
|
36
|
+
const { data: strategies } = healthCheckClient.getStrategies.useQuery({});
|
|
37
|
+
|
|
38
|
+
// Fetch collectors with useQuery
|
|
39
|
+
const { data: collectors } = healthCheckClient.getCollectors.useQuery(
|
|
40
|
+
{ strategyId },
|
|
41
|
+
{ enabled: !!strategyId }
|
|
42
|
+
);
|
|
43
|
+
|
|
35
44
|
useEffect(() => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
async function fetchSchemas() {
|
|
39
|
-
try {
|
|
40
|
-
// Fetch strategy and collectors in parallel
|
|
41
|
-
const [strategies, collectors] = await Promise.all([
|
|
42
|
-
api.getStrategies(),
|
|
43
|
-
api.getCollectors({ strategyId }),
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
const strategy = strategies.find((s) => s.id === strategyId);
|
|
47
|
-
|
|
48
|
-
if (!cancelled && strategy) {
|
|
49
|
-
// Build collector schemas object for nesting under resultSchema.properties.collectors
|
|
50
|
-
const collectorProperties: Record<string, unknown> = {};
|
|
51
|
-
for (const collector of collectors) {
|
|
52
|
-
// Use full ID so it matches stored data keys like "healthcheck-http.request"
|
|
53
|
-
collectorProperties[collector.id] = collector.resultSchema;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Merge collector schemas into strategy result schema
|
|
57
|
-
const mergedResultSchema = mergeCollectorSchemas(
|
|
58
|
-
strategy.resultSchema as Record<string, unknown> | undefined,
|
|
59
|
-
collectorProperties
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
setSchemas({
|
|
63
|
-
resultSchema: mergedResultSchema,
|
|
64
|
-
aggregatedResultSchema:
|
|
65
|
-
(strategy.aggregatedResultSchema as Record<string, unknown>) ??
|
|
66
|
-
undefined,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
} catch (error) {
|
|
70
|
-
console.error("Failed to fetch strategy schemas:", error);
|
|
71
|
-
} finally {
|
|
72
|
-
if (!cancelled) {
|
|
73
|
-
setLoading(false);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
45
|
+
if (!strategies || !collectors) {
|
|
46
|
+
return;
|
|
76
47
|
}
|
|
77
48
|
|
|
78
|
-
|
|
49
|
+
const strategy = strategies.find((s) => s.id === strategyId);
|
|
79
50
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
51
|
+
if (strategy) {
|
|
52
|
+
// Build collector schemas object for nesting under resultSchema.properties.collectors
|
|
53
|
+
const collectorProperties: Record<string, unknown> = {};
|
|
54
|
+
for (const collector of collectors) {
|
|
55
|
+
// Use full ID so it matches stored data keys like "healthcheck-http.request"
|
|
56
|
+
collectorProperties[collector.id] = collector.resultSchema;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Merge collector schemas into strategy result schema
|
|
60
|
+
const mergedResultSchema = mergeCollectorSchemas(
|
|
61
|
+
strategy.resultSchema as Record<string, unknown> | undefined,
|
|
62
|
+
collectorProperties
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
setSchemas({
|
|
66
|
+
resultSchema: mergedResultSchema,
|
|
67
|
+
aggregatedResultSchema:
|
|
68
|
+
(strategy.aggregatedResultSchema as Record<string, unknown>) ??
|
|
69
|
+
undefined,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setLoading(false);
|
|
74
|
+
}, [strategies, collectors, strategyId]);
|
|
84
75
|
|
|
85
76
|
return { schemas, loading };
|
|
86
77
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { healthCheckApiRef, HealthCheckRunPublic } from "../api";
|
|
1
|
+
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
2
|
+
import { HealthCheckApi } from "../api";
|
|
4
3
|
import { SystemDetailsSlot } from "@checkstack/catalog-common";
|
|
5
4
|
import {
|
|
6
5
|
Table,
|
|
@@ -25,18 +24,15 @@ export const HealthCheckHistory: React.FC<SlotProps> = (props) => {
|
|
|
25
24
|
const { system, configurationId, limit } = props as Props;
|
|
26
25
|
const systemId = system?.id;
|
|
27
26
|
|
|
28
|
-
const
|
|
29
|
-
const [history, setHistory] = useState<HealthCheckRunPublic[]>([]);
|
|
30
|
-
const [loading, setLoading] = useState(true);
|
|
27
|
+
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}, [healthCheckApi, systemId, configurationId, limit]);
|
|
29
|
+
// Fetch history with useQuery
|
|
30
|
+
const { data, isLoading: loading } = healthCheckClient.getHistory.useQuery(
|
|
31
|
+
{ systemId, configurationId, limit },
|
|
32
|
+
{ enabled: true }
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const history = data?.runs ?? [];
|
|
40
36
|
|
|
41
37
|
if (loading) return <LoadingSpinner />;
|
|
42
38
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import React, { useState, useCallback } from "react";
|
|
2
|
+
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
3
3
|
import { useSignal } from "@checkstack/signal-frontend";
|
|
4
|
-
import { healthCheckApiRef } from "../api";
|
|
5
4
|
import { SystemDetailsSlot } from "@checkstack/catalog-common";
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
HEALTH_CHECK_RUN_COMPLETED,
|
|
7
|
+
HealthCheckApi,
|
|
8
|
+
} from "@checkstack/healthcheck-common";
|
|
7
9
|
import {
|
|
8
10
|
HealthBadge,
|
|
9
11
|
LoadingSpinner,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
Tooltip,
|
|
17
19
|
Pagination,
|
|
18
20
|
usePagination,
|
|
21
|
+
usePaginationSync,
|
|
19
22
|
DateRangeFilter,
|
|
20
23
|
} from "@checkstack/ui";
|
|
21
24
|
import { formatDistanceToNow } from "date-fns";
|
|
@@ -50,7 +53,7 @@ interface ExpandedRowProps {
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
53
|
-
const
|
|
56
|
+
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
54
57
|
|
|
55
58
|
// Date range state for filtering
|
|
56
59
|
const [dateRange, setDateRange] = useState<{
|
|
@@ -79,42 +82,33 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
79
82
|
limit: 1000,
|
|
80
83
|
});
|
|
81
84
|
|
|
82
|
-
//
|
|
85
|
+
// Pagination state for history table
|
|
86
|
+
const pagination = usePagination({ defaultLimit: 10 });
|
|
87
|
+
|
|
88
|
+
// Fetch paginated history with useQuery
|
|
83
89
|
const {
|
|
84
|
-
|
|
85
|
-
loading,
|
|
86
|
-
|
|
87
|
-
} =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}) =>
|
|
95
|
-
api.getHistory({
|
|
96
|
-
systemId: params.systemId,
|
|
97
|
-
configurationId: params.configurationId,
|
|
98
|
-
limit: params.limit,
|
|
99
|
-
offset: params.offset,
|
|
100
|
-
startDate: params.startDate,
|
|
101
|
-
// Don't pass endDate - backend defaults to 'now' so new runs are included
|
|
102
|
-
}),
|
|
103
|
-
getItems: (response) => response.runs,
|
|
104
|
-
getTotal: (response) => response.total,
|
|
105
|
-
extraParams: {
|
|
106
|
-
systemId,
|
|
107
|
-
configurationId: item.configurationId,
|
|
108
|
-
startDate: dateRange.startDate,
|
|
109
|
-
},
|
|
110
|
-
defaultLimit: 10,
|
|
90
|
+
data: historyData,
|
|
91
|
+
isLoading: loading,
|
|
92
|
+
refetch,
|
|
93
|
+
} = healthCheckClient.getHistory.useQuery({
|
|
94
|
+
systemId,
|
|
95
|
+
configurationId: item.configurationId,
|
|
96
|
+
limit: pagination.limit,
|
|
97
|
+
offset: pagination.offset,
|
|
98
|
+
startDate: dateRange.startDate,
|
|
99
|
+
// Don't pass endDate - backend defaults to 'now' so new runs are included
|
|
111
100
|
});
|
|
112
101
|
|
|
102
|
+
// Sync total from response
|
|
103
|
+
usePaginationSync(pagination, historyData?.total);
|
|
104
|
+
|
|
105
|
+
const runs = historyData?.runs ?? [];
|
|
106
|
+
|
|
113
107
|
// Listen for realtime health check updates to refresh history table
|
|
114
108
|
// Charts are refreshed automatically by useHealthCheckData
|
|
115
109
|
useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
|
|
116
110
|
if (changedId === systemId) {
|
|
117
|
-
|
|
111
|
+
void refetch();
|
|
118
112
|
}
|
|
119
113
|
});
|
|
120
114
|
|
|
@@ -248,100 +242,49 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
|
|
|
248
242
|
|
|
249
243
|
export function HealthCheckSystemOverview(props: SlotProps) {
|
|
250
244
|
const systemId = props.system.id;
|
|
251
|
-
const
|
|
245
|
+
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
252
246
|
|
|
253
|
-
// Fetch health check overview
|
|
254
|
-
const [overview, setOverview] = React.useState<HealthCheckOverviewItem[]>([]);
|
|
255
|
-
const [initialLoading, setInitialLoading] = React.useState(true);
|
|
256
247
|
const [expandedRow, setExpandedRow] = React.useState<string | undefined>();
|
|
257
248
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
state: check.status,
|
|
266
|
-
intervalSeconds: check.intervalSeconds,
|
|
267
|
-
lastRunAt: check.recentRuns[0]?.timestamp
|
|
268
|
-
? new Date(check.recentRuns[0].timestamp)
|
|
269
|
-
: undefined,
|
|
270
|
-
stateThresholds: check.stateThresholds,
|
|
271
|
-
recentStatusHistory: check.recentRuns.map((r) => r.status),
|
|
272
|
-
}))
|
|
273
|
-
);
|
|
274
|
-
setInitialLoading(false);
|
|
275
|
-
});
|
|
276
|
-
}, [api, systemId]);
|
|
277
|
-
|
|
278
|
-
React.useEffect(() => {
|
|
279
|
-
fetchOverview();
|
|
280
|
-
}, [fetchOverview]);
|
|
281
|
-
|
|
282
|
-
// Listen for realtime health check updates - merge into existing state to avoid remounting expanded content
|
|
283
|
-
useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
|
|
284
|
-
if (changedId === systemId) {
|
|
285
|
-
// Fetch fresh data but merge it into existing state to preserve object identity
|
|
286
|
-
// for unchanged items, preventing unnecessary re-renders of expanded content
|
|
287
|
-
api.getSystemHealthOverview({ systemId }).then((data) => {
|
|
288
|
-
setOverview((prev) => {
|
|
289
|
-
// Create a map of new items for quick lookup
|
|
290
|
-
const newItemsMap = new Map(
|
|
291
|
-
data.checks.map((item) => [item.configurationId, item])
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
// Update existing items in place, add new ones
|
|
295
|
-
const merged = prev.map((existing) => {
|
|
296
|
-
const updated = newItemsMap.get(existing.configurationId);
|
|
297
|
-
if (updated) {
|
|
298
|
-
newItemsMap.delete(existing.configurationId);
|
|
299
|
-
// Map API response to our internal format
|
|
300
|
-
const mappedItem: HealthCheckOverviewItem = {
|
|
301
|
-
configurationId: updated.configurationId,
|
|
302
|
-
strategyId: updated.strategyId,
|
|
303
|
-
name: updated.configurationName,
|
|
304
|
-
state: updated.status,
|
|
305
|
-
intervalSeconds: updated.intervalSeconds,
|
|
306
|
-
lastRunAt: updated.recentRuns[0]?.timestamp
|
|
307
|
-
? new Date(updated.recentRuns[0].timestamp)
|
|
308
|
-
: undefined,
|
|
309
|
-
stateThresholds: updated.stateThresholds,
|
|
310
|
-
recentStatusHistory: updated.recentRuns.map((r) => r.status),
|
|
311
|
-
};
|
|
312
|
-
// Return updated data but preserve reference if nothing changed
|
|
313
|
-
return JSON.stringify(existing) === JSON.stringify(mappedItem)
|
|
314
|
-
? existing
|
|
315
|
-
: mappedItem;
|
|
316
|
-
}
|
|
317
|
-
return existing;
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// Add any new items that weren't in the previous list
|
|
321
|
-
for (const newItem of newItemsMap.values()) {
|
|
322
|
-
merged.push({
|
|
323
|
-
configurationId: newItem.configurationId,
|
|
324
|
-
strategyId: newItem.strategyId,
|
|
325
|
-
name: newItem.configurationName,
|
|
326
|
-
state: newItem.status,
|
|
327
|
-
intervalSeconds: newItem.intervalSeconds,
|
|
328
|
-
lastRunAt: newItem.recentRuns[0]?.timestamp
|
|
329
|
-
? new Date(newItem.recentRuns[0].timestamp)
|
|
330
|
-
: undefined,
|
|
331
|
-
stateThresholds: newItem.stateThresholds,
|
|
332
|
-
recentStatusHistory: newItem.recentRuns.map((r) => r.status),
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Remove items that no longer exist
|
|
337
|
-
return merged.filter((item) =>
|
|
338
|
-
data.checks.some((c) => c.configurationId === item.configurationId)
|
|
339
|
-
);
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
}
|
|
249
|
+
// Fetch health check overview using useQuery
|
|
250
|
+
const {
|
|
251
|
+
data: overviewData,
|
|
252
|
+
isLoading: initialLoading,
|
|
253
|
+
refetch,
|
|
254
|
+
} = healthCheckClient.getSystemHealthOverview.useQuery({
|
|
255
|
+
systemId,
|
|
343
256
|
});
|
|
344
257
|
|
|
258
|
+
// Transform API response to component format
|
|
259
|
+
const overview: HealthCheckOverviewItem[] = React.useMemo(() => {
|
|
260
|
+
if (!overviewData) return [];
|
|
261
|
+
return overviewData.checks.map((check) => ({
|
|
262
|
+
configurationId: check.configurationId,
|
|
263
|
+
strategyId: check.strategyId,
|
|
264
|
+
name: check.configurationName,
|
|
265
|
+
state: check.status,
|
|
266
|
+
intervalSeconds: check.intervalSeconds,
|
|
267
|
+
lastRunAt: check.recentRuns[0]?.timestamp
|
|
268
|
+
? new Date(check.recentRuns[0].timestamp)
|
|
269
|
+
: undefined,
|
|
270
|
+
stateThresholds: check.stateThresholds,
|
|
271
|
+
recentStatusHistory: check.recentRuns.map((r) => r.status),
|
|
272
|
+
}));
|
|
273
|
+
}, [overviewData]);
|
|
274
|
+
|
|
275
|
+
// Listen for realtime health check updates to refresh overview
|
|
276
|
+
useSignal(
|
|
277
|
+
HEALTH_CHECK_RUN_COMPLETED,
|
|
278
|
+
useCallback(
|
|
279
|
+
({ systemId: changedId }) => {
|
|
280
|
+
if (changedId === systemId) {
|
|
281
|
+
void refetch();
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
[systemId, refetch]
|
|
285
|
+
)
|
|
286
|
+
);
|
|
287
|
+
|
|
345
288
|
if (initialLoading) {
|
|
346
289
|
return <LoadingSpinner />;
|
|
347
290
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import {
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
|
|
3
3
|
import { useSignal } from "@checkstack/signal-frontend";
|
|
4
4
|
import { SystemStateBadgesSlot } from "@checkstack/catalog-common";
|
|
5
5
|
import { HEALTH_CHECK_RUN_COMPLETED } from "@checkstack/healthcheck-common";
|
|
6
|
-
import {
|
|
7
|
-
import { HealthBadge
|
|
6
|
+
import { HealthCheckApi } from "../api";
|
|
7
|
+
import { HealthBadge } from "@checkstack/ui";
|
|
8
|
+
import { useSystemBadgeDataOptional } from "@checkstack/dashboard-frontend";
|
|
8
9
|
|
|
9
10
|
type Props = SlotContext<typeof SystemStateBadgesSlot>;
|
|
10
11
|
|
|
@@ -12,35 +13,43 @@ type Props = SlotContext<typeof SystemStateBadgesSlot>;
|
|
|
12
13
|
* Displays a health badge for a system based on its health check results.
|
|
13
14
|
* Uses the backend's getSystemHealthStatus endpoint which evaluates
|
|
14
15
|
* health status based on configured state thresholds.
|
|
16
|
+
*
|
|
17
|
+
* When rendered within SystemBadgeDataProvider, uses bulk-fetched data.
|
|
18
|
+
* Otherwise, falls back to individual fetch.
|
|
19
|
+
*
|
|
15
20
|
* Listens for realtime updates via signals.
|
|
16
21
|
*/
|
|
17
22
|
export const SystemHealthBadge: React.FC<Props> = ({ system }) => {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
const healthCheckClient = usePluginClient(HealthCheckApi);
|
|
24
|
+
const badgeData = useSystemBadgeDataOptional();
|
|
25
|
+
|
|
26
|
+
// Try to get data from provider first
|
|
27
|
+
const providerData = badgeData?.getSystemBadgeData(system?.id ?? "");
|
|
28
|
+
const providerStatus = providerData?.health?.status;
|
|
29
|
+
|
|
30
|
+
// Query for health status if not using provider
|
|
31
|
+
// When badgeData exists (inside provider), this query is disabled
|
|
32
|
+
const { data: healthData, refetch } =
|
|
33
|
+
healthCheckClient.getSystemHealthStatus.useQuery(
|
|
34
|
+
{ systemId: system?.id ?? "" },
|
|
35
|
+
{
|
|
36
|
+
enabled: !badgeData && !!system?.id,
|
|
37
|
+
staleTime: 30_000, // Prevent unnecessary refetches
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const localStatus = healthData?.status;
|
|
42
|
+
|
|
43
|
+
// Listen for realtime health check updates (only in fallback mode)
|
|
38
44
|
useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
|
|
39
|
-
if (changedId === system?.id) {
|
|
40
|
-
refetch();
|
|
45
|
+
if (!badgeData && changedId === system?.id) {
|
|
46
|
+
void refetch();
|
|
41
47
|
}
|
|
42
48
|
});
|
|
43
49
|
|
|
44
|
-
if
|
|
50
|
+
// Use provider data if available, otherwise use local state
|
|
51
|
+
const status = providerStatus ?? localStatus;
|
|
52
|
+
|
|
53
|
+
if (!status) return <></>;
|
|
45
54
|
return <HealthBadge status={status} />;
|
|
46
55
|
};
|