@checkstack/ui 1.10.0 → 1.12.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/.storybook/main.ts +43 -0
- package/CHANGELOG.md +565 -0
- package/package.json +15 -7
- package/scripts/generate-stdlib-types.ts +25 -2
- package/src/components/ActionCard.tsx +309 -0
- package/src/components/CodeEditor/CodeEditor.tsx +132 -9
- package/src/components/CodeEditor/TypefoxEditor.tsx +1024 -0
- package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
- package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
- package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
- package/src/components/CodeEditor/generated/builtin-modules.json +1 -0
- package/src/components/CodeEditor/importSpecifiers.test.ts +286 -0
- package/src/components/CodeEditor/importSpecifiers.ts +267 -0
- package/src/components/CodeEditor/index.ts +26 -0
- package/src/components/CodeEditor/monacoTsService.ts +217 -0
- package/src/components/CodeEditor/popoutTitle.test.ts +37 -0
- package/src/components/CodeEditor/popoutTitle.ts +31 -0
- package/src/components/CodeEditor/scriptContext.test.ts +41 -0
- package/src/components/CodeEditor/scriptContext.ts +76 -1
- package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
- package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
- package/src/components/CodeEditor/templateValidation.ts +51 -0
- package/src/components/CodeEditor/types.ts +168 -0
- package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
- package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
- package/src/components/CodeEditor/validateScripts.ts +132 -0
- package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
- package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
- package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
- package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
- package/src/components/Dialog.tsx +32 -11
- package/src/components/DurationInput.tsx +121 -0
- package/src/components/DynamicForm/DynamicForm.tsx +27 -1
- package/src/components/DynamicForm/FormField.tsx +138 -10
- package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +83 -9
- package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
- package/src/components/DynamicForm/index.ts +6 -0
- package/src/components/DynamicForm/secretEnv.logic.test.ts +126 -0
- package/src/components/DynamicForm/secretEnv.logic.ts +87 -0
- package/src/components/DynamicForm/types.ts +83 -1
- package/src/components/DynamicForm/utils.ts +32 -0
- package/src/components/Popover.tsx +6 -1
- package/src/components/ScriptTestPanel.logic.test.ts +139 -0
- package/src/components/ScriptTestPanel.logic.ts +137 -0
- package/src/components/ScriptTestPanel.tsx +394 -0
- package/src/components/Sheet.tsx +21 -6
- package/src/components/TemplateInput.tsx +104 -0
- package/src/components/TemplateInputToggle.tsx +111 -0
- package/src/components/TemplateValueInput.test.ts +98 -0
- package/src/components/TemplateValueInput.tsx +470 -0
- package/src/components/TimeOfDayInput.tsx +116 -0
- package/src/components/VariablePicker.tsx +271 -0
- package/src/components/comboboxInteraction.ts +39 -0
- package/src/components/portalContainer.ts +24 -0
- package/src/hooks/useInitOnceForKey.test.ts +27 -0
- package/src/hooks/useInitOnceForKey.ts +21 -18
- package/src/index.ts +9 -0
- package/stories/ActionCard.stories.tsx +122 -0
- package/stories/Alert.stories.tsx +5 -5
- package/stories/CodeEditor.stories.tsx +47 -2
- package/stories/DurationInput.stories.tsx +59 -0
- package/stories/ScriptTestPanel.stories.tsx +106 -0
- package/stories/SecretEnvEditor.stories.tsx +80 -0
- package/stories/TemplateInputToggle.stories.tsx +77 -0
- package/stories/TemplateValueInput.stories.tsx +65 -0
- package/stories/TimeOfDayInput.stories.tsx +34 -0
- package/stories/VariablePicker.stories.tsx +109 -0
- package/tsconfig.json +1 -0
- package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
- package/src/components/CodeEditor/monacoStdlib.ts +0 -62
- package/src/components/CodeEditor/monacoWorkers.ts +0 -118
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Template-aware JSON validation. See templateValidation.ts for the approach.
|
|
2
|
+
// The real VS Code JSON service still provides highlighting + completion; only
|
|
3
|
+
// its built-in (raw-text) validation is turned off in favour of this, so `{{ }}`
|
|
4
|
+
// is tolerated in any position (including unquoted, e.g. a numeric value).
|
|
5
|
+
import { parse, printParseErrorCode, type ParseError } from "jsonc-parser";
|
|
6
|
+
import {
|
|
7
|
+
substituteTemplates,
|
|
8
|
+
type TemplateDiagnostic,
|
|
9
|
+
} from "./templateValidation";
|
|
10
|
+
|
|
11
|
+
/** Validate template-bearing JSON. Empty content is allowed (no diagnostics). */
|
|
12
|
+
export const validateJsonTemplate = (
|
|
13
|
+
text: string,
|
|
14
|
+
): TemplateDiagnostic[] => {
|
|
15
|
+
const errors: ParseError[] = [];
|
|
16
|
+
parse(substituteTemplates(text), errors, {
|
|
17
|
+
allowEmptyContent: true,
|
|
18
|
+
allowTrailingComma: false,
|
|
19
|
+
disallowComments: true,
|
|
20
|
+
});
|
|
21
|
+
return errors.map((parseError) => ({
|
|
22
|
+
offset: parseError.offset,
|
|
23
|
+
length: Math.max(parseError.length, 1),
|
|
24
|
+
message: `JSON: ${printParseErrorCode(parseError.error)}`,
|
|
25
|
+
}));
|
|
26
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Headless TypeScript / JavaScript script validator.
|
|
2
|
+
//
|
|
3
|
+
// Type-checks user scripts against their generated `context` types WITHOUT a
|
|
4
|
+
// mounted editor, so an automation's collapsed-card scripts (or any script the
|
|
5
|
+
// user isn't currently looking at) still surface type errors. It drives the
|
|
6
|
+
// SAME standalone TS worker the editor uses, via off-screen models.
|
|
7
|
+
//
|
|
8
|
+
// Browser-only (imports `monaco` + the worker accessor). The pure mapping /
|
|
9
|
+
// offset logic lives in `scriptDiagnostics.ts` and is unit-tested there; this
|
|
10
|
+
// module is the thin async glue and is intentionally not unit-tested (no DOM /
|
|
11
|
+
// worker under bun).
|
|
12
|
+
//
|
|
13
|
+
// Why prepend instead of an extra-lib: a global `context` extra-lib would
|
|
14
|
+
// collide with any open editor's own `declare const context` (extra-libs are
|
|
15
|
+
// global to the shared service). Prepending the type defs onto each validated
|
|
16
|
+
// source keeps `context` scoped to that one off-screen file. See
|
|
17
|
+
// `buildValidationSource`.
|
|
18
|
+
import * as monaco from "@codingame/monaco-vscode-editor-api";
|
|
19
|
+
import {
|
|
20
|
+
getJavaScriptWorker,
|
|
21
|
+
getTypeScriptWorker,
|
|
22
|
+
} from "@codingame/monaco-vscode-standalone-typescript-language-features";
|
|
23
|
+
import {
|
|
24
|
+
areVscodeServicesReady,
|
|
25
|
+
ensureStandaloneStdlib,
|
|
26
|
+
} from "./monacoTsService";
|
|
27
|
+
import {
|
|
28
|
+
buildValidationSource,
|
|
29
|
+
mapWorkerDiagnostics,
|
|
30
|
+
type RawTsDiagnostic,
|
|
31
|
+
type ScriptDiagnostic,
|
|
32
|
+
} from "./scriptDiagnostics";
|
|
33
|
+
|
|
34
|
+
export type { ScriptDiagnostic } from "./scriptDiagnostics";
|
|
35
|
+
|
|
36
|
+
export interface ScriptValidationInput {
|
|
37
|
+
/** Caller-chosen identity; the returned map is keyed by this. */
|
|
38
|
+
id: string;
|
|
39
|
+
/** The user's script source (without any generated type prefix). */
|
|
40
|
+
source: string;
|
|
41
|
+
/** Generated `context.d.ts` (+ ambient augmentations) for this script. */
|
|
42
|
+
typeDefinitions: string;
|
|
43
|
+
language: "typescript" | "javascript";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Monotonic across overlapping validation passes so two passes never reuse the
|
|
47
|
+
// same off-screen model URI (which would race on create/dispose).
|
|
48
|
+
let runCounter = 0;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validate each script against its `typeDefinitions`. Returns a map keyed by
|
|
52
|
+
* input `id` to that script's diagnostics (empty array = clean). Never throws:
|
|
53
|
+
* any worker/setup failure resolves to no diagnostics so validation can never
|
|
54
|
+
* break the editor it augments.
|
|
55
|
+
*
|
|
56
|
+
* Runs serially. The inline-prepend strategy removes the shared-state
|
|
57
|
+
* constraint that would otherwise force serialization, but validation is not
|
|
58
|
+
* latency-critical and serial keeps worker load predictable.
|
|
59
|
+
*/
|
|
60
|
+
export async function validateTypeScriptSources({
|
|
61
|
+
sources,
|
|
62
|
+
}: {
|
|
63
|
+
sources: ScriptValidationInput[];
|
|
64
|
+
}): Promise<Map<string, ScriptDiagnostic[]>> {
|
|
65
|
+
const results = new Map<string, ScriptDiagnostic[]>();
|
|
66
|
+
if (sources.length === 0) {
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
// CRITICAL: only proceed once an editor has initialized the monaco-vscode
|
|
70
|
+
// services. Initializing them here would collide with the editor wrapper's
|
|
71
|
+
// one-time init ("Services are already initialized") and break the editor.
|
|
72
|
+
if (!areVscodeServicesReady()) {
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
await ensureStandaloneStdlib();
|
|
77
|
+
} catch {
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const input of sources) {
|
|
82
|
+
try {
|
|
83
|
+
results.set(input.id, await validateOne(input));
|
|
84
|
+
} catch {
|
|
85
|
+
results.set(input.id, []);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function validateOne(
|
|
92
|
+
input: ScriptValidationInput,
|
|
93
|
+
): Promise<ScriptDiagnostic[]> {
|
|
94
|
+
const { text, prependedLineCount } = buildValidationSource({
|
|
95
|
+
typeDefinitions: input.typeDefinitions,
|
|
96
|
+
source: input.source,
|
|
97
|
+
});
|
|
98
|
+
const ext = input.language === "javascript" ? "js" : "ts";
|
|
99
|
+
const uri = monaco.Uri.parse(
|
|
100
|
+
`file:///script-validation/${runCounter++}.${ext}`,
|
|
101
|
+
);
|
|
102
|
+
monaco.editor.getModel(uri)?.dispose();
|
|
103
|
+
const model = monaco.editor.createModel(text, input.language, uri);
|
|
104
|
+
try {
|
|
105
|
+
const getWorker =
|
|
106
|
+
input.language === "javascript"
|
|
107
|
+
? await getJavaScriptWorker()
|
|
108
|
+
: await getTypeScriptWorker();
|
|
109
|
+
const client = await getWorker(uri);
|
|
110
|
+
const fileName = uri.toString();
|
|
111
|
+
const [syntactic, semantic] = await Promise.all([
|
|
112
|
+
client.getSyntacticDiagnostics(fileName),
|
|
113
|
+
client.getSemanticDiagnostics(fileName),
|
|
114
|
+
]);
|
|
115
|
+
const diagnostics: RawTsDiagnostic[] = [...syntactic, ...semantic].map(
|
|
116
|
+
(diagnostic) => ({
|
|
117
|
+
start: diagnostic.start,
|
|
118
|
+
length: diagnostic.length,
|
|
119
|
+
messageText: diagnostic.messageText,
|
|
120
|
+
category: diagnostic.category,
|
|
121
|
+
code: diagnostic.code,
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
return mapWorkerDiagnostics({
|
|
125
|
+
diagnostics,
|
|
126
|
+
validationText: text,
|
|
127
|
+
prependedLineCount,
|
|
128
|
+
});
|
|
129
|
+
} finally {
|
|
130
|
+
model.dispose();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { validateXmlTemplate } from "./validateXmlTemplate";
|
|
3
|
+
|
|
4
|
+
describe("validateXmlTemplate", () => {
|
|
5
|
+
it("accepts valid XML", () => {
|
|
6
|
+
expect(validateXmlTemplate("<root><a>1</a></root>")).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("accepts empty content", () => {
|
|
10
|
+
expect(validateXmlTemplate("")).toEqual([]);
|
|
11
|
+
expect(validateXmlTemplate(" \n ")).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("accepts a template in element text", () => {
|
|
15
|
+
expect(
|
|
16
|
+
validateXmlTemplate("<root><repo>{{trigger.payload.repository}}</repo></root>"),
|
|
17
|
+
).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("accepts a template in a (quoted) attribute value", () => {
|
|
21
|
+
expect(validateXmlTemplate('<root attr="{{id}}"></root>')).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("flags a mismatched closing tag", () => {
|
|
25
|
+
const diagnostics = validateXmlTemplate("<a><b></a>");
|
|
26
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
27
|
+
expect(diagnostics[0]?.message).toContain("XML:");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("flags an unclosed tag even with templates present", () => {
|
|
31
|
+
const diagnostics = validateXmlTemplate("<root><x>{{v}}</root>");
|
|
32
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Template-aware XML validation. See templateValidation.ts for the approach.
|
|
2
|
+
// There is no @codingame standalone XML language service (only Monarch
|
|
3
|
+
// highlighting), so we use fast-xml-parser's validator after template
|
|
4
|
+
// substitution. The validator reports the FIRST error only, with 1-based
|
|
5
|
+
// line/column, which we convert to an offset/range in the original text.
|
|
6
|
+
import { XMLValidator } from "fast-xml-parser";
|
|
7
|
+
import {
|
|
8
|
+
lineColumnToOffset,
|
|
9
|
+
substituteTemplates,
|
|
10
|
+
type TemplateDiagnostic,
|
|
11
|
+
} from "./templateValidation";
|
|
12
|
+
|
|
13
|
+
/** Validate template-bearing XML. Empty content is allowed (no diagnostics). */
|
|
14
|
+
export const validateXmlTemplate = (
|
|
15
|
+
text: string,
|
|
16
|
+
): TemplateDiagnostic[] => {
|
|
17
|
+
const substituted = substituteTemplates(text);
|
|
18
|
+
if (substituted.trim() === "") {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const result = XMLValidator.validate(substituted, {
|
|
22
|
+
allowBooleanAttributes: true,
|
|
23
|
+
});
|
|
24
|
+
if (result === true) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const { msg, line, col } = result.err;
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
offset: lineColumnToOffset({ text: substituted, line, column: col }),
|
|
31
|
+
length: 1,
|
|
32
|
+
message: `XML: ${msg}`,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { validateYamlTemplate } from "./validateYamlTemplate";
|
|
3
|
+
|
|
4
|
+
describe("validateYamlTemplate", () => {
|
|
5
|
+
it("accepts valid YAML", () => {
|
|
6
|
+
expect(validateYamlTemplate("a: 1\nb: two\n")).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("accepts empty content", () => {
|
|
10
|
+
expect(validateYamlTemplate("")).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("accepts a template as a quoted string value", () => {
|
|
14
|
+
expect(validateYamlTemplate('repo: "{{trigger.payload.repository}}"')).toEqual(
|
|
15
|
+
[],
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("accepts an UNQUOTED template value (e.g. a number)", () => {
|
|
20
|
+
expect(validateYamlTemplate("timeout: {{trigger.payload.timeout}}")).toEqual(
|
|
21
|
+
[],
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("accepts a template as a list item", () => {
|
|
26
|
+
expect(validateYamlTemplate("items:\n - {{count}}\n - 2")).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("flags tabs used as indentation", () => {
|
|
30
|
+
const diagnostics = validateYamlTemplate("foo:\n\tbar: 1");
|
|
31
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
32
|
+
expect(diagnostics[0]?.message).toContain("YAML:");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("flags a genuine structural error (with templates present)", () => {
|
|
36
|
+
const diagnostics = validateYamlTemplate("a: {{x}}\n b: 2");
|
|
37
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Template-aware YAML validation. See templateValidation.ts for the approach.
|
|
2
|
+
// There is no @codingame standalone YAML language service (only Monarch
|
|
3
|
+
// highlighting), so we parse with the `yaml` package after template
|
|
4
|
+
// substitution. YAML is permissive (it often parses `{{ }}` as a flow
|
|
5
|
+
// collection), but substitution gives accurate structural validation and still
|
|
6
|
+
// catches real mistakes (bad indentation, tabs, duplicate keys, ...).
|
|
7
|
+
import { parseDocument } from "yaml";
|
|
8
|
+
import {
|
|
9
|
+
substituteTemplates,
|
|
10
|
+
type TemplateDiagnostic,
|
|
11
|
+
} from "./templateValidation";
|
|
12
|
+
|
|
13
|
+
/** Validate template-bearing YAML. */
|
|
14
|
+
export const validateYamlTemplate = (
|
|
15
|
+
text: string,
|
|
16
|
+
): TemplateDiagnostic[] => {
|
|
17
|
+
const doc = parseDocument(substituteTemplates(text), {
|
|
18
|
+
prettyErrors: false,
|
|
19
|
+
});
|
|
20
|
+
return doc.errors.map((error) => {
|
|
21
|
+
const [start, end] = error.pos;
|
|
22
|
+
return {
|
|
23
|
+
offset: start,
|
|
24
|
+
length: Math.max(end - start, 1),
|
|
25
|
+
message: `YAML: ${error.message}`,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
};
|
|
@@ -4,6 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import { cn } from "../utils";
|
|
6
6
|
import { usePerformance } from "./PerformanceProvider";
|
|
7
|
+
import { PortalContainerContext } from "./portalContainer";
|
|
7
8
|
|
|
8
9
|
const Dialog = DialogPrimitive.Root;
|
|
9
10
|
|
|
@@ -64,12 +65,23 @@ const DialogContent = React.forwardRef<
|
|
|
64
65
|
DialogContentProps & DialogContentExtraProps
|
|
65
66
|
>(({ className, children, size, hideCloseButton, ...props }, ref) => {
|
|
66
67
|
const { isLowPower } = usePerformance();
|
|
68
|
+
// Expose the content element so popovers/comboboxes inside the dialog portal
|
|
69
|
+
// INTO it, otherwise the modal scroll-lock blocks their internal scrolling.
|
|
70
|
+
const [content, setContent] = React.useState<HTMLDivElement | null>(null);
|
|
71
|
+
const setRefs = React.useCallback(
|
|
72
|
+
(node: HTMLDivElement | null) => {
|
|
73
|
+
setContent(node);
|
|
74
|
+
if (typeof ref === "function") ref(node);
|
|
75
|
+
else if (ref) ref.current = node;
|
|
76
|
+
},
|
|
77
|
+
[ref],
|
|
78
|
+
);
|
|
67
79
|
return (
|
|
68
80
|
<DialogPortal>
|
|
69
81
|
<DialogOverlay />
|
|
70
82
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
|
|
71
83
|
<DialogPrimitive.Content
|
|
72
|
-
ref={
|
|
84
|
+
ref={setRefs}
|
|
73
85
|
className={cn(
|
|
74
86
|
"pointer-events-auto relative",
|
|
75
87
|
dialogContentVariants({ size }),
|
|
@@ -79,16 +91,25 @@ const DialogContent = React.forwardRef<
|
|
|
79
91
|
)}
|
|
80
92
|
{...props}
|
|
81
93
|
>
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
</
|
|
91
|
-
|
|
94
|
+
<PortalContainerContext.Provider value={content}>
|
|
95
|
+
{/* `min-h-0 flex-1` lets this wrapper fill the height when a
|
|
96
|
+
consumer makes `DialogContent` a tall flex column (e.g. the
|
|
97
|
+
CodeEditor popout, so a `fillHeight` editor fills the body).
|
|
98
|
+
Inert for the default (non-flex) dialog: `flex-1` only affects
|
|
99
|
+
flex items, and `min-h-0` is the block default. */}
|
|
100
|
+
<div className="-mx-2 px-2 flex min-h-0 flex-1 flex-col gap-6">
|
|
101
|
+
{children}
|
|
102
|
+
</div>
|
|
103
|
+
{!hideCloseButton && (
|
|
104
|
+
<DialogPrimitive.Close
|
|
105
|
+
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
106
|
+
aria-label="Close"
|
|
107
|
+
>
|
|
108
|
+
<X className="h-4 w-4" />
|
|
109
|
+
<span className="sr-only">Close</span>
|
|
110
|
+
</DialogPrimitive.Close>
|
|
111
|
+
)}
|
|
112
|
+
</PortalContainerContext.Provider>
|
|
92
113
|
</DialogPrimitive.Content>
|
|
93
114
|
</div>
|
|
94
115
|
</DialogPortal>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Input } from "./Input";
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectTrigger,
|
|
8
|
+
SelectValue,
|
|
9
|
+
} from "./Select";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The duration units this input edits. Mirrors the single-unit object
|
|
13
|
+
* form the automation `Duration` schema accepts
|
|
14
|
+
* (`{ seconds } | { minutes } | { hours }`).
|
|
15
|
+
*/
|
|
16
|
+
export type DurationUnit = "seconds" | "minutes" | "hours";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A single-unit duration value: `{ seconds: 30 }`, `{ minutes: 5 }`, etc.
|
|
20
|
+
* This is exactly the object form the backend `Duration` schema accepts,
|
|
21
|
+
* so a `DurationInput` round-trips losslessly to/from the stored value.
|
|
22
|
+
*/
|
|
23
|
+
export type DurationValue = Partial<Record<DurationUnit, number>>;
|
|
24
|
+
|
|
25
|
+
export interface DurationInputProps {
|
|
26
|
+
/** Current duration, or undefined when unset. */
|
|
27
|
+
value: DurationValue | undefined;
|
|
28
|
+
onChange: (next: DurationValue | undefined) => void;
|
|
29
|
+
/** Default unit when the operator first types a value. Defaults to "minutes". */
|
|
30
|
+
defaultUnit?: DurationUnit;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
id?: string;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const UNIT_LABELS: Record<DurationUnit, string> = {
|
|
37
|
+
seconds: "seconds",
|
|
38
|
+
minutes: "minutes",
|
|
39
|
+
hours: "hours",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Read the (unit, amount) pair out of a single-unit duration value. */
|
|
43
|
+
function decompose(
|
|
44
|
+
value: DurationValue | undefined,
|
|
45
|
+
fallbackUnit: DurationUnit,
|
|
46
|
+
): { unit: DurationUnit; amount: number | undefined } {
|
|
47
|
+
if (value) {
|
|
48
|
+
for (const unit of ["seconds", "minutes", "hours"] as const) {
|
|
49
|
+
const amount = value[unit];
|
|
50
|
+
if (amount !== undefined) return { unit, amount };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { unit: fallbackUnit, amount: undefined };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Number + unit picker for a single-unit duration (`for:` dwells,
|
|
58
|
+
* threshold windows, poll intervals). Emits the object form the backend
|
|
59
|
+
* `Duration` schema accepts, so it round-trips losslessly through YAML.
|
|
60
|
+
*
|
|
61
|
+
* No animations or heavy effects, so no `usePerformance` gating is needed
|
|
62
|
+
* - it is a plain numeric input plus a unit `Select`.
|
|
63
|
+
*/
|
|
64
|
+
export const DurationInput: React.FC<DurationInputProps> = ({
|
|
65
|
+
value,
|
|
66
|
+
onChange,
|
|
67
|
+
defaultUnit = "minutes",
|
|
68
|
+
disabled,
|
|
69
|
+
id,
|
|
70
|
+
className,
|
|
71
|
+
}) => {
|
|
72
|
+
const { unit, amount } = decompose(value, defaultUnit);
|
|
73
|
+
|
|
74
|
+
const emit = (nextUnit: DurationUnit, nextAmount: number | undefined) => {
|
|
75
|
+
const cleared: DurationValue | undefined = undefined;
|
|
76
|
+
if (nextAmount === undefined || !Number.isFinite(nextAmount)) {
|
|
77
|
+
onChange(cleared);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
onChange({ [nextUnit]: Math.max(0, Math.floor(nextAmount)) });
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className={`flex items-center gap-2 ${className ?? ""}`}>
|
|
85
|
+
<Input
|
|
86
|
+
id={id}
|
|
87
|
+
type="number"
|
|
88
|
+
min={1}
|
|
89
|
+
inputMode="numeric"
|
|
90
|
+
className="w-24"
|
|
91
|
+
value={amount ?? ""}
|
|
92
|
+
disabled={disabled}
|
|
93
|
+
placeholder="0"
|
|
94
|
+
onChange={(event) =>
|
|
95
|
+
emit(
|
|
96
|
+
unit,
|
|
97
|
+
event.target.value === ""
|
|
98
|
+
? undefined
|
|
99
|
+
: Number(event.target.value),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
/>
|
|
103
|
+
<Select
|
|
104
|
+
value={unit}
|
|
105
|
+
disabled={disabled}
|
|
106
|
+
onValueChange={(nextUnit) => emit(nextUnit as DurationUnit, amount)}
|
|
107
|
+
>
|
|
108
|
+
<SelectTrigger className="w-32">
|
|
109
|
+
<SelectValue />
|
|
110
|
+
</SelectTrigger>
|
|
111
|
+
<SelectContent>
|
|
112
|
+
{(["seconds", "minutes", "hours"] as const).map((u) => (
|
|
113
|
+
<SelectItem key={u} value={u}>
|
|
114
|
+
{UNIT_LABELS[u]}
|
|
115
|
+
</SelectItem>
|
|
116
|
+
))}
|
|
117
|
+
</SelectContent>
|
|
118
|
+
</Select>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
@@ -3,7 +3,12 @@ import React from "react";
|
|
|
3
3
|
import { EmptyState } from "../../index";
|
|
4
4
|
|
|
5
5
|
import type { DynamicFormProps } from "./types";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
extractDefaults,
|
|
8
|
+
isValueEmpty,
|
|
9
|
+
isFieldHiddenByCondition,
|
|
10
|
+
findSecretEnvSibling,
|
|
11
|
+
} from "./utils";
|
|
7
12
|
import { FormField } from "./FormField";
|
|
8
13
|
|
|
9
14
|
/**
|
|
@@ -18,9 +23,15 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
|
|
|
18
23
|
onValidChange,
|
|
19
24
|
optionsResolvers,
|
|
20
25
|
templateProperties,
|
|
26
|
+
templateCompletionProvider,
|
|
21
27
|
typeDefinitions,
|
|
22
28
|
shellEnvVars,
|
|
23
29
|
starterTemplates,
|
|
30
|
+
scriptTestRenderer,
|
|
31
|
+
secretNames,
|
|
32
|
+
acquireTypes,
|
|
33
|
+
acquireResetKey,
|
|
34
|
+
importablePackages,
|
|
24
35
|
}) => {
|
|
25
36
|
// Track previous validity to avoid redundant callbacks
|
|
26
37
|
const prevValidRef = React.useRef<boolean | undefined>(undefined);
|
|
@@ -87,6 +98,14 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
|
|
|
87
98
|
);
|
|
88
99
|
}
|
|
89
100
|
|
|
101
|
+
// The script field and its `x-secret-env` field are top-level siblings in
|
|
102
|
+
// an action config, so resolve the mapping once at the root and forward it
|
|
103
|
+
// to every field; a testable script field passes it to the test panel.
|
|
104
|
+
const rootSecretEnv = findSecretEnvSibling({
|
|
105
|
+
properties: schema.properties,
|
|
106
|
+
values: value,
|
|
107
|
+
});
|
|
108
|
+
|
|
90
109
|
return (
|
|
91
110
|
<div className="space-y-6">
|
|
92
111
|
{Object.entries(schema.properties)
|
|
@@ -116,9 +135,16 @@ export const DynamicForm: React.FC<DynamicFormProps> = ({
|
|
|
116
135
|
formValues={value}
|
|
117
136
|
optionsResolvers={optionsResolvers}
|
|
118
137
|
templateProperties={templateProperties}
|
|
138
|
+
templateCompletionProvider={templateCompletionProvider}
|
|
119
139
|
typeDefinitions={typeDefinitions}
|
|
120
140
|
shellEnvVars={shellEnvVars}
|
|
121
141
|
starterTemplates={starterTemplates}
|
|
142
|
+
scriptTestRenderer={scriptTestRenderer}
|
|
143
|
+
secretNames={secretNames}
|
|
144
|
+
acquireTypes={acquireTypes}
|
|
145
|
+
acquireResetKey={acquireResetKey}
|
|
146
|
+
importablePackages={importablePackages}
|
|
147
|
+
siblingSecretEnv={rootSecretEnv}
|
|
122
148
|
onChange={(val) => onChange({ ...value, [key]: val })}
|
|
123
149
|
/>
|
|
124
150
|
);
|