@checkstack/ui 1.11.0 → 1.13.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.
Files changed (71) hide show
  1. package/.storybook/main.ts +43 -0
  2. package/CHANGELOG.md +326 -0
  3. package/package.json +23 -18
  4. package/scripts/generate-stdlib-types.ts +23 -0
  5. package/src/components/Accordion.tsx +17 -9
  6. package/src/components/ActionCard.tsx +99 -11
  7. package/src/components/BrandIcon.tsx +57 -0
  8. package/src/components/CodeEditor/CodeEditor.tsx +159 -14
  9. package/src/components/CodeEditor/TypefoxEditor.tsx +537 -168
  10. package/src/components/CodeEditor/editorTheme.test.ts +41 -0
  11. package/src/components/CodeEditor/editorTheme.ts +26 -0
  12. package/src/components/CodeEditor/generated/builtin-modules.json +1 -0
  13. package/src/components/CodeEditor/importSpecifiers.test.ts +286 -0
  14. package/src/components/CodeEditor/importSpecifiers.ts +267 -0
  15. package/src/components/CodeEditor/index.ts +26 -0
  16. package/src/components/CodeEditor/monacoGuard.ts +76 -0
  17. package/src/components/CodeEditor/monacoTsService.ts +185 -0
  18. package/src/components/CodeEditor/popoutTitle.test.ts +37 -0
  19. package/src/components/CodeEditor/popoutTitle.ts +31 -0
  20. package/src/components/CodeEditor/scriptContext.test.ts +15 -7
  21. package/src/components/CodeEditor/scriptContext.ts +12 -18
  22. package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
  23. package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
  24. package/src/components/CodeEditor/types.ts +79 -0
  25. package/src/components/CodeEditor/validateScripts.ts +172 -0
  26. package/src/components/CodeEditor/vscodeServicesSignal.ts +72 -0
  27. package/src/components/ConfirmationModal.tsx +7 -1
  28. package/src/components/Dialog.tsx +32 -11
  29. package/src/components/DurationInput.tsx +121 -0
  30. package/src/components/DynamicForm/DynamicForm.tsx +119 -47
  31. package/src/components/DynamicForm/DynamicOptionsField.tsx +19 -14
  32. package/src/components/DynamicForm/FormField.tsx +183 -15
  33. package/src/components/DynamicForm/MultiTypeEditorField.tsx +78 -2
  34. package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
  35. package/src/components/DynamicForm/index.ts +20 -0
  36. package/src/components/DynamicForm/secretEnv.logic.test.ts +126 -0
  37. package/src/components/DynamicForm/secretEnv.logic.ts +87 -0
  38. package/src/components/DynamicForm/types.ts +134 -1
  39. package/src/components/DynamicForm/utils.test.ts +38 -0
  40. package/src/components/DynamicForm/utils.ts +54 -0
  41. package/src/components/DynamicForm/validation.logic.test.ts +255 -0
  42. package/src/components/DynamicForm/validation.logic.ts +210 -0
  43. package/src/components/DynamicIcon.tsx +39 -17
  44. package/src/components/Markdown.tsx +68 -2
  45. package/src/components/Popover.tsx +6 -1
  46. package/src/components/ScriptTestPanel.logic.test.ts +139 -0
  47. package/src/components/ScriptTestPanel.logic.ts +137 -0
  48. package/src/components/ScriptTestPanel.tsx +394 -0
  49. package/src/components/Sheet.tsx +21 -6
  50. package/src/components/Spinner.tsx +56 -0
  51. package/src/components/StatusBadge.tsx +78 -0
  52. package/src/components/StrategyConfigCard.tsx +3 -3
  53. package/src/components/Tabs.tsx +7 -1
  54. package/src/components/TimeOfDayInput.tsx +116 -0
  55. package/src/components/UserMenu.logic.test.ts +37 -0
  56. package/src/components/UserMenu.logic.ts +30 -0
  57. package/src/components/UserMenu.tsx +40 -12
  58. package/src/components/comboboxInteraction.ts +39 -0
  59. package/src/components/iconRegistry.tsx +27 -0
  60. package/src/components/portalContainer.ts +24 -0
  61. package/src/index.ts +7 -0
  62. package/stories/ActionCard.stories.tsx +60 -0
  63. package/stories/CodeEditor.stories.tsx +47 -2
  64. package/stories/DurationInput.stories.tsx +59 -0
  65. package/stories/Introduction.mdx +1 -1
  66. package/stories/Markdown.stories.tsx +56 -0
  67. package/stories/ScriptTestPanel.stories.tsx +106 -0
  68. package/stories/SecretEnvEditor.stories.tsx +80 -0
  69. package/stories/Spinner.stories.tsx +90 -0
  70. package/stories/TimeOfDayInput.stories.tsx +34 -0
  71. package/tsconfig.json +4 -0
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { Plus, Trash2 } from "lucide-react";
3
+ import { renderTemplatePreview } from "@checkstack/template-engine";
3
4
 
