@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
|
@@ -3,15 +3,38 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Parses JSON Schema objects and extracts x-chart-type, x-chart-label,
|
|
5
5
|
* and x-chart-unit metadata for auto-chart rendering.
|
|
6
|
+
*
|
|
7
|
+
* Supports nested schemas under `collectors.*` for per-collector metrics.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import type { ChartType } from "@checkstack/healthcheck-common";
|
|
11
|
+
import type {
|
|
12
|
+
JsonSchemaPropertyCore,
|
|
13
|
+
JsonSchemaBase,
|
|
14
|
+
} from "@checkstack/common";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* JSON Schema property with healthcheck result-specific x-* extensions.
|
|
18
|
+
* Uses the generic core type for proper recursive typing.
|
|
19
|
+
*/
|
|
20
|
+
export interface ResultSchemaProperty
|
|
21
|
+
extends JsonSchemaPropertyCore<ResultSchemaProperty> {
|
|
22
|
+
// Result-specific x-* extensions for chart rendering
|
|
23
|
+
"x-chart-type"?: ChartType;
|
|
24
|
+
"x-chart-label"?: string;
|
|
25
|
+
"x-chart-unit"?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* JSON Schema for result schemas with chart metadata.
|
|
30
|
+
*/
|
|
31
|
+
export type ResultSchema = JsonSchemaBase<ResultSchemaProperty>;
|
|
9
32
|
|
|
10
33
|
/**
|
|
11
34
|
* Chart field information extracted from JSON Schema.
|
|
12
35
|
*/
|
|
13
36
|
export interface ChartField {
|
|
14
|
-
/** Field name
|
|
37
|
+
/** Field name (simple name for collector fields, path for others) */
|
|
15
38
|
name: string;
|
|
16
39
|
/** Chart type to render */
|
|
17
40
|
chartType: ChartType;
|
|
@@ -21,33 +44,15 @@ export interface ChartField {
|
|
|
21
44
|
unit?: string;
|
|
22
45
|
/** JSON Schema type (number, string, boolean, etc.) */
|
|
23
46
|
schemaType: string;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* JSON Schema property with potential chart metadata.
|
|
28
|
-
*/
|
|
29
|
-
interface JsonSchemaProperty {
|
|
30
|
-
type?: string;
|
|
31
|
-
"x-chart-type"?: ChartType;
|
|
32
|
-
"x-chart-label"?: string;
|
|
33
|
-
"x-chart-unit"?: string;
|
|
34
|
-
items?: JsonSchemaProperty;
|
|
35
|
-
additionalProperties?: JsonSchemaProperty;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* JSON Schema object structure.
|
|
40
|
-
*/
|
|
41
|
-
interface JsonSchema {
|
|
42
|
-
type?: string;
|
|
43
|
-
properties?: Record<string, JsonSchemaProperty>;
|
|
47
|
+
/** Collector ID if this field is from a collector (used for data lookup) */
|
|
48
|
+
collectorId?: string;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
/**
|
|
47
52
|
* Extract chart fields from a JSON Schema.
|
|
48
53
|
*
|
|
49
54
|
* Looks for properties with x-chart-type metadata and extracts
|
|
50
|
-
* relevant chart configuration.
|
|
55
|
+
* relevant chart configuration. Supports nested collector schemas.
|
|
51
56
|
*
|
|
52
57
|
* @param schema - JSON Schema object
|
|
53
58
|
* @returns Array of chart fields with metadata
|
|
@@ -57,41 +62,100 @@ export function extractChartFields(
|
|
|
57
62
|
): ChartField[] {
|
|
58
63
|
if (!schema) return [];
|
|
59
64
|
|
|
60
|
-
const typed = schema as
|
|
65
|
+
const typed = schema as ResultSchema;
|
|
61
66
|
if (typed.type !== "object" || !typed.properties) return [];
|
|
62
67
|
|
|
63
68
|
const fields: ChartField[] = [];
|
|
64
69
|
|
|
65
70
|
for (const [name, prop] of Object.entries(typed.properties)) {
|
|
71
|
+
// Check for nested collector schemas
|
|
72
|
+
if (name === "collectors" && prop.type === "object" && prop.properties) {
|
|
73
|
+
// Traverse each collector's schema
|
|
74
|
+
for (const [collectorId, collectorProp] of Object.entries(
|
|
75
|
+
prop.properties
|
|
76
|
+
)) {
|
|
77
|
+
if (collectorProp.type === "object" && collectorProp.properties) {
|
|
78
|
+
// Extract fields from the collector's result schema
|
|
79
|
+
const collectorFields = extractFieldsFromProperties(
|
|
80
|
+
collectorProp.properties,
|
|
81
|
+
collectorId
|
|
82
|
+
);
|
|
83
|
+
fields.push(...collectorFields);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
const chartType = prop["x-chart-type"];
|
|
67
90
|
if (!chartType) continue;
|
|
68
91
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
fields.push(extractSingleField(name, prop));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return fields;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract fields from a nested properties object.
|
|
100
|
+
*/
|
|
101
|
+
function extractFieldsFromProperties(
|
|
102
|
+
properties: Record<string, ResultSchemaProperty>,
|
|
103
|
+
collectorId: string
|
|
104
|
+
): ChartField[] {
|
|
105
|
+
const fields: ChartField[] = [];
|
|
77
106
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
for (const [fieldName, prop] of Object.entries(properties)) {
|
|
108
|
+
const chartType = prop["x-chart-type"];
|
|
109
|
+
if (!chartType) continue;
|
|
110
|
+
|
|
111
|
+
// Use just field name - collectorId is stored separately for data lookup
|
|
112
|
+
const field = extractSingleField(fieldName, prop);
|
|
113
|
+
field.collectorId = collectorId;
|
|
114
|
+
// Prefix label with collector ID for clarity
|
|
115
|
+
if (!prop["x-chart-label"]?.includes(collectorId)) {
|
|
116
|
+
field.label = `${collectorId}: ${field.label}`;
|
|
117
|
+
}
|
|
118
|
+
fields.push(field);
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
return fields;
|
|
88
122
|
}
|
|
89
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Extract a single field from a property.
|
|
126
|
+
*/
|
|
127
|
+
function extractSingleField(
|
|
128
|
+
name: string,
|
|
129
|
+
prop: ResultSchemaProperty
|
|
130
|
+
): ChartField {
|
|
131
|
+
let schemaType = prop.type ?? "unknown";
|
|
132
|
+
if (prop.type === "array" && prop.items?.type) {
|
|
133
|
+
schemaType = `array<${prop.items.type}>`;
|
|
134
|
+
}
|
|
135
|
+
if (
|
|
136
|
+
prop.additionalProperties &&
|
|
137
|
+
typeof prop.additionalProperties === "object" &&
|
|
138
|
+
prop.additionalProperties.type
|
|
139
|
+
) {
|
|
140
|
+
schemaType = `record<${prop.additionalProperties.type}>`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
name,
|
|
145
|
+
chartType: prop["x-chart-type"]!,
|
|
146
|
+
label: prop["x-chart-label"] ?? formatFieldName(name),
|
|
147
|
+
unit: prop["x-chart-unit"],
|
|
148
|
+
schemaType,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
90
152
|
/**
|
|
91
153
|
* Convert camelCase or snake_case field name to human-readable label.
|
|
92
154
|
*/
|
|
93
155
|
function formatFieldName(name: string): string {
|
|
94
|
-
|
|
156
|
+
// Extract just the field name if it's a path
|
|
157
|
+
const baseName = name.includes(".") ? name.split(".").pop()! : name;
|
|
158
|
+
return baseName
|
|
95
159
|
.replaceAll(/([a-z])([A-Z])/g, "$1 $2") // camelCase
|
|
96
160
|
.replaceAll("_", " ") // snake_case
|
|
97
161
|
.replaceAll(/\b\w/g, (c) => c.toUpperCase()); // Capitalize
|
|
@@ -99,11 +163,53 @@ function formatFieldName(name: string): string {
|
|
|
99
163
|
|
|
100
164
|
/**
|
|
101
165
|
* Get the value for a field from a data object.
|
|
166
|
+
* For strategy-level fields, also searches inside collectors as fallback.
|
|
167
|
+
*
|
|
168
|
+
* @param data - The metadata object
|
|
169
|
+
* @param fieldName - Simple field name (no dot notation for collector fields)
|
|
170
|
+
* @param collectorInstanceId - Optional: if provided, looks in collectors[collectorInstanceId]
|
|
102
171
|
*/
|
|
103
172
|
export function getFieldValue(
|
|
104
173
|
data: Record<string, unknown> | undefined,
|
|
105
|
-
fieldName: string
|
|
174
|
+
fieldName: string,
|
|
175
|
+
collectorInstanceId?: string
|
|
106
176
|
): unknown {
|
|
107
177
|
if (!data) return undefined;
|
|
108
|
-
|
|
178
|
+
|
|
179
|
+
// If collectorInstanceId is provided, look in that specific collector's data
|
|
180
|
+
if (collectorInstanceId) {
|
|
181
|
+
const collectors = data.collectors as
|
|
182
|
+
| Record<string, Record<string, unknown>>
|
|
183
|
+
| undefined;
|
|
184
|
+
if (collectors && typeof collectors === "object") {
|
|
185
|
+
const collectorData = collectors[collectorInstanceId];
|
|
186
|
+
if (collectorData && typeof collectorData === "object") {
|
|
187
|
+
return collectorData[fieldName];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// For non-collector fields, try direct lookup first
|
|
194
|
+
const directValue = data[fieldName];
|
|
195
|
+
if (directValue !== undefined) {
|
|
196
|
+
return directValue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Fallback: search all collectors for the field (for strategy schema fields)
|
|
200
|
+
const collectors = data.collectors as
|
|
201
|
+
| Record<string, Record<string, unknown>>
|
|
202
|
+
| undefined;
|
|
203
|
+
if (collectors && typeof collectors === "object") {
|
|
204
|
+
for (const collectorData of Object.values(collectors)) {
|
|
205
|
+
if (collectorData && typeof collectorData === "object") {
|
|
206
|
+
const value = collectorData[fieldName];
|
|
207
|
+
if (value !== undefined) {
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return undefined;
|
|
109
215
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hook to fetch and cache strategy schemas.
|
|
3
|
+
*
|
|
4
|
+
* Fetches both strategy result schemas AND collector result schemas,
|
|
5
|
+
* merging them into a unified schema where collector schemas are nested
|
|
6
|
+
* under `properties.collectors.<collectorId>`.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
9
|
import { useEffect, useState } from "react";
|
|
@@ -14,6 +18,9 @@ interface StrategySchemas {
|
|
|
14
18
|
/**
|
|
15
19
|
* Fetch and cache strategy schemas for auto-chart rendering.
|
|
16
20
|
*
|
|
21
|
+
* Also fetches collector schemas and merges them into the result schema
|
|
22
|
+
* so that chart fields from collectors are properly extracted.
|
|
23
|
+
*
|
|
17
24
|
* @param strategyId - The strategy ID to fetch schemas for
|
|
18
25
|
* @returns Schemas for the strategy, or undefined if not found
|
|
19
26
|
*/
|
|
@@ -30,13 +37,30 @@ export function useStrategySchemas(strategyId: string): {
|
|
|
30
37
|
|
|
31
38
|
async function fetchSchemas() {
|
|
32
39
|
try {
|
|
33
|
-
|
|
40
|
+
// Fetch strategy and collectors in parallel
|
|
41
|
+
const [strategies, collectors] = await Promise.all([
|
|
42
|
+
api.getStrategies(),
|
|
43
|
+
api.getCollectors({ strategyId }),
|
|
44
|
+
]);
|
|
45
|
+
|
|
34
46
|
const strategy = strategies.find((s) => s.id === strategyId);
|
|
35
47
|
|
|
36
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
|
+
|
|
37
62
|
setSchemas({
|
|
38
|
-
resultSchema:
|
|
39
|
-
(strategy.resultSchema as Record<string, unknown>) ?? undefined,
|
|
63
|
+
resultSchema: mergedResultSchema,
|
|
40
64
|
aggregatedResultSchema:
|
|
41
65
|
(strategy.aggregatedResultSchema as Record<string, unknown>) ??
|
|
42
66
|
undefined,
|
|
@@ -60,3 +84,49 @@ export function useStrategySchemas(strategyId: string): {
|
|
|
60
84
|
|
|
61
85
|
return { schemas, loading };
|
|
62
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Merge collector result schemas into a strategy result schema.
|
|
90
|
+
*
|
|
91
|
+
* Creates a schema structure where collectors are nested under
|
|
92
|
+
* `properties.collectors.<collectorId>`, matching the actual data structure
|
|
93
|
+
* stored by the health check executor.
|
|
94
|
+
*/
|
|
95
|
+
function mergeCollectorSchemas(
|
|
96
|
+
strategySchema: Record<string, unknown> | undefined,
|
|
97
|
+
collectorProperties: Record<string, unknown>
|
|
98
|
+
): Record<string, unknown> | undefined {
|
|
99
|
+
// If no collectors, return original schema
|
|
100
|
+
if (Object.keys(collectorProperties).length === 0) {
|
|
101
|
+
return strategySchema;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Build the collectors nested schema
|
|
105
|
+
const collectorsSchema = {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: collectorProperties,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// If no strategy schema, create one just with collectors
|
|
111
|
+
if (!strategySchema) {
|
|
112
|
+
return {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
collectors: collectorsSchema,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Merge: add collectors to existing properties
|
|
121
|
+
const existingProps = (strategySchema.properties ?? {}) as Record<
|
|
122
|
+
string,
|
|
123
|
+
unknown
|
|
124
|
+
>;
|
|
125
|
+
return {
|
|
126
|
+
...strategySchema,
|
|
127
|
+
properties: {
|
|
128
|
+
...existingProps,
|
|
129
|
+
collectors: collectorsSchema,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|