@checkstack/gitops-frontend 0.3.1 → 0.3.3

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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # @checkstack/gitops-frontend
2
2
 
3
+ ## 0.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 57d54de: Fix GitOps Healthcheck reconciliation engine and Kind Registry UI
8
+
9
+ - Mandated fully qualified IDs for all healthcheck strategies and collector definitions.
10
+ - Refactored the Kind Registry UI to display schema documentation in beautifully formatted, interactive YAML examples.
11
+ - Entity Envelope Fields and Base Spec Schema are now displayed in collapsed accordions.
12
+ - Fixed condition logic that broke the collector documentation display.
13
+ - Enhanced UX by dynamically injecting fully-qualified strategy variants directly into the YAML examples.
14
+
15
+ ## 0.3.2
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies [3da7582]
20
+ - @checkstack/ui@1.5.0
21
+
3
22
  ## 0.3.1
4
23
 
5
24
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/gitops-frontend",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "checkstack": {
@@ -14,8 +14,8 @@
14
14
  "dependencies": {
15
15
  "@checkstack/common": "0.6.5",
16
16
  "@checkstack/frontend-api": "0.3.9",
17
- "@checkstack/gitops-common": "0.1.1",
18
- "@checkstack/ui": "1.3.6",
17
+ "@checkstack/gitops-common": "0.2.0",
18
+ "@checkstack/ui": "1.5.0",
19
19
  "lucide-react": "^0.344.0",
20
20
  "react": "^18.2.0",
21
21
  "react-router-dom": "^6.22.0"
@@ -13,8 +13,11 @@ import {
13
13
  SelectTrigger,
14
14
  SelectValue,
15
15
  CodeEditor,
16
- Markdown,
17
16
  MarkdownBlock,
17
+ Accordion,
18
+ AccordionItem,
19
+ AccordionTrigger,
20
+ AccordionContent,
18
21
  } from "@checkstack/ui";
19
22
  import {
20
23
  ChevronDown,
@@ -64,131 +67,48 @@ interface KindDescription {
64
67
 
65
68
  // ─── Schema Display ────────────────────────────────────────────────────────
66
69
 
67
- const TYPE_COLORS: Record<string, string> = {
68
- string: "text-green-600 dark:text-green-400",
69
- number: "text-amber-600 dark:text-amber-400",
70
- integer: "text-amber-600 dark:text-amber-400",
71
- boolean: "text-red-600 dark:text-red-400",
72
- array: "text-blue-600 dark:text-blue-400",
73
- object: "text-purple-600 dark:text-purple-400",
74
- };
75
-
76
- function SchemaPropertyDisplay({
77
- schema,
78
- depth = 0,
79
- }: {
80
- schema: JsonSchemaProperty;
81
- depth?: number;
82
- }) {
83
- if (schema.enum) {
84
- return (
85
- <span className="text-green-600 dark:text-green-400">
86
- {schema.enum.map((e) => `"${e}"`).join(" | ")}
87
- </span>
88
- );
89
- }
90
70
 
91
- if (schema.anyOf) {
92
- return (
93
- <span>
94
- {schema.anyOf.map((s, i) => (
95
- <span key={i}>
96
- {i > 0 && <span className="text-muted-foreground"> | </span>}
97
- <SchemaPropertyDisplay schema={s} depth={depth} />
98
- </span>
99
- ))}
100
- </span>
101
- );
102
- }
103
71
 
72
+ function generateSchemaYaml(schema: JsonSchemaProperty): string {
73
+ const lines: string[] = [];
74
+
104
75
  if (schema.type === "object" && schema.properties) {
105
- return (
106
- <div className="font-mono text-sm" style={{ marginLeft: depth * 16 }}>
107
- {"{"}
108
- {Object.entries(schema.properties).map(([key, value]) => (
109
- <div key={key} className="ml-4">
110
- <span className="text-blue-600 dark:text-blue-400">{key}</span>
111
- {schema.required?.includes(key) && (
112
- <span className="text-red-500">*</span>
113
- )}
114
- : <SchemaPropertyDisplay schema={value} depth={depth + 1} />
115
- {value.description && (
116
- <span className="text-muted-foreground ml-2 text-xs inline-flex items-center gap-1">
117
- //{" "}
118
- <Markdown size="sm" className="inline">
119
- {value.description}
120
- </Markdown>
121
- </span>
122
- )}
123
- </div>
124
- ))}
125
- {"}"}
126
- </div>
127
- );
76
+ const required = new Set(schema.required);
77
+ for (const [key, prop] of Object.entries(schema.properties)) {
78
+ emitProperty({
79
+ lines,
80
+ key,
81
+ prop,
82
+ indent: 0,
83
+ required: required.has(key),
84
+ });
85
+ }
86
+ } else {
87
+ emitProperty({
88
+ lines,
89
+ key: "value",
90
+ prop: schema,
91
+ indent: 0,
92
+ required: true,
93
+ });
128
94
  }
129
-
130
- if (schema.type === "array" && schema.items) {
131
- return (
132
- <span>
133
- <SchemaPropertyDisplay schema={schema.items} depth={depth} />
134
- {"[]"}
135
- </span>
136
- );
95
+
96
+ if (lines.length === 0) {
97
+ return "# No properties defined";
137
98
  }
138
-
139
- const typeStr = schema.type ?? "unknown";
140
- return (
141
- <span className={TYPE_COLORS[typeStr] ?? "text-gray-600"}>
142
- {typeStr}
143
- {schema.default !== undefined && (
144
- <span className="text-muted-foreground ml-1">
145
- = {JSON.stringify(schema.default)}
146
- </span>
147
- )}
148
- </span>
149
- );
150
- }
151
-
152
- function SchemaBlock({
153
- schema,
154
- label,
155
- }: {
156
- schema: JsonSchemaProperty;
157
- label: string;
158
- }) {
159
- const hasProperties =
160
- schema.type === "object" &&
161
- schema.properties &&
162
- Object.keys(schema.properties).length > 0;
163
-
164
- if (!hasProperties) {
165
- return (
166
- <div>
167
- <h4 className="text-sm font-medium mb-2 text-muted-foreground">
168
- {label}
169
- </h4>
170
- <div className="bg-muted rounded-md p-3 text-sm text-muted-foreground italic">
171
- No properties defined
172
- </div>
173
- </div>
174
- );
175
- }
176
-
177
- return (
178
- <div>
179
- <h4 className="text-sm font-medium mb-2 text-muted-foreground">
180
- {label}
181
- </h4>
182
- <div className="bg-muted rounded-md p-3 overflow-x-auto">
183
- <SchemaPropertyDisplay schema={schema} />
184
- </div>
185
- </div>
186
- );
99
+
100
+ return lines.join("\n");
187
101
  }
188
102
 
189
103
  // ─── YAML Example Generator ────────────────────────────────────────────────
190
104
 
191
- function generateYamlExample({ kind }: { kind: KindDescription }): string {
105
+ function generateYamlExample({
106
+ kind,
107
+ selections,
108
+ }: {
109
+ kind: KindDescription;
110
+ selections?: Record<string, string>;
111
+ }): string {
192
112
  const lines = [`apiVersion: ${kind.apiVersion}`, `kind: ${kind.kind}`];
193
113
 
194
114
  if (kind.metadataSchema) {
@@ -234,6 +154,9 @@ function generateYamlExample({ kind }: { kind: KindDescription }): string {
234
154
  prop,
235
155
  indent: 2,
236
156
  required: baseRequired.has(key),
157
+ path: key,
158
+ kind,
159
+ selections,
237
160
  });
238
161
  }
239
162
 
@@ -260,18 +183,34 @@ function emitProperty({
260
183
  prop,
261
184
  indent,
262
185
  required,
186
+ path,
187
+ kind,
188
+ selections,
263
189
  }: {
264
190
  lines: string[];
265
191
  key: string;
266
192
  prop: JsonSchemaProperty;
267
193
  indent: number;
268
194
  required: boolean;
195
+ path?: string;
196
+ kind?: KindDescription;
197
+ selections?: Record<string, string>;
269
198
  }) {
270
199
  const pad = " ".repeat(indent);
271
- const annotation = buildAnnotation({ prop, required });
272
200
 
273
- // Resolve the effective schema (unwrap nullable / anyOf wrappers)
274
- const effective = resolveEffective({ prop });
201
+ let effectiveProp = prop;
202
+ if (path && kind?.specSchemaDocumentation && selections && selections[path]) {
203
+ const variantId = selections[path];
204
+ const doc = kind.specSchemaDocumentation.find(
205
+ (d) => d.fieldPath === path && (d.variantId || d.label) === variantId
206
+ );
207
+ if (doc) {
208
+ effectiveProp = doc.specSchema;
209
+ }
210
+ }
211
+
212
+ const annotation = buildAnnotation({ prop: effectiveProp, required });
213
+ const effective = resolveEffective({ prop: effectiveProp });
275
214
 
276
215
  if (effective.type === "object" && effective.properties) {
277
216
  lines.push(`${pad}${key}:${annotation}`);
@@ -283,17 +222,36 @@ function emitProperty({
283
222
  prop: p,
284
223
  indent: indent + 2,
285
224
  required: objRequired.has(k),
225
+ path: path ? `${path}.${k}` : undefined,
226
+ kind,
227
+ selections,
286
228
  });
287
229
  }
288
230
  } else if (effective.type === "array") {
289
231
  lines.push(`${pad}${key}:${annotation}`);
290
232
  if (effective.items) {
291
- emitArrayItem({ lines, itemSchema: effective.items, indent: indent + 2 });
233
+ emitArrayItem({
234
+ lines,
235
+ itemSchema: effective.items,
236
+ indent: indent + 2,
237
+ path: path ? `${path}[]` : undefined,
238
+ kind,
239
+ selections,
240
+ });
292
241
  } else {
293
242
  lines.push(`${pad} - # ...`);
294
243
  }
295
244
  } else {
296
- lines.push(`${pad}${key}: ${scalarExample({ prop })}${annotation}`);
245
+ let val = scalarExample({ prop: effective });
246
+
247
+ if (key === "strategy" && path === "strategy" && selections?.["config"]) {
248
+ val = `"${selections["config"]}"`;
249
+ }
250
+ if (key === "collectorId" && path === "collectors[].collectorId" && selections?.["collectors[].config"]) {
251
+ val = `"${selections["collectors[].config"]}"`;
252
+ }
253
+
254
+ lines.push(`${pad}${key}: ${val}${annotation}`);
297
255
  }
298
256
  }
299
257
 
@@ -305,10 +263,16 @@ function emitArrayItem({
305
263
  lines,
306
264
  itemSchema,
307
265
  indent,
266
+ path,
267
+ kind,
268
+ selections,
308
269
  }: {
309
270
  lines: string[];
310
271
  itemSchema: JsonSchemaProperty;
311
272
  indent: number;
273
+ path?: string;
274
+ kind?: KindDescription;
275
+ selections?: Record<string, string>;
312
276
  }) {
313
277
  const pad = " ".repeat(indent);
314
278
  const effective = resolveEffective({ prop: itemSchema });
@@ -318,11 +282,25 @@ function emitArrayItem({
318
282
  const entries = Object.entries(effective.properties);
319
283
  for (const [i, [k, p]] of entries.entries()) {
320
284
  const prefix = i === 0 ? `${pad}- ` : `${pad} `;
285
+
286
+ const nextPath = path ? (path.endsWith("[]") ? `${path}.${k}` : `${path}[].${k}`) : undefined;
287
+
288
+ let currentProp = p;
289
+ if (nextPath && kind?.specSchemaDocumentation && selections && selections[nextPath]) {
290
+ const variantId = selections[nextPath];
291
+ const doc = kind.specSchemaDocumentation.find(
292
+ (d) => d.fieldPath === nextPath && (d.variantId || d.label) === variantId
293
+ );
294
+ if (doc) {
295
+ currentProp = doc.specSchema;
296
+ }
297
+ }
298
+
321
299
  const itemAnnotation = buildAnnotation({
322
- prop: p,
300
+ prop: currentProp,
323
301
  required: itemRequired.has(k),
324
302
  });
325
- const inner = resolveEffective({ prop: p });
303
+ const inner = resolveEffective({ prop: currentProp });
326
304
 
327
305
  if (inner.type === "object" && inner.properties) {
328
306
  // Recurse into nested objects
@@ -335,18 +313,32 @@ function emitArrayItem({
335
313
  prop: np,
336
314
  indent: indent + 4,
337
315
  required: nestedRequired.has(nk),
316
+ path: nextPath ? `${nextPath}.${nk}` : undefined,
317
+ kind,
318
+ selections,
338
319
  });
339
320
  }
340
321
  } else if (inner.type === "array") {
341
322
  lines.push(`${prefix}${k}:${itemAnnotation}`);
342
323
  if (inner.items) {
343
- emitArrayItem({ lines, itemSchema: inner.items, indent: indent + 4 });
324
+ emitArrayItem({
325
+ lines,
326
+ itemSchema: inner.items,
327
+ indent: indent + 4,
328
+ path: nextPath ? `${nextPath}[]` : undefined,
329
+ kind,
330
+ selections,
331
+ });
344
332
  } else {
345
333
  lines.push(`${" ".repeat(indent + 4)}- # ...`);
346
334
  }
347
335
  } else {
336
+ let val = scalarExample({ prop: currentProp });
337
+ if (k === "collectorId" && nextPath === "collectors[].collectorId" && selections?.["collectors[].config"]) {
338
+ val = `"${selections["collectors[].config"]}"`;
339
+ }
348
340
  lines.push(
349
- `${prefix}${k}: ${scalarExample({ prop: p })}${itemAnnotation}`,
341
+ `${prefix}${k}: ${val}${itemAnnotation}`,
350
342
  );
351
343
  }
352
344
  }
@@ -402,7 +394,7 @@ function resolveEffective({
402
394
  */
403
395
  function scalarExample({ prop }: { prop: JsonSchemaProperty }): string {
404
396
  const effective = resolveEffective({ prop });
405
- if (effective.enum) return `"${effective.enum[0]}"`;
397
+ if (effective.enum) return effective.enum.map((e) => `"${e}"`).join(" | ");
406
398
  if (effective.default !== undefined) return JSON.stringify(effective.default);
407
399
  switch (effective.type) {
408
400
  case "string": {
@@ -432,18 +424,13 @@ function scalarExample({ prop }: { prop: JsonSchemaProperty }): string {
432
424
 
433
425
  function SpecSchemaDocumentationSection({
434
426
  docs,
427
+ selections,
428
+ onSelect,
435
429
  }: {
436
430
  docs: NonNullable<KindDescription["specSchemaDocumentation"]>;
431
+ selections: Record<string, string>;
432
+ onSelect: (fieldPath: string, variantId: string) => void;
437
433
  }) {
438
- const [selections, setSelections] = useState<Record<string, string>>({});
439
-
440
- const handleSelect = useCallback((fieldPath: string, variantId: string) => {
441
- setSelections((prev) => {
442
- if (prev[fieldPath] === variantId) return prev;
443
- return { ...prev, [fieldPath]: variantId };
444
- });
445
- }, []);
446
-
447
434
  const groupedDocs: Record<string, typeof docs> = {};
448
435
  for (const doc of docs) {
449
436
  if (!groupedDocs[doc.fieldPath]) {
@@ -466,7 +453,7 @@ function SpecSchemaDocumentationSection({
466
453
  fieldPath={fieldPath}
467
454
  docs={fieldDocs.toSorted((a, b) => a.label.localeCompare(b.label))}
468
455
  selections={selections}
469
- onSelect={handleSelect}
456
+ onSelect={onSelect}
470
457
  />
471
458
  );
472
459
  })}
@@ -553,8 +540,14 @@ function SpecSchemaDocumentationField({
553
540
  <MarkdownBlock>{selectedDoc.description}</MarkdownBlock>
554
541
  </div>
555
542
  )}
556
- <div className="bg-muted rounded-md p-3 overflow-x-auto">
557
- <SchemaPropertyDisplay schema={selectedDoc.specSchema} />
543
+ <div className="rounded-md overflow-hidden border border-input">
544
+ <CodeEditor
545
+ value={generateSchemaYaml(selectedDoc.specSchema)}
546
+ language="yaml"
547
+ readOnly
548
+ onChange={() => {}}
549
+ minHeight={`${Math.max(100, generateSchemaYaml(selectedDoc.specSchema).split("\n").length * 20 + 20)}px`}
550
+ />
558
551
  </div>
559
552
  </div>
560
553
  ) : (
@@ -570,7 +563,16 @@ function SpecSchemaDocumentationField({
570
563
 
571
564
  function KindCard({ kind }: { kind: KindDescription }) {
572
565
  const [isOpen, setIsOpen] = useState(false);
573
- const yamlExample = useMemo(() => generateYamlExample({ kind }), [kind]);
566
+ const [selections, setSelections] = useState<Record<string, string>>({});
567
+
568
+ const handleSelect = useCallback((fieldPath: string, variantId: string) => {
569
+ setSelections((prev) => {
570
+ if (prev[fieldPath] === variantId) return prev;
571
+ return { ...prev, [fieldPath]: variantId };
572
+ });
573
+ }, []);
574
+
575
+ const yamlExample = useMemo(() => generateYamlExample({ kind, selections }), [kind, selections]);
574
576
 
575
577
  return (
576
578
  <Card className="mb-3">
@@ -610,14 +612,47 @@ function KindCard({ kind }: { kind: KindDescription }) {
610
612
 
611
613
  {isOpen && (
612
614
  <CardContent className="pt-0 space-y-6">
613
- {/* Entity Envelope Fields */}
614
- <SchemaBlock
615
- schema={kind.metadataSchema}
616
- label="Entity Envelope Fields"
617
- />
618
-
619
- {/* Base Spec Schema */}
620
- <SchemaBlock schema={kind.specSchema} label="Base Spec Schema" />
615
+ <Accordion type="multiple">
616
+ {/* Entity Envelope Fields */}
617
+ <AccordionItem value="envelope" className="border-b-0">
618
+ <AccordionTrigger className="py-2 hover:no-underline">
619
+ <h4 className="text-sm font-medium text-muted-foreground">
620
+ Entity Envelope Fields
621
+ </h4>
622
+ </AccordionTrigger>
623
+ <AccordionContent>
624
+ <div className="rounded-md overflow-hidden border border-input mt-2">
625
+ <CodeEditor
626
+ value={generateSchemaYaml(kind.metadataSchema)}
627
+ language="yaml"
628
+ readOnly
629
+ onChange={() => {}}
630
+ minHeight={`${Math.max(100, generateSchemaYaml(kind.metadataSchema).split("\n").length * 20 + 20)}px`}
631
+ />
632
+ </div>
633
+ </AccordionContent>
634
+ </AccordionItem>
635
+
636
+ {/* Base Spec Schema */}
637
+ <AccordionItem value="base-spec" className="border-b-0">
638
+ <AccordionTrigger className="py-2 hover:no-underline">
639
+ <h4 className="text-sm font-medium text-muted-foreground">
640
+ Base Spec Schema
641
+ </h4>
642
+ </AccordionTrigger>
643
+ <AccordionContent>
644
+ <div className="rounded-md overflow-hidden border border-input mt-2">
645
+ <CodeEditor
646
+ value={generateSchemaYaml(kind.specSchema)}
647
+ language="yaml"
648
+ readOnly
649
+ onChange={() => {}}
650
+ minHeight={`${Math.max(100, generateSchemaYaml(kind.specSchema).split("\n").length * 20 + 20)}px`}
651
+ />
652
+ </div>
653
+ </AccordionContent>
654
+ </AccordionItem>
655
+ </Accordion>
621
656
 
622
657
  {/* Extensions */}
623
658
  {kind.extensions.length > 0 && (
@@ -636,8 +671,14 @@ function KindCard({ kind }: { kind: KindDescription }) {
636
671
  {ext.namespace}
637
672
  </Badge>
638
673
  </div>
639
- <div className="bg-muted rounded-md p-3 overflow-x-auto">
640
- <SchemaPropertyDisplay schema={ext.specSchema} />
674
+ <div className="rounded-md overflow-hidden border border-input">
675
+ <CodeEditor
676
+ value={generateSchemaYaml(ext.specSchema)}
677
+ language="yaml"
678
+ readOnly
679
+ onChange={() => {}}
680
+ minHeight={`${Math.max(100, generateSchemaYaml(ext.specSchema).split("\n").length * 20 + 20)}px`}
681
+ />
641
682
  </div>
642
683
  </div>
643
684
  ))}
@@ -649,6 +690,8 @@ function KindCard({ kind }: { kind: KindDescription }) {
649
690
  kind.specSchemaDocumentation.length > 0 && (
650
691
  <SpecSchemaDocumentationSection
651
692
  docs={kind.specSchemaDocumentation}
693
+ selections={selections}
694
+ onSelect={handleSelect}
652
695
  />
653
696
  )}
654
697