@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.
@@ -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 in the schema */
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 JsonSchema;
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
- // Determine the underlying schema type
70
- let schemaType = prop.type ?? "unknown";
71
- if (prop.type === "array" && prop.items?.type) {
72
- schemaType = `array<${prop.items.type}>`;
73
- }
74
- if (prop.additionalProperties?.type) {
75
- schemaType = `record<${prop.additionalProperties.type}>`;
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
- fields.push({
79
- name,
80
- chartType,
81
- label: prop["x-chart-label"] ?? formatFieldName(name),
82
- unit: prop["x-chart-unit"],
83
- schemaType,
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
- return name
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
- return data[fieldName];
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
- const strategies = await api.getStrategies();
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
+ }