@checkstack/automation-frontend 0.2.0 → 0.3.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 +352 -0
- package/package.json +13 -9
- package/src/components/AutomationGroupCombobox.tsx +133 -0
- package/src/editor/ActionEditor.tsx +180 -90
- package/src/editor/ActionListEditor.tsx +27 -1
- package/src/editor/AddActionDialog.tsx +15 -45
- package/src/editor/AddConditionDialog.tsx +86 -0
- package/src/editor/AddTriggerDialog.tsx +97 -0
- package/src/editor/AutomationDefinitionEditor.tsx +41 -2
- package/src/editor/ConditionEditor.tsx +359 -70
- package/src/editor/ConditionsEditor.tsx +113 -44
- package/src/editor/ItemSheet.tsx +51 -0
- package/src/editor/RunReplayPicker.tsx +97 -0
- package/src/editor/ScriptServicesBooter.tsx +53 -0
- package/src/editor/ScriptTestRenderer.tsx +150 -0
- package/src/editor/SystemEntityPicker.test.ts +37 -0
- package/src/editor/SystemEntityPicker.tsx +109 -0
- package/src/editor/TriggersEditor.tsx +345 -137
- package/src/editor/action-helpers.test.ts +107 -0
- package/src/editor/action-helpers.ts +72 -0
- package/src/editor/action-leaf-cards.tsx +98 -1
- package/src/editor/condition-kind.test.ts +126 -0
- package/src/editor/condition-kind.ts +130 -0
- package/src/editor/item-summary.test.ts +171 -0
- package/src/editor/item-summary.ts +210 -0
- package/src/editor/picker-dialog.tsx +156 -0
- package/src/editor/registry-context.tsx +9 -2
- package/src/editor/script-actions.test.ts +184 -0
- package/src/editor/script-actions.ts +146 -0
- package/src/editor/system-entity-picker.logic.ts +23 -0
- package/src/editor/template-completion.test.ts +22 -3
- package/src/editor/template-completion.ts +16 -8
- package/src/editor/template-helpers.ts +4 -0
- package/src/editor/trigger-helpers.test.ts +28 -0
- package/src/editor/trigger-helpers.ts +17 -0
- package/src/editor/useScriptDiagnostics.ts +108 -0
- package/src/index.tsx +2 -0
- package/src/pages/AutomationEditPage.tsx +95 -47
- package/src/pages/AutomationListPage.tsx +172 -123
- package/src/pages/automation-grouping.test.ts +86 -0
- package/src/pages/automation-grouping.ts +65 -0
- package/src/script-context.test.ts +142 -1
- package/src/script-context.ts +115 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionInfo,
|
|
3
|
+
ActionInput,
|
|
4
|
+
ActionPath,
|
|
5
|
+
ActionPathStep,
|
|
6
|
+
AutomationDefinition,
|
|
7
|
+
} from "@checkstack/automation-common";
|
|
8
|
+
import type { EditorType } from "@checkstack/common";
|
|
9
|
+
import { actionPathToDefPath, defPathKey } from "./editor-validation";
|
|
10
|
+
import { secretEnvEnvNames } from "../script-context";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A single inline-script action found in a definition, with everything the
|
|
14
|
+
* headless validator + issue mapping need - independent of whether the action's
|
|
15
|
+
* editor card is mounted/expanded. `collectScriptActions` walks the whole
|
|
16
|
+
* action tree (including collapsed and nested actions) so every script is
|
|
17
|
+
* validated, not just the one on screen.
|
|
18
|
+
*/
|
|
19
|
+
export interface ScriptActionRef {
|
|
20
|
+
/** Stable key (the issue-path joined) used to correlate validator results. */
|
|
21
|
+
id: string;
|
|
22
|
+
/** Editor path to this action - drives `generateAutomationContextTypes`. */
|
|
23
|
+
path: ActionPath;
|
|
24
|
+
/** Definition path of the script field, e.g. `["actions",0,"config","script"]`. */
|
|
25
|
+
issuePath: Array<string | number>;
|
|
26
|
+
/** The script source. */
|
|
27
|
+
source: string;
|
|
28
|
+
language: "typescript" | "javascript";
|
|
29
|
+
/** Declared `secretEnv` var names, for the `process.env.*` augmentation. */
|
|
30
|
+
secretEnvNames: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Narrow an `unknown` to a plain (non-array) object record. */
|
|
34
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
35
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
36
|
+
// The lone cast: `typeof === "object"` cannot itself produce the index
|
|
37
|
+
// signature, and re-validating an arbitrary JSON-schema/config record with
|
|
38
|
+
// zod here would be wasteful. Mirrors the same helper in `script-context`.
|
|
39
|
+
return value as Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find the script field in an action's config JSON schema by the
|
|
46
|
+
* `x-editor-types` annotation (NOT a hard-coded field name) and report the
|
|
47
|
+
* field key + the TS/JS language it should validate as. Returns undefined for
|
|
48
|
+
* actions with no typescript/javascript editor field (e.g. a shell action, or
|
|
49
|
+
* a non-script provider action).
|
|
50
|
+
*/
|
|
51
|
+
function scriptFieldOf(
|
|
52
|
+
configSchema: Record<string, unknown>,
|
|
53
|
+
): { fieldKey: string; language: "typescript" | "javascript" } | undefined {
|
|
54
|
+
const properties = asRecord(configSchema.properties);
|
|
55
|
+
if (!properties) return undefined;
|
|
56
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
57
|
+
const editorTypes = asRecord(prop)?.["x-editor-types"];
|
|
58
|
+
if (!Array.isArray(editorTypes)) continue;
|
|
59
|
+
const types = new Set(
|
|
60
|
+
editorTypes.filter((t): t is EditorType => typeof t === "string"),
|
|
61
|
+
);
|
|
62
|
+
if (types.has("typescript")) {
|
|
63
|
+
return { fieldKey: key, language: "typescript" };
|
|
64
|
+
}
|
|
65
|
+
if (types.has("javascript")) {
|
|
66
|
+
return { fieldKey: key, language: "javascript" };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Walk an automation definition's whole action tree and return every inline
|
|
74
|
+
* TS/JS script action with a non-empty source. The `actions` registry supplies
|
|
75
|
+
* each action's config schema (so the script field + secretEnv mapping are
|
|
76
|
+
* located by annotation, never by a hard-coded key).
|
|
77
|
+
*
|
|
78
|
+
* The walk mirrors the editor's `ActionPath` slot grammar exactly, so the
|
|
79
|
+
* derived definition paths line up with both the backend validator's issue
|
|
80
|
+
* paths and `useActionIssues` card lookups.
|
|
81
|
+
*/
|
|
82
|
+
export function collectScriptActions({
|
|
83
|
+
definition,
|
|
84
|
+
actions,
|
|
85
|
+
}: {
|
|
86
|
+
definition: AutomationDefinition;
|
|
87
|
+
actions: ActionInfo[];
|
|
88
|
+
}): ScriptActionRef[] {
|
|
89
|
+
const infoById = new Map(actions.map((action) => [action.qualifiedId, action]));
|
|
90
|
+
const refs: ScriptActionRef[] = [];
|
|
91
|
+
|
|
92
|
+
const visit = (
|
|
93
|
+
list: ActionInput[],
|
|
94
|
+
parentPath: ActionPath,
|
|
95
|
+
slot: ActionPathStep["slot"],
|
|
96
|
+
whenIndex?: number,
|
|
97
|
+
): void => {
|
|
98
|
+
for (const [index, action] of list.entries()) {
|
|
99
|
+
const step: ActionPathStep =
|
|
100
|
+
whenIndex === undefined ? { slot, index } : { slot, whenIndex, index };
|
|
101
|
+
const path: ActionPath = [...parentPath, step];
|
|
102
|
+
|
|
103
|
+
if ("action" in action) {
|
|
104
|
+
const info = infoById.get(action.action);
|
|
105
|
+
const field = info ? scriptFieldOf(info.configSchema) : undefined;
|
|
106
|
+
if (info && field) {
|
|
107
|
+
const config = asRecord(action.config) ?? {};
|
|
108
|
+
const raw = config[field.fieldKey];
|
|
109
|
+
const source = typeof raw === "string" ? raw : "";
|
|
110
|
+
if (source.trim() !== "") {
|
|
111
|
+
const issuePath = [
|
|
112
|
+
...actionPathToDefPath(path),
|
|
113
|
+
"config",
|
|
114
|
+
field.fieldKey,
|
|
115
|
+
];
|
|
116
|
+
refs.push({
|
|
117
|
+
id: defPathKey(issuePath),
|
|
118
|
+
path,
|
|
119
|
+
issuePath,
|
|
120
|
+
source,
|
|
121
|
+
language: field.language,
|
|
122
|
+
secretEnvNames: secretEnvEnvNames({
|
|
123
|
+
configSchema: info.configSchema,
|
|
124
|
+
config: action.config,
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} else if ("choose" in action) {
|
|
130
|
+
for (const [whenIdx, branch] of action.choose.entries()) {
|
|
131
|
+
visit(branch.sequence, path, "choose-when", whenIdx);
|
|
132
|
+
}
|
|
133
|
+
if (action.else) visit(action.else, path, "choose-else");
|
|
134
|
+
} else if ("parallel" in action) {
|
|
135
|
+
visit(action.parallel, path, "parallel");
|
|
136
|
+
} else if ("repeat" in action) {
|
|
137
|
+
visit(action.repeat.sequence, path, "repeat");
|
|
138
|
+
} else if ("sequence" in action) {
|
|
139
|
+
visit(action.sequence, path, "sequence");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
visit(definition.actions, [], "root");
|
|
145
|
+
return refs;
|
|
146
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure logic for {@link SystemEntityPicker}, extracted so the manual-entry
|
|
3
|
+
* fallback decision can be unit-tested without rendering the catalog-backed
|
|
4
|
+
* picker (which pulls in `@checkstack/ui` / Monaco).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Decides whether the picker should open in manual-entry mode for a given
|
|
9
|
+
* `entity` value and the set of known catalog system ids. Templates and ids
|
|
10
|
+
* that aren't in the catalog drop to manual entry so they still round-trip;
|
|
11
|
+
* a blank value or a known system id uses the live picker.
|
|
12
|
+
*/
|
|
13
|
+
export function shouldStartManual({
|
|
14
|
+
value,
|
|
15
|
+
knownSystemIds,
|
|
16
|
+
}: {
|
|
17
|
+
value: string;
|
|
18
|
+
knownSystemIds: ReadonlySet<string>;
|
|
19
|
+
}): boolean {
|
|
20
|
+
if (value.length === 0) return false;
|
|
21
|
+
if (value.includes("{{")) return true;
|
|
22
|
+
return !knownSystemIds.has(value);
|
|
23
|
+
}
|
|
@@ -216,10 +216,29 @@ describe("createTemplateCompletionProvider — expression mode", () => {
|
|
|
216
216
|
it("treats the whole value as an expression (no {{ needed)", () => {
|
|
217
217
|
const result = provider({ value: "trigger.payload.sev", cursor: 19 });
|
|
218
218
|
expect(result!.heading).toBe("Fields");
|
|
219
|
-
// Field insert in expression mode
|
|
220
|
-
//
|
|
219
|
+
// Field insert in expression mode inserts the BARE path with NO trailing
|
|
220
|
+
// space and no braces — a path is a complete expression on its own. The
|
|
221
|
+
// operator stage only fires after a MANUAL space, so picking a field
|
|
222
|
+
// doesn't pop comparators/filters and imply a comparison is required.
|
|
221
223
|
const severity = result!.items.find((i) => i.label.includes("severity"));
|
|
222
|
-
expect(severity?.insertText).toBe("trigger.payload.severity
|
|
224
|
+
expect(severity?.insertText).toBe("trigger.payload.severity");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("does NOT auto-advance to operators after a field is inserted", () => {
|
|
228
|
+
// No trailing space (as the field insert now leaves it) → still the field
|
|
229
|
+
// stage, not the operator stage. Operators require a manual space.
|
|
230
|
+
const noSpace = provider({
|
|
231
|
+
value: "trigger.payload.severity",
|
|
232
|
+
cursor: "trigger.payload.severity".length,
|
|
233
|
+
});
|
|
234
|
+
expect(noSpace!.heading).toBe("Fields");
|
|
235
|
+
|
|
236
|
+
// A manual trailing space advances to the operator stage.
|
|
237
|
+
const withSpace = provider({
|
|
238
|
+
value: "trigger.payload.severity ",
|
|
239
|
+
cursor: "trigger.payload.severity ".length,
|
|
240
|
+
});
|
|
241
|
+
expect(withSpace!.heading).toBe("Operators");
|
|
223
242
|
});
|
|
224
243
|
|
|
225
244
|
it("suggests enum values after a comparator", () => {
|
|
@@ -494,6 +494,7 @@ export function createTemplateCompletionProvider(
|
|
|
494
494
|
replaceStart,
|
|
495
495
|
replaceEnd,
|
|
496
496
|
appendClose: mode === "template" && !window.closed,
|
|
497
|
+
mode,
|
|
497
498
|
});
|
|
498
499
|
}
|
|
499
500
|
case "operator": {
|
|
@@ -515,8 +516,9 @@ function fieldResult(args: {
|
|
|
515
516
|
replaceStart: number;
|
|
516
517
|
replaceEnd: number;
|
|
517
518
|
appendClose: boolean;
|
|
519
|
+
mode: "template" | "expression";
|
|
518
520
|
}): TemplateCompletionResult | null {
|
|
519
|
-
const { stage, fields, replaceStart, replaceEnd, appendClose } = args;
|
|
521
|
+
const { stage, fields, replaceStart, replaceEnd, appendClose, mode } = args;
|
|
520
522
|
const q = stage.query.toLowerCase();
|
|
521
523
|
// Match + insert in templateRef space (what gets written into `{{ }}`).
|
|
522
524
|
// Also match against the canonical `path` so typing the dotted form
|
|
@@ -533,13 +535,11 @@ function fieldResult(args: {
|
|
|
533
535
|
replaceStart,
|
|
534
536
|
replaceEnd,
|
|
535
537
|
items: matches.map((f) => {
|
|
536
|
-
// Always append a trailing space so the caret lands in
|
|
537
|
-
// whitespace after the field — that re-opens completion in the
|
|
538
|
-
// operator stage (comparators / filters) automatically, the same
|
|
539
|
-
// way picking a comparator advances to the value stage.
|
|
540
538
|
if (appendClose) {
|
|
541
|
-
// Unclosed `{{` —
|
|
542
|
-
// caret after the space but before `}}`
|
|
539
|
+
// Unclosed `{{` (template mode) — append a space + the closing
|
|
540
|
+
// braces and land the caret after the space but before `}}`
|
|
541
|
+
// (offset -2 into ` }}`); the space re-opens the operator stage,
|
|
542
|
+
// the natural next step inside an interpolation.
|
|
543
543
|
return {
|
|
544
544
|
label: f.templateRef,
|
|
545
545
|
detail: f.type,
|
|
@@ -548,11 +548,19 @@ function fieldResult(args: {
|
|
|
548
548
|
caretOffset: -2,
|
|
549
549
|
} satisfies TemplateCompletionItem;
|
|
550
550
|
}
|
|
551
|
+
// Expression mode (bare condition / filter / partitionBy): a path is a
|
|
552
|
+
// complete expression on its own, so DON'T auto-append a trailing space.
|
|
553
|
+
// The operator stage only requires whitespace, so auto-spacing would pop
|
|
554
|
+
// comparators/filters at the user the instant they pick a field and imply
|
|
555
|
+
// a comparison is mandatory. Inserting the bare path closes completion;
|
|
556
|
+
// the user types a space MANUALLY only when they actually want an operator
|
|
557
|
+
// (then the operator stage fires). Template mode keeps the auto-space
|
|
558
|
+
// field → operator flow.
|
|
551
559
|
return {
|
|
552
560
|
label: f.templateRef,
|
|
553
561
|
detail: f.type,
|
|
554
562
|
description: f.description,
|
|
555
|
-
insertText: `${f.templateRef} `,
|
|
563
|
+
insertText: mode === "expression" ? f.templateRef : `${f.templateRef} `,
|
|
556
564
|
caretOffset: 0,
|
|
557
565
|
} satisfies TemplateCompletionItem;
|
|
558
566
|
}),
|
|
@@ -25,6 +25,10 @@ export const TEMPLATE_FILTERS: readonly CompletionFilter[] = [
|
|
|
25
25
|
{ name: "join", signature: "separator", description: 'Join an array (default ", ").', hasArgs: true },
|
|
26
26
|
{ name: "replace", signature: "search, replacement", description: "Replace every occurrence.", hasArgs: true },
|
|
27
27
|
{ name: "not", description: "Negate truthiness." },
|
|
28
|
+
{ name: "minutes", description: "A number of minutes, as milliseconds (e.g. 30 | minutes)." },
|
|
29
|
+
{ name: "hours", description: "A number of hours, as milliseconds (e.g. 2 | hours)." },
|
|
30
|
+
{ name: "duration_since", description: "Milliseconds elapsed since an ISO timestamp." },
|
|
31
|
+
{ name: "older_than", signature: "thresholdMs", description: "True when an ISO timestamp is at least N ms in the past (e.g. ts | older_than(30 | minutes)).", hasArgs: true },
|
|
28
32
|
];
|
|
29
33
|
|
|
30
34
|
/**
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
assignDefaultTriggerIds,
|
|
5
5
|
collectTriggerIds,
|
|
6
6
|
defaultTriggerId,
|
|
7
|
+
makeTrigger,
|
|
7
8
|
} from "./trigger-helpers";
|
|
8
9
|
|
|
9
10
|
describe("trigger-helpers", () => {
|
|
@@ -55,4 +56,31 @@ describe("trigger-helpers", () => {
|
|
|
55
56
|
expect(out[0]!.id).toBe("incident_created");
|
|
56
57
|
expect(out[1]!.id).toBe("incident_created_2");
|
|
57
58
|
});
|
|
59
|
+
|
|
60
|
+
// The trigger type picker builds a trigger via `makeTrigger`; it must carry
|
|
61
|
+
// the picked event and a default id that is unique against existing siblings.
|
|
62
|
+
it("makeTrigger sets the picked event and a derived default id", () => {
|
|
63
|
+
expect(makeTrigger({ event: "incident.created", taken: new Set() })).toEqual(
|
|
64
|
+
{ event: "incident.created", id: "incident_created" },
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("makeTrigger dedupes its default id against existing trigger ids", () => {
|
|
69
|
+
const out = makeTrigger({
|
|
70
|
+
event: "incident.created",
|
|
71
|
+
taken: new Set(["incident_created"]),
|
|
72
|
+
});
|
|
73
|
+
expect(out.event).toBe("incident.created");
|
|
74
|
+
expect(out.id).toBe("incident_created_2");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Regression for the editor-load fix: a stored/seeded definition (e.g. the
|
|
78
|
+
// "Auto-incident: ... flapping" seed) whose trigger carries no `id` must be
|
|
79
|
+
// materialized to its derived id on load, so the Id field shows it without
|
|
80
|
+
// the operator focusing + blurring the input first.
|
|
81
|
+
it("materializes the derived id for an id-less seeded trigger on load", () => {
|
|
82
|
+
const stored: Trigger[] = [{ event: "healthcheck.flapping_detected" }];
|
|
83
|
+
const out = assignDefaultTriggerIds(stored);
|
|
84
|
+
expect(out[0]!.id).toBe("healthcheck_flapping_detected");
|
|
85
|
+
});
|
|
58
86
|
});
|
|
@@ -48,6 +48,23 @@ export function defaultTriggerId(
|
|
|
48
48
|
return uniqueTriggerId(suggestTriggerIdBase(trigger), taken);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Build a fresh trigger subscribed to `event` with a unique, log-friendly
|
|
53
|
+
* default `id` (deduped against `taken`). Used by the trigger type picker so a
|
|
54
|
+
* newly-added trigger is immediately referenceable as `trigger.id` rather than
|
|
55
|
+
* appearing blank.
|
|
56
|
+
*/
|
|
57
|
+
export function makeTrigger({
|
|
58
|
+
event,
|
|
59
|
+
taken,
|
|
60
|
+
}: {
|
|
61
|
+
event: string;
|
|
62
|
+
taken: Set<string>;
|
|
63
|
+
}): Trigger {
|
|
64
|
+
const base: Trigger = { event };
|
|
65
|
+
return { ...base, id: defaultTriggerId(base, taken) };
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
/**
|
|
52
69
|
* Return a copy of `triggers` with a stable, unique id assigned to every
|
|
53
70
|
* trigger that does not already have one. Existing ids are preserved and seed
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { AutomationDefinition } from "@checkstack/automation-common";
|
|
3
|
+
import { onVscodeServicesReady, validateTypeScriptSources } from "@checkstack/ui";
|
|
4
|
+
import {
|
|
5
|
+
generateAutomationContextTypes,
|
|
6
|
+
generateSecretEnvTypes,
|
|
7
|
+
} from "../script-context";
|
|
8
|
+
import { useAutomationRegistry } from "./registry-context";
|
|
9
|
+
import { collectScriptActions } from "./script-actions";
|
|
10
|
+
import type { DefinitionIssue } from "./editor-validation";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Continuously type-check every inline TS/JS script action in `definition`
|
|
14
|
+
* against its generated `context` types - even the ones whose editor cards are
|
|
15
|
+
* collapsed or never opened - and surface the results as `DefinitionIssue`s so
|
|
16
|
+
* they flow through the same `ValidationProvider` / per-card badge path as
|
|
17
|
+
* structural validation.
|
|
18
|
+
*
|
|
19
|
+
* The check runs in the browser via the headless `validateTypeScriptSources`
|
|
20
|
+
* (the same standalone TS worker the editor uses), so it needs no backend
|
|
21
|
+
* round-trip. It is debounced and generation-guarded so a burst of edits
|
|
22
|
+
* collapses to one pass and a stale async result never overwrites a newer one.
|
|
23
|
+
*
|
|
24
|
+
* NOTE: this only covers the automation currently open in the editor. Scripts
|
|
25
|
+
* in OTHER automations, or definitions authored via YAML/API, are not seen here
|
|
26
|
+
* - that platform-wide coverage is the (deferred) backend typecheck's job.
|
|
27
|
+
*/
|
|
28
|
+
export function useScriptDiagnostics({
|
|
29
|
+
definition,
|
|
30
|
+
}: {
|
|
31
|
+
definition: AutomationDefinition;
|
|
32
|
+
}): DefinitionIssue[] {
|
|
33
|
+
const { triggers, actions, artifactTypes } = useAutomationRegistry();
|
|
34
|
+
const [issues, setIssues] = React.useState<DefinitionIssue[]>([]);
|
|
35
|
+
const generationRef = React.useRef(0);
|
|
36
|
+
|
|
37
|
+
// The validator no-ops until an editor initializes the monaco-vscode services
|
|
38
|
+
// (it must never init them itself). Bumping this when services become ready
|
|
39
|
+
// re-runs the effect below, so opening any one script editor immediately
|
|
40
|
+
// validates ALL scripts - including the still-collapsed ones.
|
|
41
|
+
const [servicesReadyTick, setServicesReadyTick] = React.useState(0);
|
|
42
|
+
React.useEffect(
|
|
43
|
+
() => onVscodeServicesReady(() => setServicesReadyTick((tick) => tick + 1)),
|
|
44
|
+
[],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
React.useEffect(() => {
|
|
48
|
+
const generation = ++generationRef.current;
|
|
49
|
+
const handle = setTimeout(() => {
|
|
50
|
+
const refs = collectScriptActions({ definition, actions });
|
|
51
|
+
if (refs.length === 0) {
|
|
52
|
+
if (generation === generationRef.current) setIssues([]);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Each script validates against the SAME merged type defs the editor
|
|
57
|
+
// shows it: the scope-derived `context` union for the action's position,
|
|
58
|
+
// plus the `process.env.*` augmentation for its declared secrets.
|
|
59
|
+
const sources = refs.map((ref) => {
|
|
60
|
+
const { typeDefinitions } = generateAutomationContextTypes({
|
|
61
|
+
definition,
|
|
62
|
+
triggers,
|
|
63
|
+
actions: actions.map((action) => ({
|
|
64
|
+
qualifiedId: action.qualifiedId,
|
|
65
|
+
produces: action.produces,
|
|
66
|
+
})),
|
|
67
|
+
artifactTypes,
|
|
68
|
+
path: ref.path,
|
|
69
|
+
});
|
|
70
|
+
const secretEnvLib = generateSecretEnvTypes({
|
|
71
|
+
envNames: ref.secretEnvNames,
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
id: ref.id,
|
|
75
|
+
source: ref.source,
|
|
76
|
+
typeDefinitions: [typeDefinitions, secretEnvLib]
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.join("\n\n"),
|
|
79
|
+
language: ref.language,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
void validateTypeScriptSources({ sources })
|
|
84
|
+
.then((byId) => {
|
|
85
|
+
if (generation !== generationRef.current) return;
|
|
86
|
+
const next: DefinitionIssue[] = [];
|
|
87
|
+
for (const ref of refs) {
|
|
88
|
+
for (const diagnostic of byId.get(ref.id) ?? []) {
|
|
89
|
+
next.push({
|
|
90
|
+
path: ref.issuePath,
|
|
91
|
+
message: `Line ${diagnostic.line}: ${diagnostic.message}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
setIssues(next);
|
|
96
|
+
})
|
|
97
|
+
.catch(() => {
|
|
98
|
+
// Validator failure (e.g. worker unavailable) must never break the
|
|
99
|
+
// editor; just leave script issues empty for this pass.
|
|
100
|
+
if (generation === generationRef.current) setIssues([]);
|
|
101
|
+
});
|
|
102
|
+
}, 400);
|
|
103
|
+
|
|
104
|
+
return () => clearTimeout(handle);
|
|
105
|
+
}, [definition, triggers, actions, artifactTypes, servicesReadyTick]);
|
|
106
|
+
|
|
107
|
+
return issues;
|
|
108
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -17,6 +17,8 @@ import { AutomationMenuItems } from "./components/AutomationMenuItems";
|
|
|
17
17
|
|
|
18
18
|
export {
|
|
19
19
|
generateAutomationContextTypes,
|
|
20
|
+
generateSecretEnvTypes,
|
|
21
|
+
secretEnvEnvNames,
|
|
20
22
|
type GenerateAutomationContextTypesInput,
|
|
21
23
|
type GenerateAutomationContextTypesResult,
|
|
22
24
|
} from "./script-context";
|