@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,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
+ }