@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,365 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionInput,
|
|
3
|
+
ChooseInput,
|
|
4
|
+
ConditionGuardInput,
|
|
5
|
+
DelayInput,
|
|
6
|
+
ParallelInput,
|
|
7
|
+
ProviderAction,
|
|
8
|
+
RepeatInput,
|
|
9
|
+
SequenceInput,
|
|
10
|
+
StopInput,
|
|
11
|
+
VariablesInput,
|
|
12
|
+
WaitForTriggerInput,
|
|
13
|
+
} from "@checkstack/automation-common";
|
|
14
|
+
import type { LucideIconName } from "@checkstack/ui";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Action primitives that this editor supports. The values match the
|
|
18
|
+
* discriminator keys on `ActionInput` — checking for the matching key's
|
|
19
|
+
* presence is how the schema itself distinguishes the variants.
|
|
20
|
+
*/
|
|
21
|
+
export type ActionKind =
|
|
22
|
+
| "action"
|
|
23
|
+
| "choose"
|
|
24
|
+
| "parallel"
|
|
25
|
+
| "repeat"
|
|
26
|
+
| "variables"
|
|
27
|
+
| "condition"
|
|
28
|
+
| "stop"
|
|
29
|
+
| "wait_for_trigger"
|
|
30
|
+
| "sequence"
|
|
31
|
+
| "delay";
|
|
32
|
+
|
|
33
|
+
export const ACTION_KINDS: ActionKind[] = [
|
|
34
|
+
"action",
|
|
35
|
+
"choose",
|
|
36
|
+
"parallel",
|
|
37
|
+
"repeat",
|
|
38
|
+
"variables",
|
|
39
|
+
"condition",
|
|
40
|
+
"stop",
|
|
41
|
+
"wait_for_trigger",
|
|
42
|
+
"sequence",
|
|
43
|
+
"delay",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export interface ActionKindMeta {
|
|
47
|
+
kind: ActionKind;
|
|
48
|
+
label: string;
|
|
49
|
+
description: string;
|
|
50
|
+
icon: LucideIconName;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ACTION_KIND_META: Record<ActionKind, ActionKindMeta> = {
|
|
54
|
+
action: {
|
|
55
|
+
kind: "action",
|
|
56
|
+
label: "Action",
|
|
57
|
+
description: "Call a registered action (provider) by id.",
|
|
58
|
+
// Not "Play": a play triangle reads as a run/test button, but this
|
|
59
|
+
// icon sits inside the card's expand toggle, so clicking it just
|
|
60
|
+
// collapses the card. "Zap" is the conventional automation-action
|
|
61
|
+
// glyph and carries no "click to run" affordance.
|
|
62
|
+
icon: "Zap",
|
|
63
|
+
},
|
|
64
|
+
choose: {
|
|
65
|
+
kind: "choose",
|
|
66
|
+
label: "Choose (if / else)",
|
|
67
|
+
description: "Branch on a condition; first matching when-clause runs.",
|
|
68
|
+
icon: "GitBranch",
|
|
69
|
+
},
|
|
70
|
+
parallel: {
|
|
71
|
+
kind: "parallel",
|
|
72
|
+
label: "Parallel",
|
|
73
|
+
description: "Run branches concurrently; wait for all to complete.",
|
|
74
|
+
icon: "Columns3",
|
|
75
|
+
},
|
|
76
|
+
repeat: {
|
|
77
|
+
kind: "repeat",
|
|
78
|
+
label: "Repeat",
|
|
79
|
+
description: "Iterate (count / for_each / while / until).",
|
|
80
|
+
icon: "Repeat",
|
|
81
|
+
},
|
|
82
|
+
variables: {
|
|
83
|
+
kind: "variables",
|
|
84
|
+
label: "Variables",
|
|
85
|
+
description: "Define local var.* names for downstream actions.",
|
|
86
|
+
icon: "Variable",
|
|
87
|
+
},
|
|
88
|
+
condition: {
|
|
89
|
+
kind: "condition",
|
|
90
|
+
label: "Condition (guard)",
|
|
91
|
+
description: "Halt the run unless the condition holds.",
|
|
92
|
+
icon: "Shield",
|
|
93
|
+
},
|
|
94
|
+
stop: {
|
|
95
|
+
kind: "stop",
|
|
96
|
+
label: "Stop",
|
|
97
|
+
description: "Terminate the run with an optional reason.",
|
|
98
|
+
icon: "Square",
|
|
99
|
+
},
|
|
100
|
+
wait_for_trigger: {
|
|
101
|
+
kind: "wait_for_trigger",
|
|
102
|
+
label: "Wait for trigger",
|
|
103
|
+
description: "Suspend until a matching trigger event arrives.",
|
|
104
|
+
icon: "Hourglass",
|
|
105
|
+
},
|
|
106
|
+
sequence: {
|
|
107
|
+
kind: "sequence",
|
|
108
|
+
label: "Sequence",
|
|
109
|
+
description: "Group several actions as one (useful inside parallel).",
|
|
110
|
+
icon: "List",
|
|
111
|
+
},
|
|
112
|
+
delay: {
|
|
113
|
+
kind: "delay",
|
|
114
|
+
label: "Delay",
|
|
115
|
+
description: "Sleep for a fixed or templated number of seconds.",
|
|
116
|
+
icon: "Timer",
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Inspect an `ActionInput` and return the discriminator key. The schema
|
|
122
|
+
* deliberately uses structural discrimination (presence of `action`,
|
|
123
|
+
* `choose`, `parallel`, …) rather than a `kind:` tag, so this central
|
|
124
|
+
* helper is the only place that needs to know that fact.
|
|
125
|
+
*/
|
|
126
|
+
export function actionKindOf(action: ActionInput): ActionKind {
|
|
127
|
+
if ("action" in action) return "action";
|
|
128
|
+
if ("choose" in action) return "choose";
|
|
129
|
+
if ("parallel" in action) return "parallel";
|
|
130
|
+
if ("repeat" in action) return "repeat";
|
|
131
|
+
if ("variables" in action) return "variables";
|
|
132
|
+
if ("condition" in action) return "condition";
|
|
133
|
+
if ("stop" in action) return "stop";
|
|
134
|
+
if ("wait_for_trigger" in action) return "wait_for_trigger";
|
|
135
|
+
if ("sequence" in action) return "sequence";
|
|
136
|
+
return "delay";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const BASE = { enabled: true, continue_on_error: false } as const;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build a fresh action of the requested kind with sensible empty defaults.
|
|
143
|
+
* Used when the operator picks a building block from the add-step picker.
|
|
144
|
+
*
|
|
145
|
+
* Composite kinds (choose / parallel / repeat / sequence) start with an
|
|
146
|
+
* empty child list; the operator fills them via the nested add-step picker.
|
|
147
|
+
* The schema requires at least one nested step, so an empty composite surfaces
|
|
148
|
+
* an inline "add a step" validation hint until the operator adds one - which
|
|
149
|
+
* is clearer than seeding an unconfigurable empty provider action now that the
|
|
150
|
+
* in-card action switcher is gone.
|
|
151
|
+
*/
|
|
152
|
+
export function makeEmptyAction(kind: ActionKind): ActionInput {
|
|
153
|
+
switch (kind) {
|
|
154
|
+
case "action": {
|
|
155
|
+
return { ...BASE, action: "", config: {} } satisfies ProviderAction;
|
|
156
|
+
}
|
|
157
|
+
case "choose": {
|
|
158
|
+
return {
|
|
159
|
+
...BASE,
|
|
160
|
+
choose: [{ when: "", sequence: [] }],
|
|
161
|
+
} satisfies ChooseInput;
|
|
162
|
+
}
|
|
163
|
+
case "parallel": {
|
|
164
|
+
return {
|
|
165
|
+
...BASE,
|
|
166
|
+
parallel: [],
|
|
167
|
+
} satisfies ParallelInput;
|
|
168
|
+
}
|
|
169
|
+
case "repeat": {
|
|
170
|
+
return {
|
|
171
|
+
...BASE,
|
|
172
|
+
repeat: { count: 3, sequence: [] },
|
|
173
|
+
} satisfies RepeatInput;
|
|
174
|
+
}
|
|
175
|
+
case "variables": {
|
|
176
|
+
return {
|
|
177
|
+
...BASE,
|
|
178
|
+
variables: { example: "value" },
|
|
179
|
+
} satisfies VariablesInput;
|
|
180
|
+
}
|
|
181
|
+
case "condition": {
|
|
182
|
+
return { ...BASE, condition: "" } satisfies ConditionGuardInput;
|
|
183
|
+
}
|
|
184
|
+
case "stop": {
|
|
185
|
+
return { ...BASE, stop: { error: false } } satisfies StopInput;
|
|
186
|
+
}
|
|
187
|
+
case "wait_for_trigger": {
|
|
188
|
+
return {
|
|
189
|
+
...BASE,
|
|
190
|
+
wait_for_trigger: { event: "" },
|
|
191
|
+
} satisfies WaitForTriggerInput;
|
|
192
|
+
}
|
|
193
|
+
case "sequence": {
|
|
194
|
+
return {
|
|
195
|
+
...BASE,
|
|
196
|
+
sequence: [],
|
|
197
|
+
} satisfies SequenceInput;
|
|
198
|
+
}
|
|
199
|
+
case "delay": {
|
|
200
|
+
return { ...BASE, delay: { seconds: 30 } } satisfies DelayInput;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Build a fresh provider-action step with its `action` preset to a chosen
|
|
207
|
+
* registered action's fully-qualified id. Used by the add-step picker so the
|
|
208
|
+
* operator selects the concrete action up front (the kind is fixed at
|
|
209
|
+
* creation). The empty `config` is seeded with the action's schema defaults
|
|
210
|
+
* by `DynamicForm` when the card renders.
|
|
211
|
+
*/
|
|
212
|
+
export function makeProviderAction(qualifiedId: string): ProviderAction {
|
|
213
|
+
return { ...BASE, action: qualifiedId, config: {} } satisfies ProviderAction;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Coerce an arbitrary string into an identifier-safe action id matching the
|
|
218
|
+
* schema rule `/^[a-zA-Z_][a-zA-Z0-9_]*$/`: non-identifier characters
|
|
219
|
+
* collapse to `_`, surrounding underscores are trimmed, and a leading digit
|
|
220
|
+
* is prefixed with `_`. Empty input falls back to `action`.
|
|
221
|
+
*/
|
|
222
|
+
function slugifyId(raw: string): string {
|
|
223
|
+
const cleaned = raw.replaceAll(/[^a-zA-Z0-9_]+/g, "_").replaceAll(/^_+|_+$/g, "");
|
|
224
|
+
const base = cleaned.length > 0 ? cleaned : "action";
|
|
225
|
+
return /^[0-9]/.test(base) ? `_${base}` : base;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Base for an action's auto-suggested id. Provider actions use the local
|
|
230
|
+
* action name (the part after the plugin dot, e.g.
|
|
231
|
+
* `integration-jira.create_issue` -> `create_issue`); every other kind uses
|
|
232
|
+
* its kind name. Falls back to the kind when a provider action has not been
|
|
233
|
+
* picked yet.
|
|
234
|
+
*/
|
|
235
|
+
export function suggestActionIdBase(action: ActionInput): string {
|
|
236
|
+
if ("action" in action) {
|
|
237
|
+
const qualified = action.action;
|
|
238
|
+
const local = qualified.includes(".")
|
|
239
|
+
? qualified.slice(qualified.lastIndexOf(".") + 1)
|
|
240
|
+
: qualified;
|
|
241
|
+
return slugifyId(local || "action");
|
|
242
|
+
}
|
|
243
|
+
return slugifyId(actionKindOf(action));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Child action lists a composite action contains (empty for leaf kinds). */
|
|
247
|
+
function childActionLists(action: ActionInput): ActionInput[][] {
|
|
248
|
+
if ("choose" in action) {
|
|
249
|
+
const lists = action.choose.map((branch) => branch.sequence);
|
|
250
|
+
if (action.else) lists.push(action.else);
|
|
251
|
+
return lists;
|
|
252
|
+
}
|
|
253
|
+
if ("parallel" in action) return [action.parallel];
|
|
254
|
+
if ("repeat" in action) return [action.repeat.sequence];
|
|
255
|
+
if ("sequence" in action) return [action.sequence];
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Recursively collect every assigned action id across a tree of actions.
|
|
261
|
+
* Used to seed uniqueness when auto-assigning default ids.
|
|
262
|
+
*/
|
|
263
|
+
export function collectActionIds(
|
|
264
|
+
actions: ActionInput[],
|
|
265
|
+
into: Set<string> = new Set(),
|
|
266
|
+
): Set<string> {
|
|
267
|
+
for (const action of actions) {
|
|
268
|
+
if (typeof action.id === "string" && action.id.length > 0) {
|
|
269
|
+
into.add(action.id);
|
|
270
|
+
}
|
|
271
|
+
for (const list of childActionLists(action)) collectActionIds(list, into);
|
|
272
|
+
}
|
|
273
|
+
return into;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Make `base` unique against `taken`, appending `_2`, `_3`, ... as needed. */
|
|
277
|
+
function uniqueId(base: string, taken: Set<string>): string {
|
|
278
|
+
if (!taken.has(base)) return base;
|
|
279
|
+
let n = 2;
|
|
280
|
+
while (taken.has(`${base}_${n}`)) n += 1;
|
|
281
|
+
return `${base}_${n}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A single identifier-safe, unique default id for `action`, deduped against
|
|
286
|
+
* `taken`. Used by the editor to re-fill the Id field when an operator clears
|
|
287
|
+
* it, so every action always carries a log-friendly, referenceable id.
|
|
288
|
+
*/
|
|
289
|
+
export function defaultActionId(
|
|
290
|
+
action: ActionInput,
|
|
291
|
+
taken: Set<string>,
|
|
292
|
+
): string {
|
|
293
|
+
return uniqueId(suggestActionIdBase(action), taken);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Return a copy of `actions` with a stable, unique, identifier-safe `id`
|
|
298
|
+
* assigned to every action (and nested action) that does not already have
|
|
299
|
+
* one. `taken` seeds the uniqueness set with ids used elsewhere in the
|
|
300
|
+
* automation and is mutated as ids are assigned.
|
|
301
|
+
*
|
|
302
|
+
* Called when adding a step so freshly-created actions - including the
|
|
303
|
+
* children that composite kinds prime themselves with - always carry a
|
|
304
|
+
* log-friendly id the operator can rename. Operators are not forced to name
|
|
305
|
+
* every step, but every run-step then has a parseable id in the logs.
|
|
306
|
+
*/
|
|
307
|
+
export function assignDefaultIds(
|
|
308
|
+
actions: ActionInput[],
|
|
309
|
+
taken: Set<string>,
|
|
310
|
+
): ActionInput[] {
|
|
311
|
+
return actions.map((action) => {
|
|
312
|
+
const id =
|
|
313
|
+
typeof action.id === "string" && action.id.length > 0
|
|
314
|
+
? action.id
|
|
315
|
+
: uniqueId(suggestActionIdBase(action), taken);
|
|
316
|
+
taken.add(id);
|
|
317
|
+
const next = { ...action, id };
|
|
318
|
+
if ("choose" in next) {
|
|
319
|
+
return {
|
|
320
|
+
...next,
|
|
321
|
+
choose: next.choose.map((branch) => ({
|
|
322
|
+
...branch,
|
|
323
|
+
sequence: assignDefaultIds(branch.sequence, taken),
|
|
324
|
+
})),
|
|
325
|
+
else: next.else ? assignDefaultIds(next.else, taken) : undefined,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if ("parallel" in next) {
|
|
329
|
+
return { ...next, parallel: assignDefaultIds(next.parallel, taken) };
|
|
330
|
+
}
|
|
331
|
+
if ("repeat" in next) {
|
|
332
|
+
return {
|
|
333
|
+
...next,
|
|
334
|
+
repeat: {
|
|
335
|
+
...next.repeat,
|
|
336
|
+
sequence: assignDefaultIds(next.repeat.sequence, taken),
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if ("sequence" in next) {
|
|
341
|
+
return { ...next, sequence: assignDefaultIds(next.sequence, taken) };
|
|
342
|
+
}
|
|
343
|
+
return next;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Display-name for an action card's header. For provider actions we
|
|
349
|
+
* fall back to the namespaced id when the registry doesn't know the
|
|
350
|
+
* action (e.g. while listActions is still loading); for composite
|
|
351
|
+
* actions we use the kind's friendly label.
|
|
352
|
+
*/
|
|
353
|
+
export function actionDisplayName(
|
|
354
|
+
action: ActionInput,
|
|
355
|
+
registryLookup: (qualifiedId: string) => string | undefined,
|
|
356
|
+
): string {
|
|
357
|
+
const kind = actionKindOf(action);
|
|
358
|
+
if (kind === "action") {
|
|
359
|
+
const provider = action as ProviderAction;
|
|
360
|
+
return (
|
|
361
|
+
registryLookup(provider.action) ?? (provider.action || "Action")
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
return ACTION_KIND_META[kind].label;
|
|
365
|
+
}
|