@checkstack/automation-frontend 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.
- package/CHANGELOG.md +664 -0
- package/package.json +38 -0
- package/src/components/AutomationMenuItems.tsx +37 -0
- package/src/editor/ActionEditor.tsx +367 -0
- package/src/editor/ActionListEditor.tsx +203 -0
- package/src/editor/AddActionDialog.tsx +225 -0
- package/src/editor/AutomationDefinitionContext.tsx +37 -0
- package/src/editor/AutomationDefinitionEditor.tsx +99 -0
- package/src/editor/ConditionEditor.tsx +218 -0
- package/src/editor/ConditionsEditor.tsx +89 -0
- package/src/editor/ItemPicker.tsx +147 -0
- package/src/editor/TriggersEditor.tsx +269 -0
- package/src/editor/action-composite-cards.tsx +390 -0
- package/src/editor/action-helpers.ts +365 -0
- package/src/editor/action-leaf-cards.tsx +426 -0
- package/src/editor/editor-validation.test.ts +95 -0
- package/src/editor/editor-validation.tsx +200 -0
- package/src/editor/registry-context.tsx +192 -0
- package/src/editor/template-completion.test.ts +412 -0
- package/src/editor/template-completion.ts +664 -0
- package/src/editor/template-helpers.test.ts +145 -0
- package/src/editor/template-helpers.ts +95 -0
- package/src/editor/trigger-helpers.test.ts +58 -0
- package/src/editor/trigger-helpers.ts +67 -0
- package/src/editor/useConnectionOptionResolvers.ts +80 -0
- package/src/editor/yaml-markers.ts +116 -0
- package/src/index.tsx +95 -0
- package/src/pages/AutomationEditPage.tsx +567 -0
- package/src/pages/AutomationListPage.tsx +304 -0
- package/src/pages/RunDetailPage.tsx +333 -0
- package/src/pages/RunsPage.tsx +233 -0
- package/src/pages/TemplatePlaygroundPage.tsx +224 -0
- package/src/script-context.test.ts +247 -0
- package/src/script-context.ts +218 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ActionPath, ActionPathStep } from "@checkstack/automation-common";
|
|
3
|
+
|
|
4
|
+
export interface DefinitionIssue {
|
|
5
|
+
path: Array<string | number>;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert an editor `ActionPath` (slot/index segments) into the
|
|
11
|
+
* definition path the validator reports against (e.g.
|
|
12
|
+
* `["actions", 0, "choose", 1, "sequence", 0]`), so a card can find its
|
|
13
|
+
* own issues.
|
|
14
|
+
*/
|
|
15
|
+
export function actionPathToDefPath(
|
|
16
|
+
path: ActionPath,
|
|
17
|
+
): Array<string | number> {
|
|
18
|
+
return path.flatMap((step) => stepToDefSegments(step));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stepToDefSegments(step: ActionPathStep): Array<string | number> {
|
|
22
|
+
switch (step.slot) {
|
|
23
|
+
case "root": {
|
|
24
|
+
return ["actions", step.index];
|
|
25
|
+
}
|
|
26
|
+
case "choose-when": {
|
|
27
|
+
return ["choose", step.whenIndex ?? 0, "sequence", step.index];
|
|
28
|
+
}
|
|
29
|
+
case "choose-else": {
|
|
30
|
+
return ["else", step.index];
|
|
31
|
+
}
|
|
32
|
+
case "parallel": {
|
|
33
|
+
return ["parallel", step.index];
|
|
34
|
+
}
|
|
35
|
+
case "repeat": {
|
|
36
|
+
return ["repeat", "sequence", step.index];
|
|
37
|
+
}
|
|
38
|
+
case "sequence": {
|
|
39
|
+
return ["sequence", step.index];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function defPathKey(path: Array<string | number>): string {
|
|
45
|
+
return path.join("\u0000");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse an issue path into the deepest *action node* that owns it plus
|
|
50
|
+
* the field tail within that action. Walks the action-node grammar
|
|
51
|
+
* (`actions[i]`, then repeating `choose[j].sequence[k]` / `else[k]` /
|
|
52
|
+
* `parallel[k]` / `repeat.sequence[k]` / `sequence[k]`) greedily; the
|
|
53
|
+
* first segment that doesn't continue the grammar starts the field
|
|
54
|
+
* tail. So `actions.0.config.level` → owner `actions.0`, tail
|
|
55
|
+
* `config.level`; `actions.0.choose.0.when` → owner `actions.0`, tail
|
|
56
|
+
* `choose.0.when` (the choose's own condition); a nested provider's
|
|
57
|
+
* config attaches to the nested card.
|
|
58
|
+
*
|
|
59
|
+
* Returns null for non-action issues (triggers / conditions / top-level).
|
|
60
|
+
*/
|
|
61
|
+
function resolveActionOwner(
|
|
62
|
+
path: Array<string | number>,
|
|
63
|
+
): { owner: Array<string | number>; tail: Array<string | number> } | null {
|
|
64
|
+
if (path[0] !== "actions" || typeof path[1] !== "number") return null;
|
|
65
|
+
const owner: Array<string | number> = ["actions", path[1]];
|
|
66
|
+
let i = 2;
|
|
67
|
+
for (;;) {
|
|
68
|
+
const key = path[i];
|
|
69
|
+
if (
|
|
70
|
+
key === "choose" &&
|
|
71
|
+
typeof path[i + 1] === "number" &&
|
|
72
|
+
path[i + 2] === "sequence" &&
|
|
73
|
+
typeof path[i + 3] === "number"
|
|
74
|
+
) {
|
|
75
|
+
owner.push("choose", path[i + 1]!, "sequence", path[i + 3]!);
|
|
76
|
+
i += 4;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (key === "else" && typeof path[i + 1] === "number") {
|
|
80
|
+
owner.push("else", path[i + 1]!);
|
|
81
|
+
i += 2;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (key === "parallel" && typeof path[i + 1] === "number") {
|
|
85
|
+
owner.push("parallel", path[i + 1]!);
|
|
86
|
+
i += 2;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
key === "repeat" &&
|
|
91
|
+
path[i + 1] === "sequence" &&
|
|
92
|
+
typeof path[i + 2] === "number"
|
|
93
|
+
) {
|
|
94
|
+
owner.push("repeat", "sequence", path[i + 2]!);
|
|
95
|
+
i += 3;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (key === "sequence" && typeof path[i + 1] === "number") {
|
|
99
|
+
owner.push("sequence", path[i + 1]!);
|
|
100
|
+
i += 2;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
return { owner, tail: path.slice(i) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatIssue(tail: Array<string | number>, message: string): string {
|
|
109
|
+
return tail.length > 0 ? `${tail.join(".")}: ${message}` : message;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface PartitionedIssues {
|
|
113
|
+
/** Keyed by `defPathKey(ownerDefPath)`. */
|
|
114
|
+
actions: Map<string, string[]>;
|
|
115
|
+
/** Keyed by trigger index. */
|
|
116
|
+
triggers: Map<number, string[]>;
|
|
117
|
+
/** Keyed by condition index. */
|
|
118
|
+
conditions: Map<number, string[]>;
|
|
119
|
+
/** Issues not attributable to a specific card (top-level fields). */
|
|
120
|
+
other: string[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Group flat validation issues by the editor surface that should
|
|
125
|
+
* display them.
|
|
126
|
+
*/
|
|
127
|
+
export function partitionIssues(issues: DefinitionIssue[]): PartitionedIssues {
|
|
128
|
+
const actions = new Map<string, string[]>();
|
|
129
|
+
const triggers = new Map<number, string[]>();
|
|
130
|
+
const conditions = new Map<number, string[]>();
|
|
131
|
+
const other: string[] = [];
|
|
132
|
+
|
|
133
|
+
const pushTo = <K,>(map: Map<K, string[]>, key: K, value: string): void => {
|
|
134
|
+
const existing = map.get(key) ?? [];
|
|
135
|
+
existing.push(value);
|
|
136
|
+
map.set(key, existing);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
for (const issue of issues) {
|
|
140
|
+
if (issue.path[0] === "triggers" && typeof issue.path[1] === "number") {
|
|
141
|
+
pushTo(triggers, issue.path[1], formatIssue(issue.path.slice(2), issue.message));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (issue.path[0] === "conditions" && typeof issue.path[1] === "number") {
|
|
145
|
+
pushTo(
|
|
146
|
+
conditions,
|
|
147
|
+
issue.path[1],
|
|
148
|
+
formatIssue(issue.path.slice(2), issue.message),
|
|
149
|
+
);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const owner = resolveActionOwner(issue.path);
|
|
153
|
+
if (owner) {
|
|
154
|
+
pushTo(actions, defPathKey(owner.owner), formatIssue(owner.tail, issue.message));
|
|
155
|
+
} else {
|
|
156
|
+
other.push(formatIssue(issue.path, issue.message));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { actions, triggers, conditions, other };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── Context ─────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
const ValidationContext = React.createContext<PartitionedIssues | null>(null);
|
|
166
|
+
|
|
167
|
+
export const ValidationProvider: React.FC<{
|
|
168
|
+
issues: DefinitionIssue[];
|
|
169
|
+
children: React.ReactNode;
|
|
170
|
+
}> = ({ issues, children }) => {
|
|
171
|
+
const partitioned = React.useMemo(() => partitionIssues(issues), [issues]);
|
|
172
|
+
return (
|
|
173
|
+
<ValidationContext.Provider value={partitioned}>
|
|
174
|
+
{children}
|
|
175
|
+
</ValidationContext.Provider>
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
function usePartition(): PartitionedIssues | null {
|
|
180
|
+
return React.useContext(ValidationContext);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Issues attached to the action at the given editor `ActionPath`. */
|
|
184
|
+
export function useActionIssues(path: ActionPath): string[] {
|
|
185
|
+
const partition = usePartition();
|
|
186
|
+
const key = defPathKey(actionPathToDefPath(path));
|
|
187
|
+
return partition?.actions.get(key) ?? [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Issues attached to the trigger at the given index. */
|
|
191
|
+
export function useTriggerIssues(index: number): string[] {
|
|
192
|
+
const partition = usePartition();
|
|
193
|
+
return partition?.triggers.get(index) ?? [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Issues attached to the pre-run condition at the given index. */
|
|
197
|
+
export function useConditionIssues(index: number): string[] {
|
|
198
|
+
const partition = usePartition();
|
|
199
|
+
return partition?.conditions.get(index) ?? [];
|
|
200
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
usePluginClient,
|
|
4
|
+
} from "@checkstack/frontend-api";
|
|
5
|
+
import {
|
|
6
|
+
AutomationApi,
|
|
7
|
+
resolveVariableScope,
|
|
8
|
+
type ActionInfo,
|
|
9
|
+
type ActionPath,
|
|
10
|
+
type ArtifactTypeInfo,
|
|
11
|
+
type AutomationDefinition,
|
|
12
|
+
type TriggerInfo,
|
|
13
|
+
type VariableEntry,
|
|
14
|
+
} from "@checkstack/automation-common";
|
|
15
|
+
import type {
|
|
16
|
+
ShellEnvVar,
|
|
17
|
+
TemplateCompletionProvider,
|
|
18
|
+
TemplateProperty,
|
|
19
|
+
VariableNode,
|
|
20
|
+
} from "@checkstack/ui";
|
|
21
|
+
import {
|
|
22
|
+
TEMPLATE_FILTERS,
|
|
23
|
+
fieldsToShellEnvVars,
|
|
24
|
+
flattenScopeToFields,
|
|
25
|
+
} from "./template-helpers";
|
|
26
|
+
import { createTemplateCompletionProvider } from "./template-completion";
|
|
27
|
+
import { generateAutomationContextTypes } from "../script-context";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Registry context — loads the trigger / action / artifact-type catalog
|
|
31
|
+
* once, exposes it via hooks, and provides a `useVariableScope(path)`
|
|
32
|
+
* shortcut so every template field in the visual editor can get the
|
|
33
|
+
* right `templateProperties` without re-running the scope resolver
|
|
34
|
+
* by hand. The scope hook also returns the hierarchical
|
|
35
|
+
* `VariableNode[]` tree used by `VariablePicker`.
|
|
36
|
+
*
|
|
37
|
+
* Loaded eagerly because: (a) the catalog is small (dozens of items
|
|
38
|
+
* total), (b) every action card in the visual editor needs to look up
|
|
39
|
+
* its own action info to render the right config form, (c) the scope
|
|
40
|
+
* resolver needs the action catalog to compute upstream artifact
|
|
41
|
+
* entries.
|
|
42
|
+
*/
|
|
43
|
+
interface RegistryContextValue {
|
|
44
|
+
triggers: TriggerInfo[];
|
|
45
|
+
actions: ActionInfo[];
|
|
46
|
+
artifactTypes: ArtifactTypeInfo[];
|
|
47
|
+
/** True until all three queries have resolved. */
|
|
48
|
+
loading: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const RegistryContext = React.createContext<RegistryContextValue | null>(null);
|
|
52
|
+
|
|
53
|
+
export const AutomationRegistryProvider: React.FC<{
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
}> = ({ children }) => {
|
|
56
|
+
const client = usePluginClient(AutomationApi);
|
|
57
|
+
const triggers = client.listTriggers.useQuery();
|
|
58
|
+
const actions = client.listActions.useQuery();
|
|
59
|
+
const artifactTypes = client.listArtifactTypes.useQuery();
|
|
60
|
+
|
|
61
|
+
const value = React.useMemo<RegistryContextValue>(
|
|
62
|
+
() => ({
|
|
63
|
+
triggers: triggers.data?.items ?? [],
|
|
64
|
+
actions: actions.data?.items ?? [],
|
|
65
|
+
artifactTypes: artifactTypes.data?.items ?? [],
|
|
66
|
+
loading:
|
|
67
|
+
triggers.isLoading || actions.isLoading || artifactTypes.isLoading,
|
|
68
|
+
}),
|
|
69
|
+
[triggers, actions, artifactTypes],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<RegistryContext.Provider value={value}>
|
|
74
|
+
{children}
|
|
75
|
+
</RegistryContext.Provider>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function useAutomationRegistry(): RegistryContextValue {
|
|
80
|
+
const ctx = React.useContext(RegistryContext);
|
|
81
|
+
if (!ctx) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
"useAutomationRegistry must be used inside <AutomationRegistryProvider>",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return ctx;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the variable scope at a specific action path inside the given
|
|
91
|
+
* automation definition. Returned in both formats the editor needs:
|
|
92
|
+
*
|
|
93
|
+
* - `templateProperties` for `TemplateValueInput`'s `{{` autocomplete.
|
|
94
|
+
* - `variableNodes` for `VariablePicker`'s hierarchical tree.
|
|
95
|
+
*
|
|
96
|
+
* Re-runs whenever the definition or path changes — small enough that
|
|
97
|
+
* memoising is a clarity win, not a perf necessity.
|
|
98
|
+
*/
|
|
99
|
+
export function useVariableScope(args: {
|
|
100
|
+
definition: AutomationDefinition;
|
|
101
|
+
path: ActionPath;
|
|
102
|
+
}): {
|
|
103
|
+
/**
|
|
104
|
+
* Flat field list for the legacy `templateProperties` shorthand —
|
|
105
|
+
* consumed by `DynamicForm` config fields that only need simple
|
|
106
|
+
* `{{ path }}` insertion.
|
|
107
|
+
*/
|
|
108
|
+
templateProperties: TemplateProperty[];
|
|
109
|
+
/** Hierarchical tree for the explicit "fx" `VariablePicker`. */
|
|
110
|
+
variableNodes: VariableNode[];
|
|
111
|
+
/**
|
|
112
|
+
* Staged completion provider for `{{ … }}` template fields (filter
|
|
113
|
+
* templates, delay templates, …). Fields → comparators/filters →
|
|
114
|
+
* enum values.
|
|
115
|
+
*/
|
|
116
|
+
templateCompletion: TemplateCompletionProvider;
|
|
117
|
+
/**
|
|
118
|
+
* Staged completion provider for bare expression fields (conditions /
|
|
119
|
+
* `choose: when` clauses) — same staging, no `{{ }}` wrapper.
|
|
120
|
+
*/
|
|
121
|
+
expressionCompletion: TemplateCompletionProvider;
|
|
122
|
+
/**
|
|
123
|
+
* Monaco `addExtraLib` declarations for inline-script action editors
|
|
124
|
+
* (`Run Script (TypeScript)`). Types `context.trigger.payload` as a
|
|
125
|
+
* discriminated union over the automation's subscribed triggers, plus
|
|
126
|
+
* `context.artifacts` / `context.var` / `context.repeat` in scope at
|
|
127
|
+
* this action position — so the script editor autocompletes the real
|
|
128
|
+
* runtime context instead of falling back to an untyped default.
|
|
129
|
+
*/
|
|
130
|
+
typeDefinitions: string;
|
|
131
|
+
/**
|
|
132
|
+
* `$CHECKSTACK_*` env var names a shell script action receives, for the
|
|
133
|
+
* shell editor's `$` autocomplete. Mirrors what the backend injects at
|
|
134
|
+
* run time (shell scripts read context from env vars, not `{{ }}`).
|
|
135
|
+
*/
|
|
136
|
+
shellEnvVars: ShellEnvVar[];
|
|
137
|
+
} {
|
|
138
|
+
const { triggers, actions, artifactTypes } = useAutomationRegistry();
|
|
139
|
+
return React.useMemo(() => {
|
|
140
|
+
const scope = resolveVariableScope({
|
|
141
|
+
definition: args.definition,
|
|
142
|
+
triggers,
|
|
143
|
+
actions,
|
|
144
|
+
artifactTypes,
|
|
145
|
+
path: args.path,
|
|
146
|
+
});
|
|
147
|
+
const fields = flattenScopeToFields(scope);
|
|
148
|
+
const { typeDefinitions } = generateAutomationContextTypes({
|
|
149
|
+
definition: args.definition,
|
|
150
|
+
triggers,
|
|
151
|
+
actions: actions.map((a) => ({
|
|
152
|
+
qualifiedId: a.qualifiedId,
|
|
153
|
+
produces: a.produces,
|
|
154
|
+
})),
|
|
155
|
+
artifactTypes,
|
|
156
|
+
path: args.path,
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
templateProperties: fields.map((f) => ({
|
|
160
|
+
path: f.path,
|
|
161
|
+
templateRef: f.templateRef,
|
|
162
|
+
type: f.type,
|
|
163
|
+
description: f.description,
|
|
164
|
+
enumValues: f.enumValues,
|
|
165
|
+
})),
|
|
166
|
+
variableNodes: scope.entries.map((child) => toNode(child)),
|
|
167
|
+
templateCompletion: createTemplateCompletionProvider({
|
|
168
|
+
fields,
|
|
169
|
+
filters: [...TEMPLATE_FILTERS],
|
|
170
|
+
mode: "template",
|
|
171
|
+
}),
|
|
172
|
+
expressionCompletion: createTemplateCompletionProvider({
|
|
173
|
+
fields,
|
|
174
|
+
filters: [...TEMPLATE_FILTERS],
|
|
175
|
+
mode: "expression",
|
|
176
|
+
}),
|
|
177
|
+
typeDefinitions,
|
|
178
|
+
shellEnvVars: fieldsToShellEnvVars(fields),
|
|
179
|
+
};
|
|
180
|
+
}, [args.definition, args.path, triggers, actions, artifactTypes]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function toNode(entry: VariableEntry): VariableNode {
|
|
184
|
+
return {
|
|
185
|
+
path: entry.path,
|
|
186
|
+
templateRef: entry.templateRef,
|
|
187
|
+
type: entry.type,
|
|
188
|
+
description: entry.description,
|
|
189
|
+
children: entry.children?.map((child) => toNode(child)),
|
|
190
|
+
conditionalOnTriggers: entry.conditionalOnTriggers,
|
|
191
|
+
};
|
|
192
|
+
}
|