@checkstack/automation-frontend 0.2.0 → 0.4.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 +496 -0
- package/package.json +18 -13
- package/src/components/AutomationGroupCombobox.tsx +133 -0
- package/src/components/RunAsServiceAccountPicker.tsx +170 -0
- package/src/editor/ActionEditor.tsx +180 -90
- package/src/editor/ActionListEditor.tsx +27 -1
- package/src/editor/AddActionDialog.tsx +15 -45
- package/src/editor/AddConditionDialog.tsx +86 -0
- package/src/editor/AddTriggerDialog.tsx +97 -0
- package/src/editor/AutomationDefinitionEditor.tsx +41 -2
- package/src/editor/ConditionEditor.tsx +359 -70
- package/src/editor/ConditionsEditor.tsx +113 -44
- package/src/editor/ItemSheet.tsx +51 -0
- package/src/editor/RunReplayPicker.tsx +97 -0
- package/src/editor/ScriptServicesBooter.tsx +57 -0
- package/src/editor/ScriptTestRenderer.tsx +150 -0
- package/src/editor/SystemEntityPicker.test.ts +37 -0
- package/src/editor/SystemEntityPicker.tsx +109 -0
- package/src/editor/TriggersEditor.tsx +345 -137
- package/src/editor/action-helpers.test.ts +107 -0
- package/src/editor/action-helpers.ts +72 -0
- package/src/editor/action-leaf-cards.tsx +105 -1
- package/src/editor/condition-kind.test.ts +126 -0
- package/src/editor/condition-kind.ts +130 -0
- package/src/editor/item-summary.test.ts +171 -0
- package/src/editor/item-summary.ts +210 -0
- package/src/editor/picker-dialog.tsx +156 -0
- package/src/editor/registry-context.tsx +9 -2
- package/src/editor/script-actions.test.ts +184 -0
- package/src/editor/script-actions.ts +146 -0
- package/src/editor/system-entity-picker.logic.ts +23 -0
- package/src/editor/template-completion.test.ts +22 -3
- package/src/editor/template-completion.ts +16 -8
- package/src/editor/template-helpers.ts +4 -0
- package/src/editor/trigger-helpers.test.ts +28 -0
- package/src/editor/trigger-helpers.ts +17 -0
- package/src/editor/useScriptDiagnostics.ts +108 -0
- package/src/index.tsx +26 -24
- package/src/pages/AutomationEditPage.tsx +116 -48
- package/src/pages/AutomationListPage.tsx +172 -123
- package/src/pages/automation-grouping.test.ts +87 -0
- package/src/pages/automation-grouping.ts +65 -0
- package/src/script-context.test.ts +142 -1
- package/src/script-context.ts +115 -0
- package/tsconfig.json +15 -0
- package/src/components/AutomationMenuItems.tsx +0 -37
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Tag, X } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
Input,
|
|
5
|
+
Popover,
|
|
6
|
+
PopoverAnchor,
|
|
7
|
+
PopoverContent,
|
|
8
|
+
comboboxAnchorProps,
|
|
9
|
+
isAnchorInteraction,
|
|
10
|
+
} from "@checkstack/ui";
|
|
11
|
+
|
|
12
|
+
export interface AutomationGroupComboboxProps {
|
|
13
|
+
/** Current group value (controlled). Empty string means "Ungrouped". */
|
|
14
|
+
value: string;
|
|
15
|
+
/** Called whenever the value changes (typing, picking, or clearing). */
|
|
16
|
+
onValueChange: (next: string) => void;
|
|
17
|
+
/** Distinct existing group values to suggest. */
|
|
18
|
+
suggestions: string[];
|
|
19
|
+
id?: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Type-new-or-pick-existing group field. Composes `Popover` + `Input` (no
|
|
26
|
+
* Combobox primitive exists) and mirrors `PackageNameCombobox`'s structure:
|
|
27
|
+
* a scrollable suggestion list filtered by the current input.
|
|
28
|
+
*
|
|
29
|
+
* Free-typed new groups are allowed (the input value IS the group). Selecting
|
|
30
|
+
* a suggestion fills the value and closes the list. Reuses the shared
|
|
31
|
+
* `comboboxInteraction` dismiss-guard so the very click that opens the list
|
|
32
|
+
* does not immediately close it (Radix treats the anchor as "outside").
|
|
33
|
+
*/
|
|
34
|
+
export const AutomationGroupCombobox: React.FC<AutomationGroupComboboxProps> = ({
|
|
35
|
+
value,
|
|
36
|
+
onValueChange,
|
|
37
|
+
suggestions,
|
|
38
|
+
id,
|
|
39
|
+
disabled,
|
|
40
|
+
placeholder,
|
|
41
|
+
}) => {
|
|
42
|
+
const [open, setOpen] = React.useState(false);
|
|
43
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
44
|
+
|
|
45
|
+
const query = value.trim().toLowerCase();
|
|
46
|
+
const filtered = React.useMemo(() => {
|
|
47
|
+
const seen = new Set<string>();
|
|
48
|
+
return suggestions.filter((s) => {
|
|
49
|
+
const key = s.toLowerCase();
|
|
50
|
+
if (seen.has(key)) return false;
|
|
51
|
+
seen.add(key);
|
|
52
|
+
return query.length === 0 || key.includes(query);
|
|
53
|
+
});
|
|
54
|
+
}, [suggestions, query]);
|
|
55
|
+
|
|
56
|
+
// Don't offer a suggestion that exactly equals the current value (nothing
|
|
57
|
+
// to pick — the user already has it).
|
|
58
|
+
const showList =
|
|
59
|
+
filtered.length > 0 &&
|
|
60
|
+
!(filtered.length === 1 && filtered[0]?.toLowerCase() === query);
|
|
61
|
+
|
|
62
|
+
const handlePick = (group: string) => {
|
|
63
|
+
onValueChange(group);
|
|
64
|
+
setOpen(false);
|
|
65
|
+
inputRef.current?.focus();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Popover open={open && showList && !disabled} onOpenChange={setOpen}>
|
|
70
|
+
<PopoverAnchor asChild>
|
|
71
|
+
<div className="relative">
|
|
72
|
+
<Tag className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
73
|
+
<Input
|
|
74
|
+
ref={inputRef}
|
|
75
|
+
id={id}
|
|
76
|
+
value={value}
|
|
77
|
+
disabled={disabled}
|
|
78
|
+
onChange={(e) => {
|
|
79
|
+
onValueChange(e.target.value);
|
|
80
|
+
setOpen(true);
|
|
81
|
+
}}
|
|
82
|
+
onFocus={() => setOpen(true)}
|
|
83
|
+
placeholder={placeholder ?? "Ungrouped"}
|
|
84
|
+
autoComplete="off"
|
|
85
|
+
className="pl-8 pr-8"
|
|
86
|
+
{...comboboxAnchorProps}
|
|
87
|
+
/>
|
|
88
|
+
{value.length > 0 && !disabled && (
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
aria-label="Clear group"
|
|
92
|
+
onClick={() => {
|
|
93
|
+
onValueChange("");
|
|
94
|
+
setOpen(false);
|
|
95
|
+
inputRef.current?.focus();
|
|
96
|
+
}}
|
|
97
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-sm text-muted-foreground hover:text-foreground"
|
|
98
|
+
>
|
|
99
|
+
<X className="h-3.5 w-3.5" />
|
|
100
|
+
</button>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
</PopoverAnchor>
|
|
104
|
+
<PopoverContent
|
|
105
|
+
align="start"
|
|
106
|
+
className="w-[--radix-popover-trigger-width] p-0"
|
|
107
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
108
|
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
109
|
+
onPointerDownOutside={(e) => {
|
|
110
|
+
if (isAnchorInteraction(e.target)) e.preventDefault();
|
|
111
|
+
}}
|
|
112
|
+
onFocusOutside={(e) => {
|
|
113
|
+
if (isAnchorInteraction(e.target)) e.preventDefault();
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<div className="max-h-60 overflow-y-auto py-1">
|
|
117
|
+
{filtered.map((group) => (
|
|
118
|
+
<button
|
|
119
|
+
key={group}
|
|
120
|
+
type="button"
|
|
121
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
122
|
+
onClick={() => handlePick(group)}
|
|
123
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground"
|
|
124
|
+
>
|
|
125
|
+
<Tag className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
126
|
+
<span className="flex-1 truncate">{group}</span>
|
|
127
|
+
</button>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
</PopoverContent>
|
|
131
|
+
</Popover>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { ExternalLink, KeyRound, ShieldCheck } from "lucide-react";
|
|
4
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
5
|
+
import { AuthApi, authRoutes } from "@checkstack/auth-common";
|
|
6
|
+
import { resolveRoute, APP_DOC_SLUGS, docsPath } from "@checkstack/common";
|
|
7
|
+
import {
|
|
8
|
+
Label,
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
InfoBanner,
|
|
15
|
+
InfoBannerIcon,
|
|
16
|
+
InfoBannerContent,
|
|
17
|
+
InfoBannerDescription,
|
|
18
|
+
EmptyState,
|
|
19
|
+
Button,
|
|
20
|
+
} from "@checkstack/ui";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Deep-links into the in-app user guide. The backend serves the Astro Starlight
|
|
24
|
+
* docs build same-origin at `/checkstack/*`, so these are same-origin paths
|
|
25
|
+
* opened in a new tab. They are static doc pages, not app routes - hence a plain
|
|
26
|
+
* anchor (below), never a react-router `<Link>`. Slugs are centralised in
|
|
27
|
+
* `APP_DOC_SLUGS` and guarded by `docs-links.test.ts` against renames.
|
|
28
|
+
*/
|
|
29
|
+
const DOCS_TEAMS_AND_ACCESS = docsPath(APP_DOC_SLUGS.teamsAndAccess);
|
|
30
|
+
const DOCS_SERVICE_ACCOUNTS = docsPath(APP_DOC_SLUGS.apiKeys);
|
|
31
|
+
|
|
32
|
+
/** Route to the Applications admin tab in auth settings. */
|
|
33
|
+
const APPLICATIONS_ADMIN_ROUTE = `${resolveRoute(
|
|
34
|
+
authRoutes.routes.settings,
|
|
35
|
+
)}?tab=applications`;
|
|
36
|
+
|
|
37
|
+
export interface RunAsServiceAccountPickerProps {
|
|
38
|
+
/** Selected application id, or empty string when nothing is chosen yet. */
|
|
39
|
+
value: string;
|
|
40
|
+
/** Called with the chosen application id. */
|
|
41
|
+
onValueChange: (next: string) => void;
|
|
42
|
+
/** When false, the picker is read-only (matches the page's `canManage`). */
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
/** Show the inline "required" validation hint. */
|
|
45
|
+
showError?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* "Run as (Service Account)" picker for the automation editor.
|
|
50
|
+
*
|
|
51
|
+
* Populated from the auth plugin's `getBindableApplications` query, which
|
|
52
|
+
* server-side filters to exactly the applications THIS user is allowed to
|
|
53
|
+
* bind (their access rules are a subset of the caller's). The automation
|
|
54
|
+
* then runs with the chosen application's permissions, so picking one is
|
|
55
|
+
* required.
|
|
56
|
+
*
|
|
57
|
+
* Teaching UX:
|
|
58
|
+
* - An info banner explains what a service account is and why it is
|
|
59
|
+
* required.
|
|
60
|
+
* - When no applications are bindable, an `EmptyState` points the user to
|
|
61
|
+
* the Applications admin and the docs instead of leaving them stuck.
|
|
62
|
+
*/
|
|
63
|
+
export const RunAsServiceAccountPicker: React.FC<
|
|
64
|
+
RunAsServiceAccountPickerProps
|
|
65
|
+
> = ({ value, onValueChange, disabled, showError }) => {
|
|
66
|
+
const authClient = usePluginClient(AuthApi);
|
|
67
|
+
const { data: applications = [], isLoading } =
|
|
68
|
+
authClient.getBindableApplications.useQuery({});
|
|
69
|
+
|
|
70
|
+
const hasNoApplications = !isLoading && applications.length === 0;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="space-y-2">
|
|
74
|
+
<div className="space-y-1">
|
|
75
|
+
<Label htmlFor="runAs">Run as (Service Account)</Label>
|
|
76
|
+
<Select
|
|
77
|
+
value={value || undefined}
|
|
78
|
+
onValueChange={onValueChange}
|
|
79
|
+
disabled={disabled || hasNoApplications || isLoading}
|
|
80
|
+
>
|
|
81
|
+
<SelectTrigger
|
|
82
|
+
id="runAs"
|
|
83
|
+
aria-invalid={showError ? true : undefined}
|
|
84
|
+
className={showError ? "border-destructive" : undefined}
|
|
85
|
+
>
|
|
86
|
+
<SelectValue
|
|
87
|
+
placeholder={
|
|
88
|
+
isLoading ? "Loading service accounts..." : "Select a service account"
|
|
89
|
+
}
|
|
90
|
+
/>
|
|
91
|
+
</SelectTrigger>
|
|
92
|
+
<SelectContent>
|
|
93
|
+
{applications.map((app) => (
|
|
94
|
+
<SelectItem key={app.id} value={app.id}>
|
|
95
|
+
<span className="flex flex-col text-left">
|
|
96
|
+
<span className="truncate">{app.name}</span>
|
|
97
|
+
{app.description ? (
|
|
98
|
+
<span className="text-xs text-muted-foreground truncate">
|
|
99
|
+
{app.description}
|
|
100
|
+
</span>
|
|
101
|
+
) : null}
|
|
102
|
+
</span>
|
|
103
|
+
</SelectItem>
|
|
104
|
+
))}
|
|
105
|
+
</SelectContent>
|
|
106
|
+
</Select>
|
|
107
|
+
{showError && !hasNoApplications && (
|
|
108
|
+
<p className="text-xs text-destructive">
|
|
109
|
+
A service account is required.
|
|
110
|
+
</p>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{hasNoApplications ? (
|
|
115
|
+
<EmptyState
|
|
116
|
+
icon={<KeyRound className="h-7 w-7" />}
|
|
117
|
+
title="No service accounts available"
|
|
118
|
+
description={
|
|
119
|
+
<>
|
|
120
|
+
An automation runs with a service account's permissions, but you
|
|
121
|
+
do not have any to bind yet. Create or get access to one in the
|
|
122
|
+
Applications admin, then come back here.
|
|
123
|
+
</>
|
|
124
|
+
}
|
|
125
|
+
actions={
|
|
126
|
+
<>
|
|
127
|
+
<Link to={APPLICATIONS_ADMIN_ROUTE}>
|
|
128
|
+
<Button size="sm" variant="outline">
|
|
129
|
+
Open Applications admin
|
|
130
|
+
</Button>
|
|
131
|
+
</Link>
|
|
132
|
+
<a
|
|
133
|
+
href={DOCS_SERVICE_ACCOUNTS}
|
|
134
|
+
target="_blank"
|
|
135
|
+
rel="noreferrer"
|
|
136
|
+
>
|
|
137
|
+
<Button size="sm" variant="ghost">
|
|
138
|
+
Learn more
|
|
139
|
+
<ExternalLink className="ml-1 h-3.5 w-3.5" />
|
|
140
|
+
</Button>
|
|
141
|
+
</a>
|
|
142
|
+
</>
|
|
143
|
+
}
|
|
144
|
+
/>
|
|
145
|
+
) : (
|
|
146
|
+
<InfoBanner variant="info">
|
|
147
|
+
<InfoBannerIcon>
|
|
148
|
+
<ShieldCheck className="h-4 w-4" />
|
|
149
|
+
</InfoBannerIcon>
|
|
150
|
+
<InfoBannerContent>
|
|
151
|
+
<InfoBannerDescription>
|
|
152
|
+
A service account is an application the automation runs as. It
|
|
153
|
+
executes with that account's permissions (assigned via roles), so
|
|
154
|
+
picking one is required.{" "}
|
|
155
|
+
<a
|
|
156
|
+
href={DOCS_TEAMS_AND_ACCESS}
|
|
157
|
+
target="_blank"
|
|
158
|
+
rel="noreferrer"
|
|
159
|
+
className="inline-flex items-center gap-0.5 underline underline-offset-2 hover:no-underline"
|
|
160
|
+
>
|
|
161
|
+
Learn more
|
|
162
|
+
<ExternalLink className="h-3 w-3" />
|
|
163
|
+
</a>
|
|
164
|
+
</InfoBannerDescription>
|
|
165
|
+
</InfoBannerContent>
|
|
166
|
+
</InfoBanner>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import {
|
|
3
|
+
Accordion,
|
|
4
|
+
AccordionContent,
|
|
5
|
+
AccordionItem,
|
|
6
|
+
AccordionTrigger,
|
|
3
7
|
ActionCard,
|
|
8
|
+
type ActionCardMenuItem,
|
|
4
9
|
Checkbox,
|
|
5
10
|
cn,
|
|
6
11
|
Input,
|
|
@@ -20,6 +25,7 @@ import type {
|
|
|
20
25
|
StopInput,
|
|
21
26
|
VariablesInput,
|
|
22
27
|
WaitForTriggerInput,
|
|
28
|
+
WaitUntilInput,
|
|
23
29
|
} from "@checkstack/automation-common";
|
|
24
30
|
import {
|
|
25
31
|
ACTION_KIND_META,
|
|
@@ -28,6 +34,8 @@ import {
|
|
|
28
34
|
collectActionIds,
|
|
29
35
|
defaultActionId,
|
|
30
36
|
} from "./action-helpers";
|
|
37
|
+
import { summarizeAction } from "./item-summary";
|
|
38
|
+
import { ItemSheet } from "./ItemSheet";
|
|
31
39
|
import { useAutomationRegistry, useVariableScope } from "./registry-context";
|
|
32
40
|
import { useActionIssues } from "./editor-validation";
|
|
33
41
|
import {
|
|
@@ -37,6 +45,7 @@ import {
|
|
|
37
45
|
StopActionBody,
|
|
38
46
|
VariablesActionBody,
|
|
39
47
|
WaitForTriggerActionBody,
|
|
48
|
+
WaitUntilActionBody,
|
|
40
49
|
} from "./action-leaf-cards";
|
|
41
50
|
import {
|
|
42
51
|
ChooseActionBody,
|
|
@@ -49,6 +58,8 @@ export interface ActionEditorProps {
|
|
|
49
58
|
value: ActionInput;
|
|
50
59
|
onChange: (next: ActionInput) => void;
|
|
51
60
|
onDelete: () => void;
|
|
61
|
+
/** Clone this action (fresh ids) directly after itself. Omit to hide. */
|
|
62
|
+
onDuplicate?: () => void;
|
|
52
63
|
path: ActionPath;
|
|
53
64
|
definition: AutomationDefinition;
|
|
54
65
|
dragHandleProps?: React.HTMLAttributes<HTMLButtonElement>;
|
|
@@ -91,6 +102,7 @@ export const ActionEditor: React.FC<ActionEditorProps> = ({
|
|
|
91
102
|
value,
|
|
92
103
|
onChange,
|
|
93
104
|
onDelete,
|
|
105
|
+
onDuplicate,
|
|
94
106
|
path,
|
|
95
107
|
definition,
|
|
96
108
|
dragHandleProps,
|
|
@@ -100,6 +112,7 @@ export const ActionEditor: React.FC<ActionEditorProps> = ({
|
|
|
100
112
|
const { actions } = useAutomationRegistry();
|
|
101
113
|
const kind = actionKindOf(value);
|
|
102
114
|
const meta = ACTION_KIND_META[kind];
|
|
115
|
+
const [sheetOpen, setSheetOpen] = React.useState(false);
|
|
103
116
|
const {
|
|
104
117
|
templateProperties,
|
|
105
118
|
variableNodes,
|
|
@@ -116,101 +129,73 @@ export const ActionEditor: React.FC<ActionEditorProps> = ({
|
|
|
116
129
|
actions.find((a) => a.qualifiedId === qualified)?.displayName,
|
|
117
130
|
);
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
// Collapsed-row summary derived from the config; falls back to the
|
|
133
|
+
// operator's description/id note when there's no derivable summary.
|
|
134
|
+
const summary =
|
|
135
|
+
summarizeAction(value) ??
|
|
136
|
+
value.description ??
|
|
137
|
+
(value.id ? `id: ${value.id}` : undefined);
|
|
121
138
|
|
|
122
139
|
const enabledValue = value.enabled !== false;
|
|
123
140
|
const issues = useActionIssues(path);
|
|
141
|
+
// `id` auto-fills on blur, so it isn't a meaningful "advanced was
|
|
142
|
+
// customised" signal; open the metadata disclosure only when the operator
|
|
143
|
+
// set a description or flipped the failure behaviour.
|
|
144
|
+
const hasAdvancedMeta =
|
|
145
|
+
value.description !== undefined || value.continue_on_error === true;
|
|
124
146
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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>
|
|
147
|
+
// Validation issues (structural + inline-script type errors) surface only as
|
|
148
|
+
// the card's error badge - never auto-open the sheet. Auto-opening would pop
|
|
149
|
+
// multiple sheets at once when several cards have errors (e.g. every script
|
|
150
|
+
// action after a trigger change), so the badge is the single, calm signal and
|
|
151
|
+
// the operator clicks in for detail.
|
|
171
152
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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>
|
|
153
|
+
const menuItems: ActionCardMenuItem[] = disabled
|
|
154
|
+
? []
|
|
155
|
+
: [
|
|
156
|
+
{
|
|
157
|
+
label: enabledValue ? "Disable" : "Enable",
|
|
158
|
+
icon: enabledValue ? "PowerOff" : "Power",
|
|
159
|
+
onClick: () => onChange({ ...value, enabled: !enabledValue }),
|
|
160
|
+
},
|
|
161
|
+
...(onDuplicate
|
|
162
|
+
? [
|
|
163
|
+
{
|
|
164
|
+
label: "Duplicate",
|
|
165
|
+
icon: "Copy" as const,
|
|
166
|
+
onClick: onDuplicate,
|
|
167
|
+
},
|
|
168
|
+
]
|
|
169
|
+
: []),
|
|
170
|
+
{
|
|
171
|
+
label: "Delete",
|
|
172
|
+
icon: "Trash2",
|
|
173
|
+
onClick: onDelete,
|
|
174
|
+
variant: "destructive",
|
|
175
|
+
},
|
|
176
|
+
];
|
|
213
177
|
|
|
178
|
+
return (
|
|
179
|
+
<>
|
|
180
|
+
<ActionCard
|
|
181
|
+
id={stableId}
|
|
182
|
+
title={title}
|
|
183
|
+
summary={summary}
|
|
184
|
+
category={meta.label}
|
|
185
|
+
icon={meta.icon}
|
|
186
|
+
enabled={enabledValue}
|
|
187
|
+
onOpenSheet={() => setSheetOpen(true)}
|
|
188
|
+
actions={menuItems}
|
|
189
|
+
dragHandleProps={disabled ? undefined : dragHandleProps}
|
|
190
|
+
errors={issues}
|
|
191
|
+
/>
|
|
192
|
+
<ItemSheet
|
|
193
|
+
open={sheetOpen}
|
|
194
|
+
onOpenChange={setSheetOpen}
|
|
195
|
+
title={title}
|
|
196
|
+
description={meta.label}
|
|
197
|
+
>
|
|
198
|
+
<div className="space-y-4">
|
|
214
199
|
<ActionBody
|
|
215
200
|
kind={kind}
|
|
216
201
|
value={value}
|
|
@@ -224,8 +209,102 @@ export const ActionEditor: React.FC<ActionEditorProps> = ({
|
|
|
224
209
|
shellEnvVars={shellEnvVars}
|
|
225
210
|
disabled={disabled}
|
|
226
211
|
/>
|
|
212
|
+
|
|
213
|
+
{/* Per-action metadata — id, description, failure behaviour — tucked
|
|
214
|
+
behind a collapsed disclosure so the action's own configuration
|
|
215
|
+
above reads as the primary content of the card. The action's kind
|
|
216
|
+
is fixed at creation; to change it, add a new step and delete this
|
|
217
|
+
one. Enable/disable lives on the card header, not here. */}
|
|
218
|
+
<Accordion
|
|
219
|
+
type="single"
|
|
220
|
+
collapsible
|
|
221
|
+
defaultValue={hasAdvancedMeta ? "advanced" : undefined}
|
|
222
|
+
className="w-full"
|
|
223
|
+
>
|
|
224
|
+
<AccordionItem value="advanced" className="border-b-0">
|
|
225
|
+
<AccordionTrigger className="py-2 text-xs hover:no-underline">
|
|
226
|
+
<span className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
227
|
+
Advanced
|
|
228
|
+
</span>
|
|
229
|
+
</AccordionTrigger>
|
|
230
|
+
<AccordionContent className="pb-0">
|
|
231
|
+
<div className="rounded-lg border border-border/60 bg-muted/20 p-3 space-y-3">
|
|
232
|
+
<div className="grid gap-x-4 gap-y-3 sm:grid-cols-2">
|
|
233
|
+
<MetaField label="Id" htmlFor={`${stableId}-id`}>
|
|
234
|
+
<Input
|
|
235
|
+
id={`${stableId}-id`}
|
|
236
|
+
value={value.id ?? ""}
|
|
237
|
+
onChange={(event) =>
|
|
238
|
+
onChange({
|
|
239
|
+
...value,
|
|
240
|
+
id: event.target.value || undefined,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
onBlur={() => {
|
|
244
|
+
// Never leave the id blank: re-fill a unique,
|
|
245
|
+
// log-friendly default so the action stays
|
|
246
|
+
// referenceable (artifacts.<id>.<name>) and parseable
|
|
247
|
+
// in run logs.
|
|
248
|
+
if (value.id) return;
|
|
249
|
+
const taken = collectActionIds(definition.actions);
|
|
250
|
+
onChange({
|
|
251
|
+
...value,
|
|
252
|
+
id: defaultActionId(value, taken),
|
|
253
|
+
});
|
|
254
|
+
}}
|
|
255
|
+
placeholder="Generated on blur"
|
|
256
|
+
disabled={disabled}
|
|
257
|
+
className="h-8 text-xs"
|
|
258
|
+
/>
|
|
259
|
+
</MetaField>
|
|
260
|
+
|
|
261
|
+
<MetaField label="Description" htmlFor={`${stableId}-desc`}>
|
|
262
|
+
<Input
|
|
263
|
+
id={`${stableId}-desc`}
|
|
264
|
+
value={value.description ?? ""}
|
|
265
|
+
onChange={(event) =>
|
|
266
|
+
onChange({
|
|
267
|
+
...value,
|
|
268
|
+
description: event.target.value || undefined,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
placeholder="Optional note"
|
|
272
|
+
disabled={disabled}
|
|
273
|
+
className="h-8 text-xs"
|
|
274
|
+
/>
|
|
275
|
+
</MetaField>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<MetaField label="On failure">
|
|
279
|
+
<div
|
|
280
|
+
className={cn(
|
|
281
|
+
"flex h-8 items-center gap-2 select-none",
|
|
282
|
+
disabled ? "cursor-not-allowed" : "cursor-pointer",
|
|
283
|
+
)}
|
|
284
|
+
onClick={() =>
|
|
285
|
+
!disabled &&
|
|
286
|
+
onChange({
|
|
287
|
+
...value,
|
|
288
|
+
continue_on_error: value.continue_on_error !== true,
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
>
|
|
292
|
+
<Checkbox
|
|
293
|
+
checked={value.continue_on_error === true}
|
|
294
|
+
disabled={disabled}
|
|
295
|
+
/>
|
|
296
|
+
<Label className="text-xs font-normal text-muted-foreground cursor-pointer">
|
|
297
|
+
Continue on error
|
|
298
|
+
</Label>
|
|
299
|
+
</div>
|
|
300
|
+
</MetaField>
|
|
301
|
+
</div>
|
|
302
|
+
</AccordionContent>
|
|
303
|
+
</AccordionItem>
|
|
304
|
+
</Accordion>
|
|
227
305
|
</div>
|
|
228
|
-
|
|
306
|
+
</ItemSheet>
|
|
307
|
+
</>
|
|
229
308
|
);
|
|
230
309
|
};
|
|
231
310
|
|
|
@@ -343,6 +422,17 @@ const ActionBody: React.FC<{
|
|
|
343
422
|
/>
|
|
344
423
|
);
|
|
345
424
|
}
|
|
425
|
+
case "wait_until": {
|
|
426
|
+
return (
|
|
427
|
+
<WaitUntilActionBody
|
|
428
|
+
value={value as WaitUntilInput}
|
|
429
|
+
onChange={(next) => onChange(next)}
|
|
430
|
+
variableNodes={variableNodes}
|
|
431
|
+
completionProvider={expressionCompletion}
|
|
432
|
+
disabled={disabled}
|
|
433
|
+
/>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
346
436
|
case "sequence": {
|
|
347
437
|
return (
|
|
348
438
|
<SequenceActionBody
|