@checkstack/healthcheck-frontend 0.2.0 → 0.4.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 +171 -0
- package/package.json +2 -1
- package/src/api.ts +2 -10
- package/src/auto-charts/schema-parser.ts +1 -1
- package/src/auto-charts/useStrategySchemas.ts +39 -48
- package/src/components/AssertionBuilder.tsx +8 -0
- package/src/components/HealthCheckDiagram.tsx +6 -6
- package/src/components/HealthCheckHistory.tsx +10 -14
- package/src/components/HealthCheckMenuItems.tsx +4 -7
- package/src/components/HealthCheckSystemOverview.tsx +66 -123
- package/src/components/SystemHealthBadge.tsx +36 -27
- package/src/components/SystemHealthCheckAssignment.tsx +196 -215
- package/src/hooks/useCollectors.ts +21 -34
- package/src/hooks/useHealthCheckData.ts +104 -153
- package/src/index.tsx +6 -20
- package/src/pages/HealthCheckConfigPage.tsx +70 -46
- package/src/pages/HealthCheckHistoryDetailPage.tsx +32 -28
- package/src/pages/HealthCheckHistoryPage.tsx +27 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,176 @@
|
|
|
1
1
|
# @checkstack/healthcheck-frontend
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7a23261: ## TanStack Query Integration
|
|
8
|
+
|
|
9
|
+
Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
|
|
10
|
+
|
|
11
|
+
### New Features
|
|
12
|
+
|
|
13
|
+
- **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
|
|
14
|
+
- **Automatic request deduplication**: Multiple components requesting the same data share a single network request
|
|
15
|
+
- **Built-in caching**: Configurable stale time and cache duration per query
|
|
16
|
+
- **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
|
|
17
|
+
- **Background refetching**: Stale data is automatically refreshed when components mount
|
|
18
|
+
|
|
19
|
+
### Contract Changes
|
|
20
|
+
|
|
21
|
+
All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const getItems = proc()
|
|
25
|
+
.meta({ operationType: "query", access: [access.read] })
|
|
26
|
+
.output(z.array(itemSchema))
|
|
27
|
+
.query();
|
|
28
|
+
|
|
29
|
+
const createItem = proc()
|
|
30
|
+
.meta({ operationType: "mutation", access: [access.manage] })
|
|
31
|
+
.input(createItemSchema)
|
|
32
|
+
.output(itemSchema)
|
|
33
|
+
.mutation();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Migration
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Before (forPlugin pattern)
|
|
40
|
+
const api = useApi(myPluginApiRef);
|
|
41
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
api.getItems().then(setItems);
|
|
44
|
+
}, [api]);
|
|
45
|
+
|
|
46
|
+
// After (usePluginClient pattern)
|
|
47
|
+
const client = usePluginClient(MyPluginApi);
|
|
48
|
+
const { data: items, isLoading } = client.getItems.useQuery({});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Bug Fixes
|
|
52
|
+
|
|
53
|
+
- Fixed `rpc.test.ts` test setup for middleware type inference
|
|
54
|
+
- Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
|
|
55
|
+
- Fixed null→undefined warnings in notification and queue frontends
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- Updated dependencies [180be38]
|
|
60
|
+
- Updated dependencies [7a23261]
|
|
61
|
+
- @checkstack/dashboard-frontend@0.2.0
|
|
62
|
+
- @checkstack/frontend-api@0.2.0
|
|
63
|
+
- @checkstack/common@0.3.0
|
|
64
|
+
- @checkstack/auth-frontend@0.3.0
|
|
65
|
+
- @checkstack/catalog-common@1.2.0
|
|
66
|
+
- @checkstack/healthcheck-common@0.4.0
|
|
67
|
+
- @checkstack/ui@0.2.1
|
|
68
|
+
- @checkstack/signal-frontend@0.0.7
|
|
69
|
+
|
|
70
|
+
## 0.3.0
|
|
71
|
+
|
|
72
|
+
### Minor Changes
|
|
73
|
+
|
|
74
|
+
- 9faec1f: # Unified AccessRule Terminology Refactoring
|
|
75
|
+
|
|
76
|
+
This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
|
|
77
|
+
|
|
78
|
+
## Changes
|
|
79
|
+
|
|
80
|
+
### Core Infrastructure (`@checkstack/common`)
|
|
81
|
+
|
|
82
|
+
- Introduced `AccessRule` interface as the primary access control type
|
|
83
|
+
- Added `accessPair()` helper for creating read/manage access rule pairs
|
|
84
|
+
- Added `access()` builder for individual access rules
|
|
85
|
+
- Replaced `Permission` type with `AccessRule` throughout
|
|
86
|
+
|
|
87
|
+
### API Changes
|
|
88
|
+
|
|
89
|
+
- `env.registerPermissions()` → `env.registerAccessRules()`
|
|
90
|
+
- `meta.permissions` → `meta.access` in RPC contracts
|
|
91
|
+
- `usePermission()` → `useAccess()` in frontend hooks
|
|
92
|
+
- Route `permission:` field → `accessRule:` field
|
|
93
|
+
|
|
94
|
+
### UI Changes
|
|
95
|
+
|
|
96
|
+
- "Roles & Permissions" tab → "Roles & Access Rules"
|
|
97
|
+
- "You don't have permission..." → "You don't have access..."
|
|
98
|
+
- All permission-related UI text updated
|
|
99
|
+
|
|
100
|
+
### Documentation & Templates
|
|
101
|
+
|
|
102
|
+
- Updated 18 documentation files with AccessRule terminology
|
|
103
|
+
- Updated 7 scaffolding templates with `accessPair()` pattern
|
|
104
|
+
- All code examples use new AccessRule API
|
|
105
|
+
|
|
106
|
+
## Migration Guide
|
|
107
|
+
|
|
108
|
+
### Backend Plugins
|
|
109
|
+
|
|
110
|
+
```diff
|
|
111
|
+
- import { permissionList } from "./permissions";
|
|
112
|
+
- env.registerPermissions(permissionList);
|
|
113
|
+
+ import { accessRules } from "./access";
|
|
114
|
+
+ env.registerAccessRules(accessRules);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### RPC Contracts
|
|
118
|
+
|
|
119
|
+
```diff
|
|
120
|
+
- .meta({ userType: "user", permissions: [permissions.read.id] })
|
|
121
|
+
+ .meta({ userType: "user", access: [access.read] })
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Frontend Hooks
|
|
125
|
+
|
|
126
|
+
```diff
|
|
127
|
+
- const canRead = accessApi.usePermission(permissions.read.id);
|
|
128
|
+
+ const canRead = accessApi.useAccess(access.read);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Routes
|
|
132
|
+
|
|
133
|
+
```diff
|
|
134
|
+
- permission: permissions.entityRead.id,
|
|
135
|
+
+ accessRule: access.read,
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- 827b286: Add array assertion operators for string array fields
|
|
139
|
+
|
|
140
|
+
New operators for asserting on array fields (e.g., playerNames in RCON collectors):
|
|
141
|
+
|
|
142
|
+
- **includes** - Check if array contains a specific value
|
|
143
|
+
- **notIncludes** - Check if array does NOT contain a specific value
|
|
144
|
+
- **lengthEquals** - Check if array length equals a value
|
|
145
|
+
- **lengthGreaterThan** - Check if array length is greater than a value
|
|
146
|
+
- **lengthLessThan** - Check if array length is less than a value
|
|
147
|
+
- **isEmpty** - Check if array is empty
|
|
148
|
+
- **isNotEmpty** - Check if array has at least one element
|
|
149
|
+
|
|
150
|
+
Also exports a new `arrayField()` schema factory for creating array assertion schemas.
|
|
151
|
+
|
|
152
|
+
### Patch Changes
|
|
153
|
+
|
|
154
|
+
- f533141: Enforce health result factory function usage via branded types
|
|
155
|
+
|
|
156
|
+
- Added `healthResultSchema()` builder that enforces the use of factory functions at compile-time
|
|
157
|
+
- Added `healthResultArray()` factory for array fields (e.g., DNS resolved values)
|
|
158
|
+
- Added branded `HealthResultField<T>` type to mark schemas created by factory functions
|
|
159
|
+
- Consolidated `ChartType` and `HealthResultMeta` into `@checkstack/common` as single source of truth
|
|
160
|
+
- Updated all 12 health check strategies and 11 collectors to use `healthResultSchema()`
|
|
161
|
+
- Using raw `z.number()` etc. inside `healthResultSchema()` now causes a TypeScript error
|
|
162
|
+
|
|
163
|
+
- Updated dependencies [9faec1f]
|
|
164
|
+
- Updated dependencies [95eeec7]
|
|
165
|
+
- Updated dependencies [f533141]
|
|
166
|
+
- @checkstack/auth-frontend@0.2.0
|
|
167
|
+
- @checkstack/catalog-common@1.1.0
|
|
168
|
+
- @checkstack/common@0.2.0
|
|
169
|
+
- @checkstack/frontend-api@0.1.0
|
|
170
|
+
- @checkstack/healthcheck-common@0.3.0
|
|
171
|
+
- @checkstack/ui@0.2.0
|
|
172
|
+
- @checkstack/signal-frontend@0.0.6
|
|
173
|
+
|
|
3
174
|
## 0.2.0
|
|
4
175
|
|
|
5
176
|
### 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.0",
|
|
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
|
}
|
|
@@ -81,6 +81,13 @@ const OPERATORS: Record<FieldType, { value: string; label: string }[]> = {
|
|
|
81
81
|
],
|
|
82
82
|
enum: [{ value: "equals", label: "Equals" }],
|
|
83
83
|
array: [
|
|
84
|
+
{ value: "includes", label: "Includes" },
|
|
85
|
+
{ value: "notIncludes", label: "Not Includes" },
|
|
86
|
+
{ value: "lengthEquals", label: "Length Equals" },
|
|
87
|
+
{ value: "lengthGreaterThan", label: "Length Greater Than" },
|
|
88
|
+
{ value: "lengthLessThan", label: "Length Less Than" },
|
|
89
|
+
{ value: "isEmpty", label: "Is Empty" },
|
|
90
|
+
{ value: "isNotEmpty", label: "Is Not Empty" },
|
|
84
91
|
{ value: "exists", label: "Exists" },
|
|
85
92
|
{ value: "notExists", label: "Not Exists" },
|
|
86
93
|
],
|
|
@@ -98,6 +105,7 @@ const OPERATORS: Record<FieldType, { value: string; label: string }[]> = {
|
|
|
98
105
|
// Operators that don't need a value input
|
|
99
106
|
const VALUE_LESS_OPERATORS = new Set([
|
|
100
107
|
"isEmpty",
|
|
108
|
+
"isNotEmpty",
|
|
101
109
|
"isTrue",
|
|
102
110
|
"isFalse",
|
|
103
111
|
"exists",
|
|
@@ -44,20 +44,20 @@ export function HealthCheckDiagram({
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Wrapper that shows
|
|
47
|
+
* Wrapper that shows access message when user lacks access.
|
|
48
48
|
*/
|
|
49
|
-
export function
|
|
50
|
-
|
|
49
|
+
export function HealthCheckDiagramAccessGate({
|
|
50
|
+
hasAccess,
|
|
51
51
|
children,
|
|
52
52
|
}: {
|
|
53
|
-
|
|
53
|
+
hasAccess: boolean;
|
|
54
54
|
children: React.ReactNode;
|
|
55
55
|
}) {
|
|
56
|
-
if (!
|
|
56
|
+
if (!hasAccess) {
|
|
57
57
|
return (
|
|
58
58
|
<InfoBanner variant="info">
|
|
59
59
|
Additional strategy-specific visualizations are available with the
|
|
60
|
-
"Read Health Check Details"
|
|
60
|
+
"Read Health Check Details" access rule.
|
|
61
61
|
</InfoBanner>
|
|
62
62
|
);
|
|
63
63
|
}
|
|
@@ -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
|
|
|
@@ -3,20 +3,17 @@ import { Link } from "react-router-dom";
|
|
|
3
3
|
import { Activity } from "lucide-react";
|
|
4
4
|
import type { UserMenuItemsContext } from "@checkstack/frontend-api";
|
|
5
5
|
import { DropdownMenuItem } from "@checkstack/ui";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveRoute } from "@checkstack/common";
|
|
7
7
|
import {
|
|
8
8
|
healthcheckRoutes,
|
|
9
|
-
|
|
9
|
+
healthCheckAccess,
|
|
10
10
|
pluginMetadata,
|
|
11
11
|
} from "@checkstack/healthcheck-common";
|
|
12
12
|
|
|
13
13
|
export const HealthCheckMenuItems = ({
|
|
14
|
-
|
|
14
|
+
accessRules: userPerms,
|
|
15
15
|
}: UserMenuItemsContext) => {
|
|
16
|
-
const qualifiedId =
|
|
17
|
-
pluginMetadata,
|
|
18
|
-
permissions.healthCheckRead
|
|
19
|
-
);
|
|
16
|
+
const qualifiedId = `${pluginMetadata.pluginId}.${healthCheckAccess.configuration.read.id}`;
|
|
20
17
|
const canRead = userPerms.includes("*") || userPerms.includes(qualifiedId);
|
|
21
18
|
|
|
22
19
|
if (!canRead) {
|
|
@@ -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
|
}
|