4
5
  import {
5
6
  Input,
@@ -14,13 +15,21 @@ import {
14
15
  Toggle,
15
16
  ColorPicker,
16
17
  TemplateValueInput,
18
+ DurationInput,
19
+ type DurationValue,
17
20
  } from "../../index";
18
21
 
19
22
  import type { FormFieldProps, JsonSchemaProperty } from "./types";
20
- import { getCleanDescription, NONE_SENTINEL } from "./utils";
23
+ import {
24
+ getCleanDescription,
25
+ NONE_SENTINEL,
26
+ findSecretEnvSibling,
27
+ nestedChildrenRequired,
28
+ } from "./utils";
21
29
  import { DynamicOptionsField } from "./DynamicOptionsField";
22
30
  import { JsonField } from "./JsonField";
23
31
  import { MultiTypeEditorField } from "./MultiTypeEditorField";
32
+ import { SecretEnvEditor } from "./SecretEnvEditor";
24
33
 
25
34
  /**
26
35
  * Recursive field renderer that handles all supported JSON Schema types.
@@ -38,6 +47,15 @@ export const FormField: React.FC<FormFieldProps> = ({
38
47
  typeDefinitions,
39
48
  shellEnvVars,
40
49
  starterTemplates,
50
+ scriptTestRenderer,
51
+ secretNames,
52
+ acquireTypes,
53
+ acquireResetKey,
54
+ sdkTypes,
55
+ sdkTypesResetKey,
56
+ importablePackages,
57
+ templatePreviewContext,
58
+ siblingSecretEnv,
41
59
  onChange,
42
60
  }) => {
43
61
  const description = propSchema.description || "";
@@ -74,6 +92,34 @@ export const FormField: React.FC<FormFieldProps> = ({
74
92
  return <></>;
75
93
  }
76
94
 
95
+ // Duration field — render the DurationInput (single-unit duration
96
+ // object). Marked via `x-duration: true` or `format: "duration"`. This
97
+ // branch is intentionally additive and sits before the generic union /
98
+ // object handlers so a `for:` / threshold-window config renders the
99
+ // widget rather than the raw oneOf discriminator picker.
100
+ const isDuration =
101
+ propSchema["x-duration"] === true || propSchema.format === "duration";
102
+ if (isDuration) {
103
+ const cleanDesc = getCleanDescription(description);
104
+ return (
105
+ <div className="space-y-2">
106
+ <div>
107
+ <Label htmlFor={id}>
108
+ {label} {isRequired && "*"}
109
+ </Label>
110
+ {cleanDesc && (
111
+ <p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
112
+ )}
113
+ </div>
114
+ <DurationInput
115
+ id={id}
116
+ value={value as DurationValue | undefined}
117
+ onChange={(next) => onChange(next)}
118
+ />
119
+ </div>
120
+ );
121
+ }
122
+
77
123
  // Enum handling
78
124
  if (propSchema.enum) {
79
125
  const cleanDesc = getCleanDescription(description);
@@ -126,19 +172,39 @@ export const FormField: React.FC<FormFieldProps> = ({
126
172
  const editorTypes = propSchema["x-editor-types"];
127
173
  if (editorTypes && editorTypes.length > 0) {
128
174
  return (
129
- <MultiTypeEditorField
130
- id={id}
131
- label={label}
132
- description={cleanDesc}
133
- value={value as string | undefined}
134
- isRequired={isRequired}
135
- editorTypes={editorTypes}
136
- templateProperties={templateProperties}
137
- typeDefinitions={typeDefinitions}
138
- shellEnvVars={shellEnvVars}
139
- starterTemplates={starterTemplates}
140
- onChange={onChange as (val: string | undefined) => void}
141
- />
175
+ <div className="space-y-2">
176
+ <MultiTypeEditorField
177
+ id={id}
178
+ label={label}
179
+ description={cleanDesc}
180
+ value={value as string | undefined}
181
+ isRequired={isRequired}
182
+ editorTypes={editorTypes}
183
+ templateProperties={templateProperties}
184
+ typeDefinitions={typeDefinitions}
185
+ shellEnvVars={shellEnvVars}
186
+ starterTemplates={starterTemplates}
187
+ scriptTestRenderer={
188
+ propSchema["x-script-testable"] === true
189
+ ? scriptTestRenderer
190
+ : undefined
191
+ }
192
+ acquireTypes={acquireTypes}
193
+ acquireResetKey={acquireResetKey}
194
+ sdkTypes={sdkTypes}
195
+ sdkTypesResetKey={sdkTypesResetKey}
196
+ importablePackages={importablePackages}
197
+ fieldId={id}
198
+ siblingSecretEnv={siblingSecretEnv}
199
+ onChange={onChange as (val: string | undefined) => void}
200
+ />
201
+ {propSchema["x-templatable"] && templatePreviewContext && (
202
+ <TemplatePreviewLine
203
+ value={(value as string) || ""}
204
+ context={templatePreviewContext}
205
+ />
206
+ )}
207
+ </div>
142
208
  );
143
209
  }
144
210
 
@@ -268,6 +334,12 @@ export const FormField: React.FC<FormFieldProps> = ({
268
334
  placeholder={placeholder}
269
335
  />
270
336
  )}
337
+ {propSchema["x-templatable"] && templatePreviewContext && (
338
+ <TemplatePreviewLine
339
+ value={(value as string) || ""}
340
+ context={templatePreviewContext}
341
+ />
342
+ )}
271
343
  </div>
272
344
  );
273
345
  }
@@ -331,6 +403,34 @@ export const FormField: React.FC<FormFieldProps> = ({
331
403
  }
332
404
 
333
405
  // Dictionary/Record (headers)
406
+ // Secret -> env mapping: a dedicated editor (env name + secret-name
407
+ // picker) instead of the raw JSON record fallback.
408
+ if (
409
+ propSchema.type === "object" &&
410
+ propSchema.additionalProperties &&
411
+ propSchema["x-secret-env"]
412
+ ) {
413
+ const cleanDesc = getCleanDescription(description);
414
+ return (
415
+ <div className="space-y-2">
416
+ <div>
417
+ <Label htmlFor={id}>
418
+ {label} {isRequired && "*"}
419
+ </Label>
420
+ {cleanDesc && (
421
+ <p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
422
+ )}
423
+ </div>
424
+ <SecretEnvEditor
425
+ id={id}
426
+ value={(value as Record<string, string> | undefined) ?? {}}
427
+ secretNames={secretNames}
428
+ onChange={(next) => onChange(next)}
429
+ />
430
+ </div>
431
+ );
432
+ }
433
+
334
434
  if (propSchema.type === "object" && propSchema.additionalProperties) {
335
435
  const cleanDesc = getCleanDescription(description);
336
436
  return (
@@ -355,6 +455,20 @@ export const FormField: React.FC<FormFieldProps> = ({
355
455
 
356
456
  // Object (Nested Form)
357
457
  if (propSchema.type === "object" && propSchema.properties) {
458
+ // Resolve the secret→env sibling within THIS object so a nested
459
+ // testable script field forwards the right mapping to the test panel.
460
+ const nestedSecretEnv = findSecretEnvSibling({
461
+ properties: propSchema.properties,
462
+ values: value as Record<string, unknown> | undefined,
463
+ });
464
+ // An OPTIONAL nested object (e.g. an opt-in spend cap) only marks its
465
+ // schema-required children with `*` once the operator starts providing the
466
+ // object; while empty, supplying it is optional. A required object always
467
+ // marks them. (See nestedChildrenRequired.)
468
+ const childrenRequired = nestedChildrenRequired({
469
+ objectRequired: isRequired ?? false,
470
+ objectValue: value,
471
+ });
358
472
  return (
359
473
  <div className="space-y-4 p-4 border rounded-lg bg-muted/30">
360
474
  <p className="text-sm font-semibold">{label}</p>
@@ -365,7 +479,7 @@ export const FormField: React.FC<FormFieldProps> = ({
365
479
  label={key.charAt(0).toUpperCase() + key.slice(1)}
366
480
  propSchema={subSchema}
367
481
  value={(value as Record<string, unknown>)?.[key]}
368
- isRequired={propSchema.required?.includes(key)}
482
+ isRequired={childrenRequired && (propSchema.required?.includes(key) ?? false)}
369
483
  formValues={formValues}
370
484
  optionsResolvers={optionsResolvers}
371
485
  templateProperties={templateProperties}
@@ -373,6 +487,15 @@ export const FormField: React.FC<FormFieldProps> = ({
373
487
  typeDefinitions={typeDefinitions}
374
488
  shellEnvVars={shellEnvVars}
375
489
  starterTemplates={starterTemplates}
490
+ scriptTestRenderer={scriptTestRenderer}
491
+ secretNames={secretNames}
492
+ acquireTypes={acquireTypes}
493
+ acquireResetKey={acquireResetKey}
494
+ sdkTypes={sdkTypes}
495
+ sdkTypesResetKey={sdkTypesResetKey}
496
+ importablePackages={importablePackages}
497
+ templatePreviewContext={templatePreviewContext}
498
+ siblingSecretEnv={nestedSecretEnv}
376
499
  onChange={(val) =>
377
500
  onChange({ ...(value as Record<string, unknown>), [key]: val })
378
501
  }
@@ -485,6 +608,14 @@ export const FormField: React.FC<FormFieldProps> = ({
485
608
  typeDefinitions={typeDefinitions}
486
609
  shellEnvVars={shellEnvVars}
487
610
  starterTemplates={starterTemplates}
611
+ scriptTestRenderer={scriptTestRenderer}
612
+ secretNames={secretNames}
613
+ acquireTypes={acquireTypes}
614
+ acquireResetKey={acquireResetKey}
615
+ sdkTypes={sdkTypes}
616
+ sdkTypesResetKey={sdkTypesResetKey}
617
+ importablePackages={importablePackages}
618
+ templatePreviewContext={templatePreviewContext}
488
619
  onChange={(val) => {
489
620
  const next = [...(items as unknown[])];
490
621
  next[index] = val;
@@ -543,6 +674,12 @@ export const FormField: React.FC<FormFieldProps> = ({
543
674
  const displayDiscriminatorField =
544
675
  discriminatorField.charAt(0).toUpperCase() + discriminatorField.slice(1);
545
676
 
677
+ // Secret→env sibling within the selected variant's object.
678
+ const variantSecretEnv = findSecretEnvSibling({
679
+ properties: selectedVariant.properties,
680
+ values: currentValue,
681
+ });
682
+
546
683
  return (
547
684
  <div className="space-y-3 p-3 border rounded-lg bg-background">
548
685
  {/* Discriminator selector */}
