@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +664 -0
  2. package/package.json +38 -0
  3. package/src/components/AutomationMenuItems.tsx +37 -0
  4. package/src/editor/ActionEditor.tsx +367 -0
  5. package/src/editor/ActionListEditor.tsx +203 -0
  6. package/src/editor/AddActionDialog.tsx +225 -0
  7. package/src/editor/AutomationDefinitionContext.tsx +37 -0
  8. package/src/editor/AutomationDefinitionEditor.tsx +99 -0
  9. package/src/editor/ConditionEditor.tsx +218 -0
  10. package/src/editor/ConditionsEditor.tsx +89 -0
  11. package/src/editor/ItemPicker.tsx +147 -0
  12. package/src/editor/TriggersEditor.tsx +269 -0
  13. package/src/editor/action-composite-cards.tsx +390 -0
  14. package/src/editor/action-helpers.ts +365 -0
  15. package/src/editor/action-leaf-cards.tsx +426 -0
  16. package/src/editor/editor-validation.test.ts +95 -0
  17. package/src/editor/editor-validation.tsx +200 -0
  18. package/src/editor/registry-context.tsx +192 -0
  19. package/src/editor/template-completion.test.ts +412 -0
  20. package/src/editor/template-completion.ts +664 -0
  21. package/src/editor/template-helpers.test.ts +145 -0
  22. package/src/editor/template-helpers.ts +95 -0
  23. package/src/editor/trigger-helpers.test.ts +58 -0
  24. package/src/editor/trigger-helpers.ts +67 -0
  25. package/src/editor/useConnectionOptionResolvers.ts +80 -0
  26. package/src/editor/yaml-markers.ts +116 -0
  27. package/src/index.tsx +95 -0
  28. package/src/pages/AutomationEditPage.tsx +567 -0
  29. package/src/pages/AutomationListPage.tsx +304 -0
  30. package/src/pages/RunDetailPage.tsx +333 -0
  31. package/src/pages/RunsPage.tsx +233 -0
  32. package/src/pages/TemplatePlaygroundPage.tsx +224 -0
  33. package/src/script-context.test.ts +247 -0
  34. package/src/script-context.ts +218 -0
  35. 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
+ }