@checkstack/healthcheck-frontend 0.0.3 → 0.2.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 +144 -0
- package/package.json +3 -2
- package/src/auto-charts/AutoChartGrid.tsx +256 -23
- package/src/auto-charts/schema-parser.ts +147 -41
- package/src/auto-charts/useStrategySchemas.ts +73 -3
- package/src/components/AssertionBuilder.tsx +432 -0
- package/src/components/CollectorList.tsx +309 -0
- package/src/components/HealthCheckEditor.tsx +54 -8
- package/src/components/HealthCheckRunsTable.tsx +142 -6
- package/src/components/SystemHealthCheckAssignment.tsx +9 -4
- package/src/hooks/useCollectors.ts +63 -0
- package/src/pages/HealthCheckConfigPage.tsx +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,149 @@
|
|
|
1
1
|
# @checkstack/healthcheck-frontend
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8e43507: # Teams and Resource-Level Access Control
|
|
8
|
+
|
|
9
|
+
This release introduces a comprehensive Teams system for organizing users and controlling access to resources at a granular level.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
### Team Management
|
|
14
|
+
|
|
15
|
+
- Create, update, and delete teams with name and description
|
|
16
|
+
- Add/remove users from teams
|
|
17
|
+
- Designate team managers with elevated privileges
|
|
18
|
+
- View team membership and manager status
|
|
19
|
+
|
|
20
|
+
### Resource-Level Access Control
|
|
21
|
+
|
|
22
|
+
- Grant teams access to specific resources (systems, health checks, incidents, maintenances)
|
|
23
|
+
- Configure read-only or manage permissions per team
|
|
24
|
+
- Resource-level "Team Only" mode that restricts access exclusively to team members
|
|
25
|
+
- Separate `resourceAccessSettings` table for resource-level settings (not per-grant)
|
|
26
|
+
- Automatic cleanup of grants when teams are deleted (database cascade)
|
|
27
|
+
|
|
28
|
+
### Middleware Integration
|
|
29
|
+
|
|
30
|
+
- Extended `autoAuthMiddleware` to support resource access checks
|
|
31
|
+
- Single-resource pre-handler validation for detail endpoints
|
|
32
|
+
- Automatic list filtering for collection endpoints
|
|
33
|
+
- S2S endpoints for access verification
|
|
34
|
+
|
|
35
|
+
### Frontend Components
|
|
36
|
+
|
|
37
|
+
- `TeamsTab` component for managing teams in Auth Settings
|
|
38
|
+
- `TeamAccessEditor` component for assigning team access to resources
|
|
39
|
+
- Resource-level "Team Only" toggle in `TeamAccessEditor`
|
|
40
|
+
- Integration into System, Health Check, Incident, and Maintenance editors
|
|
41
|
+
|
|
42
|
+
## Breaking Changes
|
|
43
|
+
|
|
44
|
+
### API Response Format Changes
|
|
45
|
+
|
|
46
|
+
List endpoints now return objects with named keys instead of arrays directly:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Before
|
|
50
|
+
const systems = await catalogApi.getSystems();
|
|
51
|
+
|
|
52
|
+
// After
|
|
53
|
+
const { systems } = await catalogApi.getSystems();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Affected endpoints:
|
|
57
|
+
|
|
58
|
+
- `catalog.getSystems` → `{ systems: [...] }`
|
|
59
|
+
- `healthcheck.getConfigurations` → `{ configurations: [...] }`
|
|
60
|
+
- `incident.listIncidents` → `{ incidents: [...] }`
|
|
61
|
+
- `maintenance.listMaintenances` → `{ maintenances: [...] }`
|
|
62
|
+
|
|
63
|
+
### User Identity Enrichment
|
|
64
|
+
|
|
65
|
+
`RealUser` and `ApplicationUser` types now include `teamIds: string[]` field with team memberships.
|
|
66
|
+
|
|
67
|
+
## Documentation
|
|
68
|
+
|
|
69
|
+
See `docs/backend/teams.md` for complete API reference and integration guide.
|
|
70
|
+
|
|
71
|
+
- 97c5a6b: Add UUID-based collector identification for better multiple collector support
|
|
72
|
+
|
|
73
|
+
**Breaking Change**: Existing health check configurations with collectors need to be recreated.
|
|
74
|
+
|
|
75
|
+
- Each collector instance now has a unique UUID assigned on creation
|
|
76
|
+
- Collector results are stored under the UUID key with `_collectorId` and `_assertionFailed` metadata
|
|
77
|
+
- Auto-charts correctly display separate charts for each collector instance
|
|
78
|
+
- Charts are now grouped by collector instance with clear headings
|
|
79
|
+
- Assertion status card shows pass/fail for each collector
|
|
80
|
+
- Renamed "Success" to "HTTP Success" to clarify it's about HTTP request success
|
|
81
|
+
- Fixed deletion of collectors not persisting to database
|
|
82
|
+
- Fixed duplicate React key warnings in auto-chart grid
|
|
83
|
+
|
|
84
|
+
### Patch Changes
|
|
85
|
+
|
|
86
|
+
- 97c5a6b: Fix Radix UI accessibility warning in dialog components by adding visually hidden DialogDescription components
|
|
87
|
+
- Updated dependencies [8e43507]
|
|
88
|
+
- Updated dependencies [97c5a6b]
|
|
89
|
+
- Updated dependencies [97c5a6b]
|
|
90
|
+
- Updated dependencies [8e43507]
|
|
91
|
+
- Updated dependencies [8e43507]
|
|
92
|
+
- Updated dependencies [97c5a6b]
|
|
93
|
+
- @checkstack/ui@0.1.0
|
|
94
|
+
- @checkstack/auth-frontend@0.1.0
|
|
95
|
+
- @checkstack/catalog-common@1.0.0
|
|
96
|
+
- @checkstack/common@0.1.0
|
|
97
|
+
- @checkstack/healthcheck-common@0.2.0
|
|
98
|
+
- @checkstack/frontend-api@0.0.4
|
|
99
|
+
- @checkstack/signal-frontend@0.0.5
|
|
100
|
+
|
|
101
|
+
## 0.1.0
|
|
102
|
+
|
|
103
|
+
### Minor Changes
|
|
104
|
+
|
|
105
|
+
- f5b1f49: Added support for nested collector result display in auto-charts and history table.
|
|
106
|
+
|
|
107
|
+
- Updated `schema-parser.ts` to traverse `collectors.*` nested schemas and extract chart fields with dot-notation paths
|
|
108
|
+
- Added `getFieldValue()` support for dot-notation paths like `collectors.request.responseTimeMs`
|
|
109
|
+
- Added `ExpandedResultView` component to `HealthCheckRunsTable.tsx` that displays:
|
|
110
|
+
- Connection info (status, latency, connection time)
|
|
111
|
+
- Per-collector results as structured cards with key-value pairs
|
|
112
|
+
|
|
113
|
+
- f5b1f49: Added JSONPath assertions for response body validation and fully qualified strategy IDs.
|
|
114
|
+
|
|
115
|
+
**JSONPath Assertions:**
|
|
116
|
+
|
|
117
|
+
- Added `healthResultJSONPath()` factory in healthcheck-common for fields supporting JSONPath queries
|
|
118
|
+
- Extended AssertionBuilder with jsonpath field type showing path input (e.g., `$.data.status`)
|
|
119
|
+
- Added `jsonPath` field to `CollectorAssertionSchema` for persistence
|
|
120
|
+
- HTTP Request collector body field now supports JSONPath assertions
|
|
121
|
+
|
|
122
|
+
**Fully Qualified Strategy IDs:**
|
|
123
|
+
|
|
124
|
+
- HealthCheckRegistry now uses scoped factories like CollectorRegistry
|
|
125
|
+
- Strategies are stored with `pluginId.strategyId` format
|
|
126
|
+
- Added `getStrategiesWithMeta()` method to HealthCheckRegistry interface
|
|
127
|
+
- Router returns qualified IDs so frontend can correctly fetch collectors
|
|
128
|
+
|
|
129
|
+
**UI Improvements:**
|
|
130
|
+
|
|
131
|
+
- Save button disabled when collector configs have invalid required fields
|
|
132
|
+
- Fixed nested button warning in CollectorList accordion
|
|
133
|
+
|
|
134
|
+
### Patch Changes
|
|
135
|
+
|
|
136
|
+
- Updated dependencies [f5b1f49]
|
|
137
|
+
- Updated dependencies [f5b1f49]
|
|
138
|
+
- Updated dependencies [f5b1f49]
|
|
139
|
+
- Updated dependencies [f5b1f49]
|
|
140
|
+
- @checkstack/healthcheck-common@0.1.0
|
|
141
|
+
- @checkstack/common@0.0.3
|
|
142
|
+
- @checkstack/ui@0.0.4
|
|
143
|
+
- @checkstack/catalog-common@0.0.3
|
|
144
|
+
- @checkstack/frontend-api@0.0.3
|
|
145
|
+
- @checkstack/signal-frontend@0.0.4
|
|
146
|
+
|
|
3
147
|
## 0.0.3
|
|
4
148
|
|
|
5
149
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-frontend",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"scripts": {
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
"lint:code": "eslint . --max-warnings 0"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@checkstack/auth-frontend": "workspace:*",
|
|
12
13
|
"@checkstack/catalog-common": "workspace:*",
|
|
13
14
|
"@checkstack/common": "workspace:*",
|
|
14
15
|
"@checkstack/frontend-api": "workspace:*",
|
|
15
16
|
"@checkstack/healthcheck-common": "workspace:*",
|
|
16
17
|
"@checkstack/signal-frontend": "workspace:*",
|
|
17
18
|
"@checkstack/ui": "workspace:*",
|
|
18
|
-
"@types/prismjs": "^1.26.5",
|
|
19
19
|
"ajv": "^8.17.1",
|
|
20
20
|
"ajv-formats": "^3.0.1",
|
|
21
21
|
"date-fns": "^4.1.0",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"zod": "^4.2.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
+
"@types/prismjs": "^1.26.5",
|
|
31
32
|
"typescript": "^5.0.0",
|
|
32
33
|
"@types/react": "^18.2.0",
|
|
33
34
|
"@checkstack/tsconfig": "workspace:*",
|
|
@@ -33,6 +33,9 @@ interface AutoChartGridProps {
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Main component that renders a grid of auto-generated charts.
|
|
36
|
+
*
|
|
37
|
+
* Discovers actual collector instances from run data and creates
|
|
38
|
+
* separate charts for each instance, grouped with headings.
|
|
36
39
|
*/
|
|
37
40
|
export function AutoChartGrid({ context }: AutoChartGridProps) {
|
|
38
41
|
const { schemas, loading } = useStrategySchemas(context.strategyId);
|
|
@@ -55,22 +58,247 @@ export function AutoChartGrid({ context }: AutoChartGridProps) {
|
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
61
|
+
const schemaFields = extractChartFields(schema);
|
|
62
|
+
if (schemaFields.length === 0) {
|
|
60
63
|
return;
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
// Discover actual collector instances from run data
|
|
67
|
+
const instanceMap = discoverCollectorInstances(context);
|
|
68
|
+
|
|
69
|
+
// Separate strategy-level fields from collector fields
|
|
70
|
+
const strategyFields = schemaFields.filter((f) => !f.collectorId);
|
|
71
|
+
|
|
72
|
+
// Build grouped collector fields
|
|
73
|
+
const collectorGroups = buildCollectorGroups(schemaFields, instanceMap);
|
|
74
|
+
|
|
63
75
|
return (
|
|
64
|
-
<div className="
|
|
65
|
-
{fields
|
|
66
|
-
|
|
76
|
+
<div className="space-y-6 mt-4">
|
|
77
|
+
{/* Strategy-level fields */}
|
|
78
|
+
{strategyFields.length > 0 && (
|
|
79
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
80
|
+
{strategyFields.map((field) => (
|
|
81
|
+
<AutoChartCard key={field.name} field={field} context={context} />
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{/* Collector groups */}
|
|
87
|
+
{collectorGroups.map((group) => (
|
|
88
|
+
<CollectorGroup
|
|
89
|
+
key={group.instanceKey}
|
|
90
|
+
group={group}
|
|
91
|
+
context={context}
|
|
92
|
+
/>
|
|
67
93
|
))}
|
|
68
94
|
</div>
|
|
69
95
|
);
|
|
70
96
|
}
|
|
71
97
|
|
|
98
|
+
/**
|
|
99
|
+
* A group of fields for a single collector instance.
|
|
100
|
+
*/
|
|
101
|
+
interface CollectorGroupData {
|
|
102
|
+
instanceKey: string;
|
|
103
|
+
collectorId: string;
|
|
104
|
+
displayName: string;
|
|
105
|
+
fields: ExpandedChartField[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build grouped collector data from schema fields and instance map.
|
|
110
|
+
*/
|
|
111
|
+
function buildCollectorGroups(
|
|
112
|
+
schemaFields: ChartField[],
|
|
113
|
+
instanceMap: Record<string, string[]>
|
|
114
|
+
): CollectorGroupData[] {
|
|
115
|
+
const groups: CollectorGroupData[] = [];
|
|
116
|
+
|
|
117
|
+
// Process each collector type
|
|
118
|
+
for (const [collectorId, instanceKeys] of Object.entries(instanceMap)) {
|
|
119
|
+
// Get fields for this collector type
|
|
120
|
+
const collectorFields = schemaFields.filter(
|
|
121
|
+
(f) => f.collectorId === collectorId
|
|
122
|
+
);
|
|
123
|
+
if (collectorFields.length === 0) continue;
|
|
124
|
+
|
|
125
|
+
// Create a group for each instance
|
|
126
|
+
for (const [index, instanceKey] of instanceKeys.entries()) {
|
|
127
|
+
const displayName =
|
|
128
|
+
instanceKeys.length === 1
|
|
129
|
+
? collectorId.split(".").pop() || collectorId
|
|
130
|
+
: `${collectorId.split(".").pop() || collectorId} #${index + 1}`;
|
|
131
|
+
|
|
132
|
+
groups.push({
|
|
133
|
+
instanceKey,
|
|
134
|
+
collectorId,
|
|
135
|
+
displayName,
|
|
136
|
+
fields: collectorFields.map((field) => ({
|
|
137
|
+
...field,
|
|
138
|
+
instanceKey,
|
|
139
|
+
})),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return groups;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Renders a collector group with heading, assertion status, and field cards.
|
|
149
|
+
*/
|
|
150
|
+
function CollectorGroup({
|
|
151
|
+
group,
|
|
152
|
+
context,
|
|
153
|
+
}: {
|
|
154
|
+
group: CollectorGroupData;
|
|
155
|
+
context: HealthCheckDiagramSlotContext;
|
|
156
|
+
}) {
|
|
157
|
+
// Get assertion status for this collector instance
|
|
158
|
+
const assertionFailed = getAssertionFailed(context, group.instanceKey);
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div>
|
|
162
|
+
<h4 className="text-sm font-medium text-muted-foreground mb-3 uppercase tracking-wide">
|
|
163
|
+
{group.displayName}
|
|
164
|
+
</h4>
|
|
165
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
166
|
+
{/* Assertion status card */}
|
|
167
|
+
<AssertionStatusCard assertionFailed={assertionFailed} />
|
|
168
|
+
|
|
169
|
+
{/* Field cards */}
|
|
170
|
+
{group.fields.map((field) => (
|
|
171
|
+
<AutoChartCard
|
|
172
|
+
key={`${field.instanceKey}-${field.name}`}
|
|
173
|
+
field={field}
|
|
174
|
+
context={context}
|
|
175
|
+
/>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get the _assertionFailed value for a specific collector instance.
|
|
184
|
+
*/
|
|
185
|
+
function getAssertionFailed(
|
|
186
|
+
context: HealthCheckDiagramSlotContext,
|
|
187
|
+
instanceKey: string
|
|
188
|
+
): string | undefined {
|
|
189
|
+
if (context.type === "raw" && context.runs.length > 0) {
|
|
190
|
+
const latestRun = context.runs[0];
|
|
191
|
+
const result = latestRun.result as StoredHealthCheckResult | undefined;
|
|
192
|
+
const collectors = result?.metadata?.collectors as
|
|
193
|
+
| Record<string, Record<string, unknown>>
|
|
194
|
+
| undefined;
|
|
195
|
+
const collectorData = collectors?.[instanceKey];
|
|
196
|
+
return collectorData?._assertionFailed as string | undefined;
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Card showing assertion pass/fail status.
|
|
203
|
+
*/
|
|
204
|
+
function AssertionStatusCard({
|
|
205
|
+
assertionFailed,
|
|
206
|
+
}: {
|
|
207
|
+
assertionFailed: string | undefined;
|
|
208
|
+
}) {
|
|
209
|
+
if (!assertionFailed) {
|
|
210
|
+
return (
|
|
211
|
+
<Card>
|
|
212
|
+
<CardHeader className="pb-2">
|
|
213
|
+
<CardTitle className="text-sm font-medium">Assertion</CardTitle>
|
|
214
|
+
</CardHeader>
|
|
215
|
+
<CardContent>
|
|
216
|
+
<div className="flex items-center gap-2 text-green-600">
|
|
217
|
+
<div className="w-3 h-3 rounded-full bg-green-500" />
|
|
218
|
+
<span>Passed</span>
|
|
219
|
+
</div>
|
|
220
|
+
</CardContent>
|
|
221
|
+
</Card>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<Card className="border-red-200 dark:border-red-900">
|
|
227
|
+
<CardHeader className="pb-2">
|
|
228
|
+
<CardTitle className="text-sm font-medium text-red-600">
|
|
229
|
+
Assertion Failed
|
|
230
|
+
</CardTitle>
|
|
231
|
+
</CardHeader>
|
|
232
|
+
<CardContent>
|
|
233
|
+
<div className="text-sm text-red-600 bg-red-50 dark:bg-red-950 px-2 py-1 rounded">
|
|
234
|
+
{assertionFailed}
|
|
235
|
+
</div>
|
|
236
|
+
</CardContent>
|
|
237
|
+
</Card>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Discover collector instances from actual run data.
|
|
243
|
+
* Returns a map from base collector ID (type) to array of instance UUIDs.
|
|
244
|
+
*/
|
|
245
|
+
function discoverCollectorInstances(
|
|
246
|
+
context: HealthCheckDiagramSlotContext
|
|
247
|
+
): Record<string, string[]> {
|
|
248
|
+
const instanceMap: Record<string, Set<string>> = {};
|
|
249
|
+
|
|
250
|
+
const addInstances = (collectors: Record<string, unknown> | undefined) => {
|
|
251
|
+
if (!collectors || typeof collectors !== "object") return;
|
|
252
|
+
|
|
253
|
+
for (const [uuid, data] of Object.entries(collectors)) {
|
|
254
|
+
// Read the collector type from the stored _collectorId metadata
|
|
255
|
+
const collectorData = data as Record<string, unknown> | undefined;
|
|
256
|
+
const collectorId = collectorData?._collectorId as string | undefined;
|
|
257
|
+
|
|
258
|
+
if (collectorId) {
|
|
259
|
+
if (!instanceMap[collectorId]) {
|
|
260
|
+
instanceMap[collectorId] = new Set();
|
|
261
|
+
}
|
|
262
|
+
instanceMap[collectorId].add(uuid);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
if (context.type === "raw") {
|
|
268
|
+
for (const run of context.runs) {
|
|
269
|
+
const result = run.result as StoredHealthCheckResult | undefined;
|
|
270
|
+
const collectors = result?.metadata?.collectors as
|
|
271
|
+
| Record<string, unknown>
|
|
272
|
+
| undefined;
|
|
273
|
+
addInstances(collectors);
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
for (const bucket of context.buckets) {
|
|
277
|
+
const collectors = (
|
|
278
|
+
bucket.aggregatedResult as Record<string, unknown> | undefined
|
|
279
|
+
)?.collectors as Record<string, unknown> | undefined;
|
|
280
|
+
addInstances(collectors);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Convert sets to arrays
|
|
285
|
+
const result: Record<string, string[]> = {};
|
|
286
|
+
for (const [collectorId, uuids] of Object.entries(instanceMap)) {
|
|
287
|
+
result[collectorId] = [...uuids].toSorted();
|
|
288
|
+
}
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Extended ChartField with instance UUID for data lookup.
|
|
294
|
+
*/
|
|
295
|
+
interface ExpandedChartField extends ChartField {
|
|
296
|
+
/** Instance UUID for data lookup */
|
|
297
|
+
instanceKey?: string;
|
|
298
|
+
}
|
|
299
|
+
|
|
72
300
|
interface AutoChartCardProps {
|
|
73
|
-
field:
|
|
301
|
+
field: ExpandedChartField;
|
|
74
302
|
context: HealthCheckDiagramSlotContext;
|
|
75
303
|
}
|
|
76
304
|
|
|
@@ -91,7 +319,7 @@ function AutoChartCard({ field, context }: AutoChartCardProps) {
|
|
|
91
319
|
}
|
|
92
320
|
|
|
93
321
|
interface ChartRendererProps {
|
|
94
|
-
field:
|
|
322
|
+
field: ExpandedChartField;
|
|
95
323
|
context: HealthCheckDiagramSlotContext;
|
|
96
324
|
}
|
|
97
325
|
|
|
@@ -139,7 +367,7 @@ function ChartRenderer({ field, context }: ChartRendererProps) {
|
|
|
139
367
|
* Counts how many times each value appears across all runs/buckets.
|
|
140
368
|
*/
|
|
141
369
|
function CounterRenderer({ field, context }: ChartRendererProps) {
|
|
142
|
-
const counts = getValueCounts(field.name, context);
|
|
370
|
+
const counts = getValueCounts(field.name, context, field.instanceKey);
|
|
143
371
|
const entries = Object.entries(counts);
|
|
144
372
|
|
|
145
373
|
if (entries.length === 0) {
|
|
@@ -184,7 +412,7 @@ function CounterRenderer({ field, context }: ChartRendererProps) {
|
|
|
184
412
|
* Renders a percentage gauge visualization using Recharts RadialBarChart.
|
|
185
413
|
*/
|
|
186
414
|
function GaugeRenderer({ field, context }: ChartRendererProps) {
|
|
187
|
-
const value = getLatestValue(field.name, context);
|
|
415
|
+
const value = getLatestValue(field.name, context, field.instanceKey);
|
|
188
416
|
const numValue =
|
|
189
417
|
typeof value === "number" ? Math.min(100, Math.max(0, value)) : 0;
|
|
190
418
|
const unit = field.unit ?? "%";
|
|
@@ -231,7 +459,7 @@ function GaugeRenderer({ field, context }: ChartRendererProps) {
|
|
|
231
459
|
* Renders a boolean indicator (success/failure).
|
|
232
460
|
*/
|
|
233
461
|
function BooleanRenderer({ field, context }: ChartRendererProps) {
|
|
234
|
-
const value = getLatestValue(field.name, context);
|
|
462
|
+
const value = getLatestValue(field.name, context, field.instanceKey);
|
|
235
463
|
const isTrue = value === true;
|
|
236
464
|
|
|
237
465
|
return (
|
|
@@ -252,7 +480,7 @@ function BooleanRenderer({ field, context }: ChartRendererProps) {
|
|
|
252
480
|
* Renders text value.
|
|
253
481
|
*/
|
|
254
482
|
function TextRenderer({ field, context }: ChartRendererProps) {
|
|
255
|
-
const value = getLatestValue(field.name, context);
|
|
483
|
+
const value = getLatestValue(field.name, context, field.instanceKey);
|
|
256
484
|
const displayValue = formatTextValue(value);
|
|
257
485
|
|
|
258
486
|
return (
|
|
@@ -269,7 +497,7 @@ function TextRenderer({ field, context }: ChartRendererProps) {
|
|
|
269
497
|
* Renders error/status badge.
|
|
270
498
|
*/
|
|
271
499
|
function StatusRenderer({ field, context }: ChartRendererProps) {
|
|
272
|
-
const value = getLatestValue(field.name, context);
|
|
500
|
+
const value = getLatestValue(field.name, context, field.instanceKey);
|
|
273
501
|
const hasValue = value !== undefined && value !== null && value !== "";
|
|
274
502
|
|
|
275
503
|
if (!hasValue) {
|
|
@@ -287,7 +515,7 @@ function StatusRenderer({ field, context }: ChartRendererProps) {
|
|
|
287
515
|
* Renders an area chart for time series data using Recharts AreaChart.
|
|
288
516
|
*/
|
|
289
517
|
function LineChartRenderer({ field, context }: ChartRendererProps) {
|
|
290
|
-
const values = getAllValues(field.name, context);
|
|
518
|
+
const values = getAllValues(field.name, context, field.instanceKey);
|
|
291
519
|
const unit = field.unit ?? "";
|
|
292
520
|
|
|
293
521
|
if (values.length === 0) {
|
|
@@ -443,7 +671,7 @@ const CHART_COLORS = [
|
|
|
443
671
|
*/
|
|
444
672
|
function PieChartRenderer({ field, context }: ChartRendererProps) {
|
|
445
673
|
// First, try to get a pre-aggregated object value
|
|
446
|
-
const value = getLatestValue(field.name, context);
|
|
674
|
+
const value = getLatestValue(field.name, context, field.instanceKey);
|
|
447
675
|
|
|
448
676
|
// Determine the data source: use pre-aggregated object or count simple values
|
|
449
677
|
let dataRecord: Record<string, number>;
|
|
@@ -453,7 +681,7 @@ function PieChartRenderer({ field, context }: ChartRendererProps) {
|
|
|
453
681
|
dataRecord = value as Record<string, number>;
|
|
454
682
|
} else {
|
|
455
683
|
// Simple values (like statusCode) - count occurrences
|
|
456
|
-
dataRecord = getValueCounts(field.name, context);
|
|
684
|
+
dataRecord = getValueCounts(field.name, context, field.instanceKey);
|
|
457
685
|
}
|
|
458
686
|
|
|
459
687
|
const entries = Object.entries(dataRecord).slice(0, 8);
|
|
@@ -542,7 +770,8 @@ function PieChartRenderer({ field, context }: ChartRendererProps) {
|
|
|
542
770
|
*/
|
|
543
771
|
function getLatestValue(
|
|
544
772
|
fieldName: string,
|
|
545
|
-
context: HealthCheckDiagramSlotContext
|
|
773
|
+
context: HealthCheckDiagramSlotContext,
|
|
774
|
+
collectorId?: string
|
|
546
775
|
): unknown {
|
|
547
776
|
if (context.type === "raw") {
|
|
548
777
|
const runs = context.runs;
|
|
@@ -550,7 +779,7 @@ function getLatestValue(
|
|
|
550
779
|
// For raw runs, aggregate across all runs for record types
|
|
551
780
|
const allValues = runs.map((run) => {
|
|
552
781
|
const result = run.result as StoredHealthCheckResult | undefined;
|
|
553
|
-
return getFieldValue(result?.metadata, fieldName);
|
|
782
|
+
return getFieldValue(result?.metadata, fieldName, collectorId);
|
|
554
783
|
});
|
|
555
784
|
|
|
556
785
|
// If the values are record types (like statusCodeCounts), combine them
|
|
@@ -613,14 +842,15 @@ function combineRecordValues(
|
|
|
613
842
|
*/
|
|
614
843
|
function getValueCounts(
|
|
615
844
|
fieldName: string,
|
|
616
|
-
context: HealthCheckDiagramSlotContext
|
|
845
|
+
context: HealthCheckDiagramSlotContext,
|
|
846
|
+
collectorId?: string
|
|
617
847
|
): Record<string, number> {
|
|
618
848
|
const counts: Record<string, number> = {};
|
|
619
849
|
|
|
620
850
|
if (context.type === "raw") {
|
|
621
851
|
for (const run of context.runs) {
|
|
622
852
|
const result = run.result as StoredHealthCheckResult | undefined;
|
|
623
|
-
const value = getFieldValue(result?.metadata, fieldName);
|
|
853
|
+
const value = getFieldValue(result?.metadata, fieldName, collectorId);
|
|
624
854
|
if (value !== undefined && value !== null) {
|
|
625
855
|
const key = String(value);
|
|
626
856
|
counts[key] = (counts[key] || 0) + 1;
|
|
@@ -631,7 +861,8 @@ function getValueCounts(
|
|
|
631
861
|
for (const bucket of context.buckets) {
|
|
632
862
|
const value = getFieldValue(
|
|
633
863
|
bucket.aggregatedResult as Record<string, unknown>,
|
|
634
|
-
fieldName
|
|
864
|
+
fieldName,
|
|
865
|
+
collectorId
|
|
635
866
|
);
|
|
636
867
|
if (value !== undefined && value !== null) {
|
|
637
868
|
const key = String(value);
|
|
@@ -651,14 +882,15 @@ function getValueCounts(
|
|
|
651
882
|
*/
|
|
652
883
|
function getAllValues(
|
|
653
884
|
fieldName: string,
|
|
654
|
-
context: HealthCheckDiagramSlotContext
|
|
885
|
+
context: HealthCheckDiagramSlotContext,
|
|
886
|
+
collectorId?: string
|
|
655
887
|
): number[] {
|
|
656
888
|
if (context.type === "raw") {
|
|
657
889
|
return context.runs
|
|
658
890
|
.map((run) => {
|
|
659
891
|
// result is typed as StoredHealthCheckResult with { status, latencyMs, message, metadata }
|
|
660
892
|
const result = run.result as StoredHealthCheckResult;
|
|
661
|
-
return getFieldValue(result?.metadata, fieldName);
|
|
893
|
+
return getFieldValue(result?.metadata, fieldName, collectorId);
|
|
662
894
|
})
|
|
663
895
|
.filter((v): v is number => typeof v === "number");
|
|
664
896
|
}
|
|
@@ -666,7 +898,8 @@ function getAllValues(
|
|
|
666
898
|
.map((bucket) =>
|
|
667
899
|
getFieldValue(
|
|
668
900
|
bucket.aggregatedResult as Record<string, unknown>,
|
|
669
|
-
fieldName
|
|
901
|
+
fieldName,
|
|
902
|
+
collectorId
|
|
670
903
|
)
|
|
671
904
|
)
|
|
672
905
|
.filter((v): v is number => typeof v === "number");
|