@@ -617,6 +754,15 @@ export const FormField: React.FC<FormFieldProps> = ({
617
754
  typeDefinitions={typeDefinitions}
618
755
  shellEnvVars={shellEnvVars}
619
756
  starterTemplates={starterTemplates}
757
+ scriptTestRenderer={scriptTestRenderer}
758
+ secretNames={secretNames}
759
+ acquireTypes={acquireTypes}
760
+ acquireResetKey={acquireResetKey}
761
+ sdkTypes={sdkTypes}
762
+ sdkTypesResetKey={sdkTypesResetKey}
763
+ importablePackages={importablePackages}
764
+ templatePreviewContext={templatePreviewContext}
765
+ siblingSecretEnv={variantSecretEnv}
620
766
  onChange={(val) => onChange({ ...currentValue, [key]: val })}
621
767
  />
622
768
  ))}
@@ -627,6 +773,28 @@ export const FormField: React.FC<FormFieldProps> = ({
627
773
  return <></>;
628
774
  };
629
775
 
776
+ /**
777
+ * Inline preview of a templatable field's rendered output against a sample
778
+ * context. Shown below `x-templatable` string fields when the owning form
779
+ * supplies a `templatePreviewContext`. Pure render (no DOM/Monaco), so it
780
+ * matches the run-time `x-templatable` pass exactly.
781
+ */
782
+ const TemplatePreviewLine: React.FC<{
783
+ value: string;
784
+ context: Record<string, unknown>;
785
+ }> = ({ value, context }) => {
786
+ if (!value || !value.includes("{{")) return null;
787
+ const rendered = renderTemplatePreview({ value, context });
788
+ return (
789
+ <p className="text-xs text-muted-foreground">
790
+ Preview:{" "}
791
+ <span className="font-mono break-all text-foreground">
792
+ {rendered || "(empty)"}
793
+ </span>
794
+ </p>
795
+ );
796
+ };
797
+
630
798
  /**
631
799
  * Shared visibility toggle button for secret fields.
632
800
  */
@@ -1,7 +1,12 @@
1
1
  import React from "react";
2
2
  import { Label } from "../Label";
3
3
  import { Textarea } from "../Textarea";
4
- import { CodeEditor, type TemplateProperty } from "../CodeEditor";
4
+ import {
5
+ CodeEditor,
6
+ type TemplateProperty,
7
+ type AcquireTypes,
8
+ type AcquiredTypeFile,
9
+ } from "../CodeEditor";
5
10
  import {
6
11
  Select,
7
12
  SelectContent,
@@ -17,7 +22,11 @@ import {
17
22
  parseFormData,
18
23
  EDITOR_TYPE_LABELS,
19
24
  } from "./utils";
20
- import type { EditorStarterTemplates, ShellEnvVar } from "./types";
25
+ import type {
26
+ EditorStarterTemplates,
27
+ ScriptTestRenderer,
28
+ ShellEnvVar,
29
+ } from "./types";
21
30
  import { pickStarterForEmptyField } from "./starterTemplateSelector";
22
31
 
23
32
  export interface MultiTypeEditorFieldProps {
@@ -54,6 +63,38 @@ export interface MultiTypeEditorFieldProps {
54
63
  * working example instead of a blank canvas.
55
64
  */
56
65
  starterTemplates?: EditorStarterTemplates;
66
+ /**
67
+ * Optional renderer for the inline script-test panel. When supplied (the
68
+ * field is `x-script-testable`), it is rendered beneath the editor for
69
+ * `typescript` / `shell` modes, with the currently-selected language as
70
+ * `kind`. The owning page wires it to the `testScript` RPC.
71
+ */
72
+ scriptTestRenderer?: ScriptTestRenderer;
73
+ /**
74
+ * Optional lazy type-acquisition resolver, forwarded to the TS/JS
75
+ * `CodeEditor` so imported npm packages autocomplete (lazy ATA).
76
+ */
77
+ acquireTypes?: AcquireTypes;
78
+ /** Install identity (lockfile hash); resets acquired types on a new install. */
79
+ acquireResetKey?: string;
80
+ /** The running release's `@checkstack/sdk` editor bundle (helpers + client). */
81
+ sdkTypes?: ReadonlyArray<AcquiredTypeFile>;
82
+ /** Release version; resets the mounted SDK libs on a deployment upgrade. */
83
+ sdkTypesResetKey?: string;
84
+ /**
85
+ * Importable installed package names (`@types/*`-free), forwarded to the
86
+ * TS/JS `CodeEditor` so the import specifier itself autocompletes.
87
+ */
88
+ importablePackages?: string[];
89
+ /** Form key of this field, forwarded to {@link scriptTestRenderer}. */
90
+ fieldId?: string;
91
+ /**
92
+ * Current value of the sibling `x-secret-env` mapping (located by
93
+ * annotation in the parent config object), forwarded to
94
+ * {@link scriptTestRenderer} so the test panel injects placeholders /
95
+ * overrides for the same secrets the action declares.
96
+ */
97
+ siblingSecretEnv?: Record<string, string>;
57
98
  /** Callback when value changes */
58
99
  onChange: (value: string | undefined) => void;
59
100
  }
@@ -74,6 +115,14 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
74
115
  typeDefinitions,
75
116
  shellEnvVars,
76
117
  starterTemplates,
118
+ scriptTestRenderer,
119
+ acquireTypes,
120
+ acquireResetKey,
121
+ sdkTypes,
122
+ sdkTypesResetKey,
123
+ importablePackages,
124
+ fieldId,
125
+ siblingSecretEnv,
77
126
  onChange,
78
127
  }) => {
79
128
  // Detect initial editor type from value
@@ -352,6 +401,11 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
352
401
  language="javascript"
353
402
  minHeight="150px"
354
403
  typeDefinitions={typeDefinitions}
404
+ acquireTypes={acquireTypes}
405
+ acquireResetKey={acquireResetKey}
406
+ sdkTypes={sdkTypes}
407
+ sdkTypesResetKey={sdkTypesResetKey}
408
+ importablePackages={importablePackages}
355
409
  />
356
410
  )}
