@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,390 @@
1
+ import React from "react";
2
+ import { Plus, Trash2 } from "lucide-react";
3
+ import {
4
+ Button,
5
+ Card,
6
+ CardContent,
7
+ Input,
8
+ Label,
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ TemplateValueInput,
15
+ type TemplateCompletionProvider,
16
+ type VariableNode,
17
+ } from "@checkstack/ui";
18
+ import type {
19
+ ActionInput,
20
+ ActionPath,
21
+ ActionPathStep,
22
+ ChooseInput,
23
+ ConditionInput,
24
+ ParallelInput,
25
+ RepeatInput,
26
+ SequenceInput,
27
+ RepeatMode,
28
+ } from "@checkstack/automation-common";
29
+ import { ConditionEditor } from "./ConditionEditor";
30
+ import { ActionListEditor } from "./ActionListEditor";
31
+
32
+ /**
33
+ * `choose:` — list of `when` clauses, each gating a nested sequence,
34
+ * plus an optional `else` sequence. The when-clauses themselves use
35
+ * the same recursive `ConditionEditor` the top-level pre-run
36
+ * conditions use. Each when-clause's sequence renders the same
37
+ * `ActionListEditor` the top level uses — that's how nesting works,
38
+ * top-down composition all the way down.
39
+ */
40
+ export const ChooseActionBody: React.FC<{
41
+ value: ChooseInput;
42
+ onChange: (next: ChooseInput) => void;
43
+ path: ActionPath;
44
+ variableNodes: VariableNode[];
45
+ /** Expression-mode provider for the `when` clauses (bare expressions). */
46
+ expressionCompletion: TemplateCompletionProvider;
47
+ disabled?: boolean;
48
+ }> = ({
49
+ value,
50
+ onChange,
51
+ path,
52
+ variableNodes,
53
+ expressionCompletion,
54
+ disabled,
55
+ }) => {
56
+ const handleBranchChange = (
57
+ branchIndex: number,
58
+ update: (branch: { when: ConditionInput; sequence: ActionInput[] }) => {
59
+ when: ConditionInput;
60
+ sequence: ActionInput[];
61
+ },
62
+ ) => {
63
+ const next = [...value.choose];
64
+ next[branchIndex] = update(next[branchIndex]!);
65
+ onChange({ ...value, choose: next });
66
+ };
67
+
68
+ return (
69
+ <div className="space-y-3">
70
+ {value.choose.map((branch, branchIndex) => {
71
+ const branchPath: ActionPath = [
72
+ ...path,
73
+ { slot: "choose-when", whenIndex: branchIndex, index: 0 } as ActionPathStep,
74
+ ];
75
+ return (
76
+ <Card key={branchIndex} className="border-border/60 bg-muted/30">
77
+ <CardContent className="space-y-2 p-3">
78
+ <div className="flex items-center justify-between">
79
+ <Label className="text-xs">
80
+ When (branch {branchIndex + 1})
81
+ </Label>
82
+ <Button
83
+ type="button"
84
+ variant="ghost"
85
+ size="icon"
86
+ className="h-6 w-6 text-destructive hover:bg-destructive/10"
87
+ onClick={() =>
88
+ onChange({
89
+ ...value,
90
+ choose: value.choose.filter((_, i) => i !== branchIndex),
91
+ })
92
+ }
93
+ disabled={disabled}
94
+ aria-label="Remove branch"
95
+ >
96
+ <Trash2 className="h-3 w-3" />
97
+ </Button>
98
+ </div>
99
+ <ConditionEditor
100
+ value={branch.when}
101
+ onChange={(when) =>
102
+ handleBranchChange(branchIndex, (b) => ({ ...b, when }))
103
+ }
104
+ variableNodes={variableNodes}
105
+ completionProvider={expressionCompletion}
106
+ bare
107
+ />
108
+ <Label className="text-xs">Then run</Label>
109
+ <ActionListEditor
110
+ value={branch.sequence}
111
+ onChange={(sequence) =>
112
+ handleBranchChange(branchIndex, (b) => ({ ...b, sequence }))
113
+ }
114
+ parentPath={branchPath.slice(0, -1)}
115
+ slotForChildren={{ slot: "choose-when", whenIndex: branchIndex }}
116
+ disabled={disabled}
117
+ />
118
+ </CardContent>
119
+ </Card>
120
+ );
121
+ })}
122
+ <Button
123
+ type="button"
124
+ variant="outline"
125
+ size="sm"
126
+ onClick={() =>
127
+ onChange({
128
+ ...value,
129
+ choose: [...value.choose, { when: "", sequence: [] }],
130
+ })
131
+ }
132
+ disabled={disabled}
133
+ className="h-7 text-xs"
134
+ >
135
+ <Plus className="mr-1 h-3 w-3" />
136
+ Add branch
137
+ </Button>
138
+
139
+ <Card className="border-dashed border-border/60 bg-muted/20">
140
+ <CardContent className="space-y-2 p-3">
141
+ <Label className="text-xs">Else (optional)</Label>
142
+ <ActionListEditor
143
+ value={value.else ?? []}
144
+ onChange={(next) =>
145
+ onChange({ ...value, else: next.length === 0 ? undefined : next })
146
+ }
147
+ parentPath={path}
148
+ slotForChildren={{ slot: "choose-else" }}
149
+ disabled={disabled}
150
+ />
151
+ </CardContent>
152
+ </Card>
153
+ </div>
154
+ );
155
+ };
156
+
157
+ export const ParallelActionBody: React.FC<{
158
+ value: ParallelInput;
159
+ onChange: (next: ParallelInput) => void;
160
+ path: ActionPath;
161
+ disabled?: boolean;
162
+ }> = ({ value, onChange, path, disabled }) => (
163
+ <div className="space-y-2">
164
+ <Label className="text-xs">Branches (run concurrently)</Label>
165
+ <ActionListEditor
166
+ value={value.parallel}
167
+ onChange={(parallel) => onChange({ ...value, parallel })}
168
+ parentPath={path}
169
+ slotForChildren={{ slot: "parallel" }}
170
+ disabled={disabled}
171
+ />
172
+ </div>
173
+ );
174
+
175
+ export const SequenceActionBody: React.FC<{
176
+ value: SequenceInput;
177
+ onChange: (next: SequenceInput) => void;
178
+ path: ActionPath;
179
+ disabled?: boolean;
180
+ }> = ({ value, onChange, path, disabled }) => (
181
+ <div className="space-y-2">
182
+ <Label className="text-xs">Steps</Label>
183
+ <ActionListEditor
184
+ value={value.sequence}
185
+ onChange={(sequence) => onChange({ ...value, sequence })}
186
+ parentPath={path}
187
+ slotForChildren={{ slot: "sequence" }}
188
+ disabled={disabled}
189
+ />
190
+ </div>
191
+ );
192
+
193
+ type RepeatModeKind = "count" | "for_each" | "while" | "until";
194
+
195
+ function repeatModeKind(mode: RepeatMode): RepeatModeKind {
196
+ if ("count" in mode) return "count";
197
+ if ("for_each" in mode) return "for_each";
198
+ if ("while" in mode) return "while";
199
+ return "until";
200
+ }
201
+
202
+ export const RepeatActionBody: React.FC<{
203
+ value: RepeatInput;
204
+ onChange: (next: RepeatInput) => void;
205
+ path: ActionPath;
206
+ /** Template-mode provider for the for_each / while / until expressions. */
207
+ completionProvider: TemplateCompletionProvider;
208
+ disabled?: boolean;
209
+ }> = ({ value, onChange, path, completionProvider, disabled }) => {
210
+ const kind = repeatModeKind(value.repeat);
211
+
212
+ const changeKind = (next: RepeatModeKind) => {
213
+ const sequence = value.repeat.sequence;
214
+ switch (next) {
215
+ case "count": {
216
+ onChange({ ...value, repeat: { count: 3, sequence } });
217
+
218
+ break;
219
+ }
220
+ case "for_each": {
221
+ onChange({ ...value, repeat: { for_each: "", sequence } });
222
+
223
+ break;
224
+ }
225
+ case "while": {
226
+ onChange({ ...value, repeat: { while: "", sequence } });
227
+
228
+ break;
229
+ }
230
+ default: {
231
+ onChange({ ...value, repeat: { until: "", sequence } });
232
+ }
233
+ }
234
+ };
235
+
236
+ return (
237
+ <div className="space-y-3">
238
+ <div className="flex items-center gap-2">
239
+ <Label className="text-xs">Mode</Label>
240
+ <Select
241
+ value={kind}
242
+ onValueChange={(next) => changeKind(next as RepeatModeKind)}
243
+ disabled={disabled}
244
+ >
245
+ <SelectTrigger className="h-7 w-32 text-xs">
246
+ <SelectValue />
247
+ </SelectTrigger>
248
+ <SelectContent>
249
+ <SelectItem value="count">count</SelectItem>
250
+ <SelectItem value="for_each">for_each</SelectItem>
251
+ <SelectItem value="while">while</SelectItem>
252
+ <SelectItem value="until">until</SelectItem>
253
+ </SelectContent>
254
+ </Select>
255
+ </div>
256
+
257
+ {kind === "count" && (
258
+ <div className="space-y-1">
259
+ <Label className="text-xs">Iterations</Label>
260
+ <Input
261
+ type="number"
262
+ min={1}
263
+ max={10_000}
264
+ value={(value.repeat as { count: number }).count}
265
+ onChange={(event) =>
266
+ onChange({
267
+ ...value,
268
+ repeat: {
269
+ ...value.repeat,
270
+ count: Math.max(1, Number(event.target.value)),
271
+ },
272
+ })
273
+ }
274
+ disabled={disabled}
275
+ />
276
+ </div>
277
+ )}
278
+
279
+ {(kind === "for_each" || kind === "while" || kind === "until") && (
280
+ <div className="space-y-1">
281
+ <Label className="text-xs">
282
+ {kind === "for_each"
283
+ ? "Iterable (renders to JSON array)"
284
+ : "Condition template"}
285
+ </Label>
286
+ <TemplateValueInput
287
+ value={
288
+ kind === "for_each"
289
+ ? (value.repeat as { for_each: string }).for_each
290
+ : kind === "while"
291
+ ? (value.repeat as { while: string }).while
292
+ : (value.repeat as { until: string }).until
293
+ }
294
+ onChange={(next) => {
295
+ const sequence = value.repeat.sequence;
296
+ if (kind === "for_each") {
297
+ onChange({ ...value, repeat: { for_each: next, sequence } });
298
+ } else if (kind === "while") {
299
+ onChange({
300
+ ...value,
301
+ repeat: {
302
+ while: next,
303
+ sequence,
304
+ max_iterations: (
305
+ value.repeat as { max_iterations?: number }
306
+ ).max_iterations,
307
+ },
308
+ });
309
+ } else {
310
+ onChange({
311
+ ...value,
312
+ repeat: {
313
+ until: next,
314
+ sequence,
315
+ max_iterations: (
316
+ value.repeat as { max_iterations?: number }
317
+ ).max_iterations,
318
+ },
319
+ });
320
+ }
321
+ }}
322
+ placeholder={
323
+ kind === "for_each"
324
+ ? "{{ trigger.payload.items }}"
325
+ : "{{ var.done == false }}"
326
+ }
327
+ completionProvider={completionProvider}
328
+ disabled={disabled}
329
+ />
330
+ </div>
331
+ )}
332
+
333
+ {(kind === "while" || kind === "until") && (
334
+ <div className="space-y-1">
335
+ <Label className="text-xs">Max iterations (safety net)</Label>
336
+ <Input
337
+ type="number"
338
+ min={1}
339
+ max={10_000}
340
+ value={
341
+ (value.repeat as { max_iterations?: number }).max_iterations ?? 1000
342
+ }
343
+ onChange={(event) => {
344
+ const max_iterations = Math.max(1, Number(event.target.value));
345
+ if (kind === "while") {
346
+ onChange({
347
+ ...value,
348
+ repeat: {
349
+ while: (value.repeat as { while: string }).while,
350
+ max_iterations,
351
+ sequence: value.repeat.sequence,
352
+ },
353
+ });
354
+ } else {
355
+ onChange({
356
+ ...value,
357
+ repeat: {
358
+ until: (value.repeat as { until: string }).until,
359
+ max_iterations,
360
+ sequence: value.repeat.sequence,
361
+ },
362
+ });
363
+ }
364
+ }}
365
+ disabled={disabled}
366
+ />
367
+ </div>
368
+ )}
369
+
370
+ <div className="space-y-1">
371
+ <Label className="text-xs">Sequence (runs each iteration)</Label>
372
+ <ActionListEditor
373
+ value={value.repeat.sequence}
374
+ onChange={(sequence) =>
375
+ onChange({
376
+ ...value,
377
+ repeat: {
378
+ ...value.repeat,
379
+ sequence,
380
+ } as typeof value.repeat,
381
+ })
382
+ }
383
+ parentPath={path}
384
+ slotForChildren={{ slot: "repeat" }}
385
+ disabled={disabled}
386
+ />
387
+ </div>
388
+ </div>
389
+ );
390
+ };