@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
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@checkstack/automation-frontend",
3
+ "version": "0.2.0",
4
+ "license": "Elastic-2.0",
5
+ "type": "module",
6
+ "main": "src/index.tsx",
7
+ "checkstack": {
8
+ "type": "frontend"
9
+ },
10
+ "scripts": {
11
+ "typecheck": "tsgo -b",
12
+ "lint": "bun run lint:code",
13
+ "lint:code": "eslint . --max-warnings 0"
14
+ },
15
+ "dependencies": {
16
+ "@checkstack/automation-common": "0.1.0",
17
+ "@checkstack/common": "0.11.0",
18
+ "@checkstack/frontend-api": "0.5.2",
19
+ "@checkstack/integration-common": "0.5.0",
20
+ "@checkstack/signal-frontend": "0.1.4",
21
+ "@checkstack/template-engine": "0.1.0",
22
+ "@checkstack/ui": "1.10.0",
23
+ "@dnd-kit/core": "^6.3.1",
24
+ "@dnd-kit/sortable": "^8.0.0",
25
+ "@dnd-kit/utilities": "^3.2.2",
26
+ "date-fns": "^4.1.0",
27
+ "lucide-react": "^0.344.0",
28
+ "react": "^18.2.0",
29
+ "react-router-dom": "^6.22.0",
30
+ "yaml": "^2.6.1"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.0.0",
34
+ "@types/react": "^18.2.0",
35
+ "@checkstack/tsconfig": "0.0.7",
36
+ "@checkstack/scripts": "0.3.3"
37
+ }
38
+ }
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { Workflow } from "lucide-react";
4
+ import type { UserMenuItemsContext } from "@checkstack/frontend-api";
5
+ import { DropdownMenuItem } from "@checkstack/ui";
6
+ import { resolveRoute } from "@checkstack/common";
7
+ import {
8
+ automationRoutes,
9
+ automationAccess,
10
+ pluginMetadata,
11
+ } from "@checkstack/automation-common";
12
+
13
+ /**
14
+ * "Automations" entry in the user menu. Only renders for users with
15
+ * `automation.read` access — mirrors `incident-frontend`'s pattern of
16
+ * gating the menu item rather than the underlying route (the route is
17
+ * also access-gated by `createFrontendPlugin`, but hiding the link is
18
+ * the cleaner UX for unauthorised users).
19
+ */
20
+ export const AutomationMenuItems = ({
21
+ accessRules: userPerms,
22
+ }: UserMenuItemsContext) => {
23
+ const qualifiedId = `${pluginMetadata.pluginId}.${automationAccess.read.id}`;
24
+ const canRead = userPerms.includes("*") || userPerms.includes(qualifiedId);
25
+
26
+ if (!canRead) {
27
+ return <React.Fragment />;
28
+ }
29
+
30
+ return (
31
+ <Link to={resolveRoute(automationRoutes.routes.list)}>
32
+ <DropdownMenuItem icon={<Workflow className="w-4 h-4" />}>
33
+ Automations
34
+ </DropdownMenuItem>
35
+ </Link>
36
+ );
37
+ };
@@ -0,0 +1,367 @@
1
+ import React from "react";
2
+ import {
3
+ ActionCard,
4
+ Checkbox,
5
+ cn,
6
+ Input,
7
+ Label,
8
+ } from "@checkstack/ui";
9
+ import type {
10
+ ActionInput,
11
+ ActionPath,
12
+ AutomationDefinition,
13
+ ChooseInput,
14
+ ConditionGuardInput,
15
+ DelayInput,
16
+ ParallelInput,
17
+ ProviderAction,
18
+ RepeatInput,
19
+ SequenceInput,
20
+ StopInput,
21
+ VariablesInput,
22
+ WaitForTriggerInput,
23
+ } from "@checkstack/automation-common";
24
+ import {
25
+ ACTION_KIND_META,
26
+ actionDisplayName,
27
+ actionKindOf,
28
+ collectActionIds,
29
+ defaultActionId,
30
+ } from "./action-helpers";
31
+ import { useAutomationRegistry, useVariableScope } from "./registry-context";
32
+ import { useActionIssues } from "./editor-validation";
33
+ import {
34
+ ConditionGuardActionBody,
35
+ DelayActionBody,
36
+ ProviderActionBody,
37
+ StopActionBody,
38
+ VariablesActionBody,
39
+ WaitForTriggerActionBody,
40
+ } from "./action-leaf-cards";
41
+ import {
42
+ ChooseActionBody,
43
+ ParallelActionBody,
44
+ RepeatActionBody,
45
+ SequenceActionBody,
46
+ } from "./action-composite-cards";
47
+
48
+ export interface ActionEditorProps {
49
+ value: ActionInput;
50
+ onChange: (next: ActionInput) => void;
51
+ onDelete: () => void;
52
+ path: ActionPath;
53
+ definition: AutomationDefinition;
54
+ dragHandleProps?: React.HTMLAttributes<HTMLButtonElement>;
55
+ stableId: string;
56
+ disabled?: boolean;
57
+ }
58
+
59
+ /**
60
+ * Compact, uppercase "eyebrow" label for the action's secondary metadata
61
+ * fields (type / id / description / failure behaviour). Deliberately
62
+ * smaller and quieter than the `DynamicForm` config field labels so the
63
+ * action's own configuration stays the focal point of the card.
64
+ */
65
+ const MetaField: React.FC<{
66
+ label: string;
67
+ htmlFor?: string;
68
+ children: React.ReactNode;
69
+ }> = ({ label, htmlFor, children }) => (
70
+ <div className="space-y-1.5">
71
+ <label
72
+ htmlFor={htmlFor}
73
+ className="block text-[11px] font-medium uppercase tracking-wide text-muted-foreground"
74
+ >
75
+ {label}
76
+ </label>
77
+ {children}
78
+ </div>
79
+ );
80
+
81
+ /**
82
+ * Dispatch component — given an `ActionInput`, picks the right card
83
+ * body and wraps it in a shared `ActionCard` from `@checkstack/ui` so
84
+ * every action has the same collapsible header, enable toggle, drag
85
+ * handle, and delete button.
86
+ *
87
+ * Re-deriving the kind on every render is fine: the discriminator is
88
+ * structural and cheap to check.
89
+ */
90
+ export const ActionEditor: React.FC<ActionEditorProps> = ({
91
+ value,
92
+ onChange,
93
+ onDelete,
94
+ path,
95
+ definition,
96
+ dragHandleProps,
97
+ stableId,
98
+ disabled,
99
+ }) => {
100
+ const { actions } = useAutomationRegistry();
101
+ const kind = actionKindOf(value);
102
+ const meta = ACTION_KIND_META[kind];
103
+ const {
104
+ templateProperties,
105
+ variableNodes,
106
+ templateCompletion,
107
+ expressionCompletion,
108
+ typeDefinitions,
109
+ shellEnvVars,
110
+ } = useVariableScope({
111
+ definition,
112
+ path,
113
+ });
114
+
115
+ const title = actionDisplayName(value, (qualified) =>
116
+ actions.find((a) => a.qualifiedId === qualified)?.displayName,
117
+ );
118
+
119
+ const description =
120
+ value.description ?? (value.id ? `id: ${value.id}` : undefined);
121
+
122
+ const enabledValue = value.enabled !== false;
123
+ const issues = useActionIssues(path);
124
+
125
+ return (
126
+ <ActionCard
127
+ id={stableId}
128
+ title={title}
129
+ description={description}
130
+ category={meta.label}
131
+ icon={meta.icon}
132
+ enabled={enabledValue}
133
+ onEnabledChange={
134
+ disabled
135
+ ? undefined
136
+ : (next) => onChange({ ...value, enabled: next })
137
+ }
138
+ onDelete={disabled ? undefined : onDelete}
139
+ dragHandleProps={disabled ? undefined : dragHandleProps}
140
+ errors={issues}
141
+ >
142
+ <div className="space-y-4">
143
+ {/* Action settings — identity and failure behaviour. Grouped in a
144
+ quiet panel and labelled with small uppercase eyebrows so the
145
+ action's own configuration below reads as the primary content of
146
+ the card rather than competing with it. The action's kind is
147
+ fixed at creation; to change it, add a new step and delete this
148
+ one. */}
149
+ <div className="rounded-lg border border-border/60 bg-muted/20 p-3 space-y-3">
150
+ <div className="grid gap-x-4 gap-y-3 sm:grid-cols-2">
151
+ <MetaField label="Id" htmlFor={`${stableId}-id`}>
152
+ <Input
153
+ id={`${stableId}-id`}
154
+ value={value.id ?? ""}
155
+ onChange={(event) =>
156
+ onChange({ ...value, id: event.target.value || undefined })
157
+ }
158
+ onBlur={() => {
159
+ // Never leave the id blank: re-fill a unique, log-friendly
160
+ // default so the action stays referenceable
161
+ // (artifacts.<id>.<name>) and parseable in run logs.
162
+ if (value.id) return;
163
+ const taken = collectActionIds(definition.actions);
164
+ onChange({ ...value, id: defaultActionId(value, taken) });
165
+ }}
166
+ placeholder="Generated on blur"
167
+ disabled={disabled}
168
+ className="h-8 text-xs"
169
+ />
170
+ </MetaField>
171
+
172
+ <MetaField label="Description" htmlFor={`${stableId}-desc`}>
173
+ <Input
174
+ id={`${stableId}-desc`}
175
+ value={value.description ?? ""}
176
+ onChange={(event) =>
177
+ onChange({
178
+ ...value,
179
+ description: event.target.value || undefined,
180
+ })
181
+ }
182
+ placeholder="Optional note"
183
+ disabled={disabled}
184
+ className="h-8 text-xs"
185
+ />
186
+ </MetaField>
187
+ </div>
188
+
189
+ <MetaField label="On failure">
190
+ <div
191
+ className={cn(
192
+ "flex h-8 items-center gap-2 select-none",
193
+ disabled ? "cursor-not-allowed" : "cursor-pointer",
194
+ )}
195
+ onClick={() =>
196
+ !disabled &&
197
+ onChange({
198
+ ...value,
199
+ continue_on_error: value.continue_on_error !== true,
200
+ })
201
+ }
202
+ >
203
+ <Checkbox
204
+ checked={value.continue_on_error === true}
205
+ disabled={disabled}
206
+ />
207
+ <Label className="text-xs font-normal text-muted-foreground cursor-pointer">
208
+ Continue on error
209
+ </Label>
210
+ </div>
211
+ </MetaField>
212
+ </div>
213
+
214
+ <ActionBody
215
+ kind={kind}
216
+ value={value}
217
+ onChange={onChange}
218
+ path={path}
219
+ templateProperties={templateProperties}
220
+ variableNodes={variableNodes}
221
+ templateCompletion={templateCompletion}
222
+ expressionCompletion={expressionCompletion}
223
+ typeDefinitions={typeDefinitions}
224
+ shellEnvVars={shellEnvVars}
225
+ disabled={disabled}
226
+ />
227
+ </div>
228
+ </ActionCard>
229
+ );
230
+ };
231
+
232
+ const ActionBody: React.FC<{
233
+ kind: ReturnType<typeof actionKindOf>;
234
+ value: ActionInput;
235
+ onChange: (next: ActionInput) => void;
236
+ path: ActionPath;
237
+ templateProperties: Array<{ path: string; type: string; description?: string }>;
238
+ variableNodes: Parameters<typeof ConditionGuardActionBody>[0]["variableNodes"];
239
+ templateCompletion: Parameters<typeof DelayActionBody>[0]["completionProvider"];
240
+ expressionCompletion: Parameters<
241
+ typeof ConditionGuardActionBody
242
+ >[0]["completionProvider"];
243
+ typeDefinitions: string;
244
+ shellEnvVars: Parameters<typeof ProviderActionBody>[0]["shellEnvVars"];
245
+ disabled?: boolean;
246
+ }> = ({
247
+ kind,
248
+ value,
249
+ onChange,
250
+ path,
251
+ templateProperties,
252
+ variableNodes,
253
+ templateCompletion,
254
+ expressionCompletion,
255
+ typeDefinitions,
256
+ shellEnvVars,
257
+ disabled,
258
+ }) => {
259
+ switch (kind) {
260
+ case "action": {
261
+ return (
262
+ <ProviderActionBody
263
+ value={value as ProviderAction}
264
+ onChange={(next) => onChange(next)}
265
+ templateProperties={templateProperties}
266
+ completionProvider={templateCompletion}
267
+ typeDefinitions={typeDefinitions}
268
+ shellEnvVars={shellEnvVars}
269
+ disabled={disabled}
270
+ />
271
+ );
272
+ }
273
+ case "choose": {
274
+ return (
275
+ <ChooseActionBody
276
+ value={value as ChooseInput}
277
+ onChange={(next) => onChange(next)}
278
+ path={path}
279
+ variableNodes={variableNodes}
280
+ expressionCompletion={expressionCompletion}
281
+ disabled={disabled}
282
+ />
283
+ );
284
+ }
285
+ case "parallel": {
286
+ return (
287
+ <ParallelActionBody
288
+ value={value as ParallelInput}
289
+ onChange={(next) => onChange(next)}
290
+ path={path}
291
+ disabled={disabled}
292
+ />
293
+ );
294
+ }
295
+ case "repeat": {
296
+ return (
297
+ <RepeatActionBody
298
+ value={value as RepeatInput}
299
+ onChange={(next) => onChange(next)}
300
+ path={path}
301
+ completionProvider={templateCompletion}
302
+ disabled={disabled}
303
+ />
304
+ );
305
+ }
306
+ case "variables": {
307
+ return (
308
+ <VariablesActionBody
309
+ value={value as VariablesInput}
310
+ onChange={(next) => onChange(next)}
311
+ templateProperties={templateProperties}
312
+ disabled={disabled}
313
+ />
314
+ );
315
+ }
316
+ case "condition": {
317
+ return (
318
+ <ConditionGuardActionBody
319
+ value={value as ConditionGuardInput}
320
+ onChange={(next) => onChange(next)}
321
+ variableNodes={variableNodes}
322
+ completionProvider={expressionCompletion}
323
+ disabled={disabled}
324
+ />
325
+ );
326
+ }
327
+ case "stop": {
328
+ return (
329
+ <StopActionBody
330
+ value={value as StopInput}
331
+ onChange={(next) => onChange(next)}
332
+ disabled={disabled}
333
+ />
334
+ );
335
+ }
336
+ case "wait_for_trigger": {
337
+ return (
338
+ <WaitForTriggerActionBody
339
+ value={value as WaitForTriggerInput}
340
+ onChange={(next) => onChange(next)}
341
+ completionProvider={templateCompletion}
342
+ disabled={disabled}
343
+ />
344
+ );
345
+ }
346
+ case "sequence": {
347
+ return (
348
+ <SequenceActionBody
349
+ value={value as SequenceInput}
350
+ onChange={(next) => onChange(next)}
351
+ path={path}
352
+ disabled={disabled}
353
+ />
354
+ );
355
+ }
356
+ case "delay": {
357
+ return (
358
+ <DelayActionBody
359
+ value={value as DelayInput}
360
+ onChange={(next) => onChange(next)}
361
+ completionProvider={templateCompletion}
362
+ disabled={disabled}
363
+ />
364
+ );
365
+ }
366
+ }
367
+ };
@@ -0,0 +1,203 @@
1
+ import React from "react";
2
+ import {
3
+ DndContext,
4
+ type DragEndEvent,
5
+ PointerSensor,
6
+ closestCenter,
7
+ useSensor,
8
+ useSensors,
9
+ } from "@dnd-kit/core";
10
+ import {
11
+ SortableContext,
12
+ arrayMove,
13
+ useSortable,
14
+ verticalListSortingStrategy,
15
+ } from "@dnd-kit/sortable";
16
+ import { CSS } from "@dnd-kit/utilities";
17
+ import type {
18
+ ActionInput,
19
+ ActionPath,
20
+ ActionPathStep,
21
+ } from "@checkstack/automation-common";
22
+ import {
23
+ assignDefaultIds,
24
+ collectActionIds,
25
+ makeEmptyAction,
26
+ makeProviderAction,
27
+ } from "./action-helpers";
28
+ import { ActionEditor } from "./ActionEditor";
29
+ import { AddActionDialog } from "./AddActionDialog";
30
+ import { useAutomationDefinitionContext } from "./AutomationDefinitionContext";
31
+
32
+ type Slot = ActionPathStep["slot"];
33
+
34
+ export interface ActionListEditorProps {
35
+ value: ActionInput[];
36
+ onChange: (next: ActionInput[]) => void;
37
+ /** Path of the parent action — empty array means we're at the root. */
38
+ parentPath: ActionPath;
39
+ /**
40
+ * Describes which child slot of the parent contains this list. Drives
41
+ * `ActionPathStep`s for nested resolution. Use `{ slot: "root" }` at the
42
+ * top of the editor; composite cards pass their own (`"choose-when"` with
43
+ * `whenIndex`, `"parallel"`, `"repeat"`, `"sequence"`, `"choose-else"`).
44
+ */
45
+ slotForChildren: { slot: Slot; whenIndex?: number };
46
+ disabled?: boolean;
47
+ }
48
+
49
+ let idCounter = 0;
50
+ const nextId = (): string => {
51
+ idCounter += 1;
52
+ return `act-${idCounter}`;
53
+ };
54
+
55
+ /**
56
+ * Sortable list of actions. The hard parts:
57
+ *
58
+ * 1. **Drag-to-reorder** — we pair every value-slot with a stable
59
+ * `string` id (kept in a parallel array). Reorders shuffle both
60
+ * arrays together; edits don't touch the id array, so each card's
61
+ * expanded-state survives parent re-renders.
62
+ *
63
+ * 2. **Path computation** — each child's `ActionPath` is the parent
64
+ * path plus a final segment describing the child's position. The
65
+ * segment's `slot` comes from `slotForChildren`; the `index` is
66
+ * the child's position in the array.
67
+ *
68
+ * 3. **Add menu** — `AddActionDialog` lets the operator decide the
69
+ * step's type up front: a concrete provider action (preset via
70
+ * `makeProviderAction`) or a structural building block. Composite
71
+ * kinds start with an empty child list, which the operator fills via
72
+ * the nested add-step picker.
73
+ */
74
+ export const ActionListEditor: React.FC<ActionListEditorProps> = ({
75
+ value,
76
+ onChange,
77
+ parentPath,
78
+ slotForChildren,
79
+ disabled,
80
+ }) => {
81
+ const { definition } = useAutomationDefinitionContext();
82
+ const sensors = useSensors(
83
+ useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
84
+ );
85
+
86
+ // Parallel id array; kept in sync with `value`'s length so reorders +
87
+ // add + delete are stable, while in-place edits don't churn keys.
88
+ const [ids, setIds] = React.useState<string[]>(() => value.map(() => nextId()));
89
+ React.useEffect(() => {
90
+ setIds((current) => {
91
+ if (current.length === value.length) return current;
92
+ if (current.length < value.length) {
93
+ return [
94
+ ...current,
95
+ ...Array.from({ length: value.length - current.length }, nextId),
96
+ ];
97
+ }
98
+ return current.slice(0, value.length);
99
+ });
100
+ }, [value.length]);
101
+
102
+ const handleDragEnd = (event: DragEndEvent) => {
103
+ if (!event.over || event.active.id === event.over.id) return;
104
+ const oldIndex = ids.indexOf(String(event.active.id));
105
+ const newIndex = ids.indexOf(String(event.over.id));
106
+ if (oldIndex === -1 || newIndex === -1) return;
107
+ setIds(arrayMove(ids, oldIndex, newIndex));
108
+ onChange(arrayMove(value, oldIndex, newIndex));
109
+ };
110
+
111
+ const childPathAt = (index: number): ActionPath => [
112
+ ...parentPath,
113
+ {
114
+ slot: slotForChildren.slot,
115
+ whenIndex: slotForChildren.whenIndex,
116
+ index,
117
+ },
118
+ ];
119
+
120
+ const appendStep = (action: ActionInput): void => {
121
+ // Auto-assign log-friendly default ids (deduped against every id already
122
+ // used anywhere in the automation), covering the new step and any
123
+ // children composite kinds prime themselves with. The operator can
124
+ // rename them in the card.
125
+ const taken = collectActionIds(definition.actions);
126
+ const [fresh] = assignDefaultIds([action], taken);
127
+ onChange([...value, fresh!]);
128
+ setIds((current) => [...current, nextId()]);
129
+ };
130
+
131
+ return (
132
+ <div className="space-y-2">
133
+ <DndContext
134
+ sensors={sensors}
135
+ collisionDetection={closestCenter}
136
+ onDragEnd={handleDragEnd}
137
+ >
138
+ <SortableContext items={ids} strategy={verticalListSortingStrategy}>
139
+ {value.map((action, index) => (
140
+ <SortableActionItem
141
+ key={ids[index]}
142
+ id={ids[index]!}
143
+ value={action}
144
+ onChange={(next) => {
145
+ const list = [...value];
146
+ list[index] = next;
147
+ onChange(list);
148
+ }}
149
+ onDelete={() => {
150
+ onChange(value.filter((_, i) => i !== index));
151
+ setIds((current) => current.filter((_, i) => i !== index));
152
+ }}
153
+ path={childPathAt(index)}
154
+ definition={definition}
155
+ disabled={disabled}
156
+ />
157
+ ))}
158
+ </SortableContext>
159
+ </DndContext>
160
+ {value.length === 0 && (
161
+ <p className="text-xs italic text-muted-foreground">No steps yet.</p>
162
+ )}
163
+ <AddActionDialog
164
+ disabled={disabled}
165
+ onAddKind={(kind) => appendStep(makeEmptyAction(kind))}
166
+ onAddAction={(qualifiedId) => appendStep(makeProviderAction(qualifiedId))}
167
+ />
168
+ </div>
169
+ );
170
+ };
171
+
172
+ const SortableActionItem: React.FC<{
173
+ id: string;
174
+ value: ActionInput;
175
+ onChange: (next: ActionInput) => void;
176
+ onDelete: () => void;
177
+ path: ActionPath;
178
+ definition: Parameters<typeof ActionEditor>[0]["definition"];
179
+ disabled?: boolean;
180
+ }> = ({ id, value, onChange, onDelete, path, definition, disabled }) => {
181
+ const sortable = useSortable({ id });
182
+ const style: React.CSSProperties = {
183
+ transform: CSS.Transform.toString(sortable.transform),
184
+ transition: sortable.transition,
185
+ };
186
+ return (
187
+ <div ref={sortable.setNodeRef} style={style}>
188
+ <ActionEditor
189
+ value={value}
190
+ onChange={onChange}
191
+ onDelete={onDelete}
192
+ path={path}
193
+ definition={definition}
194
+ stableId={id}
195
+ dragHandleProps={{
196
+ ...sortable.attributes,
197
+ ...sortable.listeners,
198
+ }}
199
+ disabled={disabled}
200
+ />
201
+ </div>
202
+ );
203
+ };