357
411
 
@@ -363,6 +417,11 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
363
417
  language="typescript"
364
418
  minHeight="150px"
365
419
  typeDefinitions={typeDefinitions}
420
+ acquireTypes={acquireTypes}
421
+ acquireResetKey={acquireResetKey}
422
+ sdkTypes={sdkTypes}
423
+ sdkTypesResetKey={sdkTypesResetKey}
424
+ importablePackages={importablePackages}
366
425
  />
367
426
  )}
368
427
 
@@ -376,6 +435,23 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
376
435
  shellEnvVars={shellEnvVars}
377
436
  />
378
437
  )}
438
+
439
+ {/* Inline script-test panel for testable code fields. Shell maps to
440
+ the `shell` test kind; both typescript and javascript map to the
441
+ `typescript` runner kind (the ESM runner handles both). */}
442
+ {scriptTestRenderer !== undefined &&
443
+ (selectedType === "typescript" ||
444
+ selectedType === "javascript" ||
445
+ selectedType === "shell") &&
446
+ scriptTestRenderer({
447
+ fieldId: fieldId ?? id,
448
+ kind: selectedType === "shell" ? "shell" : "typescript",
449
+ script: value ?? "",
450
+ secretEnv:
451
+ siblingSecretEnv && Object.keys(siblingSecretEnv).length > 0
452
+ ? siblingSecretEnv
453
+ : undefined,
454
+ })}
379
455
  </div>
380
456
  );
381
457
  };