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