@checkstack/automation-frontend 0.7.1 → 0.9.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 CHANGED
@@ -1,5 +1,192 @@
1
1
  # @checkstack/automation-frontend
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 748dc50: Fix automation expression fields and harden the Jira search action so actions are reliable to author.
8
+
9
+ - **Expression fields reject `{{ }}` at save time.** `when` / `conditions` / a `condition` guard / `wait_until.condition` / a trigger or `wait_for_trigger` `filter` / `repeat.for_each|while|until` / `numeric_state.value` are BARE expressions and reference fields directly. Wrapping one in `{{ }}` (template syntax) used to pass validation and then throw a parse error at dispatch time. A new schema refinement (`collectExpressionDelimiterIssues`) now blocks the save (create / update / GitOps / editor) with a clear message. The misleading "Template returning truthy/falsy" schema descriptions are reworded to say "bare expression (no `{{ }}`)".
10
+ - **Fixed three built-in templates** that wrapped their `when` condition in `{{ }}` (`ai-triage-file-jira-bug`, `jira-comment-transition-on-recovery`, `ai-severity-escalation`) and the webhook-subscription migration that emitted a `{{ }}`-wrapped `systemFilter` condition.
11
+ - **The artifact-wiring validator now scans bare expression conditions**, not just `{{ }}` template spans, so a dropped `<artifactType>` segment (e.g. `artifacts.find.found` instead of `artifacts.find.issue_search.found`) is still caught in a `when` / `condition`.
12
+ - **Jira `search_issues` correlation overhaul.** `statusCategory` is now a dropdown (`new` / `indeterminate` / `done`) instead of free text. A new `labels` filter (`labels in (...)`, AND of all labels) is the reliable way to find the ticket for a specific system, and the two Jira built-in templates now tag issues with a stable `checkstack-sys-<systemId>` label on create and search by it instead of fuzzy `summaryContains`. A search whose CONFIGURED filter renders empty now fails loudly instead of silently broadening to "every ticket in the project". Results are ordered `created DESC` so `firstIssueKey` is deterministic.
13
+ - **Fixed `{{ }}` autocomplete hiding upstream artifacts in raw config fields.** The raw multi-type editor (used by Jira `summary` / `description` and every other `["raw"]` action-config field) matched its autocomplete query without trimming the leading space after `{{`, so typing `{{ arti` produced the query `" arti"`, which matched nothing — the popup emptied the instant a letter was typed and `artifacts.*` (and all other fields) never appeared. The query is now trimmed before matching (the autocomplete logic was extracted to a pure, unit-tested helper). This was most visible on an action nested in a `choose` branch, where upstream artifacts are exactly what you reference. The popup rows also now keep the distinguishing leaf segment (`…analysis.summary`) visible instead of end-truncating every deep path to an identical shared prefix.
14
+ - **Editor UX: expression vs template is now visible.** `TemplateValueInput` gains a `mode` prop; in `expression` mode it shows a focus hint ("reference fields directly, without `{{ }}`") and an inline error the moment a `{{ }}` delimiter is typed, instead of failing only at save / run time. Every expression-field editor (conditions, trigger / `wait_for_trigger` filters, `repeat` for_each/while/until, `window.partitionBy`) now runs in expression mode, and their misleading "Filter template" / "Condition template" labels and `{{ }}` placeholders are corrected.
15
+ - **AI assistant guidance corrected.** The `building-automations` doc (bundled into the AI docs index) no longer tells the model to wrap a condition in `{{ }}`.
16
+ - **Jira provider docs corrected.** The setup help advertised a fabricated nested `payload.system.name`; platform events expose flat `systemId` / `systemName`, so the example payload and template-syntax snippet now use the real flat shape.
17
+
18
+ BREAKING CHANGE: An automation definition that wrongly wrapped a condition / filter in `{{ }}` is now rejected on save. These definitions already failed at run time; re-save them with the braces removed (e.g. `artifacts.find.issue_search.found != true`).
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [748dc50]
23
+ - @checkstack/automation-common@0.8.0
24
+ - @checkstack/ui@1.18.0
25
+ - @checkstack/auth-frontend@0.9.1
26
+ - @checkstack/gitops-frontend@0.6.1
27
+ - @checkstack/script-packages-frontend@0.4.1
28
+ - @checkstack/secrets-frontend@0.3.1
29
+
30
+ ## 0.8.0
31
+
32
+ ### Minor Changes
33
+
34
+ - 8cad340: feat: live run polling, optimistic automation toggle, and relative public-status freshness
35
+
36
+ Implements three loading/feedback UX findings from the read-only review.
37
+
38
+ - **Automation run detail goes live.** `RunDetailPage` now polls
39
+ `getRun` every 2s while the run is `running`/`waiting` and stops the
40
+ moment it reaches a terminal status, so a watched execution updates
41
+ its status badge and step timeline without a manual reload. A subtle
42
+ "Live" indicator shows in the header while polling.
43
+ - **Optimistic automation enable/disable.** The per-row toggle on
44
+ `AutomationListPage` now applies the documented optimistic pattern:
45
+ `onMutate` cancels in-flight refetches, snapshots, and flips the row
46
+ in the cache so the switch flips on click; `onError` rolls back from
47
+ the snapshot and surfaces an error toast; `onSettled` invalidates to
48
+ reconcile with server truth. The success toast is suppressed (the
49
+ switch flip is the feedback), per `optimistic-updates.md`.
50
+ - **Relative, visibly-live public-status freshness.** The public status
51
+ page renders "Updated x ago" as relative time (was a static absolute
52
+ timestamp) and ticks periodically so the wording stays honest. A small
53
+ refresh dot pulses on each successful 60s refetch (gated behind
54
+ `usePerformance().isLowPower`, falling back to a static dot on
55
+ low-power devices). The "auto-updates every minute" copy is unchanged.
56
+
57
+ BREAKING CHANGE: the automation enable/disable toggle no longer raises a
58
+ "<name> enabled/disabled" success toast; the optimistic switch flip is now
59
+ the sole success feedback (error toast retained on failure).
60
+
61
+ - 8cad340: Make data-dense tables mobile-friendly and align status colors with semantic tokens.
62
+
63
+ - Migrated the remaining data-dense tables to the `ResponsiveTable` + `MobileCardList` dual-layout: catalog (Systems/Groups/Environments), incident config, maintenance config + system history, announcement management, notification delivery attempts, plugin manager (installed plugins + events), satellite list, automation list, healthcheck runs, OAuth applications, and the queue runtime panel. On viewports below `sm` these now render stacked cards surfacing the high-priority fields instead of an overflowing table. Genuinely narrow or runtime-diagnostic panels (cache runtime, healthcheck history, anomaly mute list) were intentionally left as plain tables.
64
+ - Swapped hardcoded semantic status colors for design tokens (`text-warning`, `text-success`, `text-destructive`, `text-muted-foreground`) in GitOps provenance status, healthcheck editor warnings, dependency canvas node status, automation run-step status, queue runtime tone map, and script-packages settings. Chart-series literals, syntax/terminal palettes, and intentional brand accents (tips lightbulb, SLO streak flame ramp) were left untouched.
65
+ - Extracted pure display/validation logic into sibling `.logic.ts` modules (SLO display + editor, maintenance editor + config summary, dependency display, incident sort + validation, gitops kind-registry YAML) so it can be unit-tested in isolation. These extractions are behavior-preserving.
66
+
67
+ - 8cad340: Explain why Save is disabled and guard against losing unsaved edits in the
68
+ automation and health-check editors.
69
+
70
+ - A greyed-out Save is no longer a dead end: both editors now render a
71
+ "N issue(s) blocking" affordance next to the Save button. Opening it
72
+ lists every blocker, and clicking one jumps to the offending field/section
73
+ (the automation Name / Run-as fields or the visual definition editor; the
74
+ health-check tree node that owns the issue). The existing validation logic is
75
+ unchanged - the blockers are just surfaced and made actionable.
76
+ - The first field of a fresh automation (Name) now auto-focuses so keyboard-first
77
+ users can type immediately.
78
+ - Both editors now use the shared `useUnsavedChanges` hook for unsaved-changes
79
+ protection: a native prompt on tab close / refresh plus an in-app
80
+ "Discard unsaved changes?" confirmation when navigating away mid-edit. The
81
+ health-check editor's previous hand-rolled `beforeunload` listener is migrated
82
+ to the shared hook; the automation editor gains dirty tracking and the same
83
+ guard.
84
+
85
+ ### Patch Changes
86
+
87
+ - 8cad340: Design-system rework: a premium, consistent UI language across the platform.
88
+
89
+ Foundation (`@checkstack/ui` + the shared Tailwind preset):
90
+
91
+ - A token system wired into the shared preset so it generates app-wide: a
92
+ surface elevation ramp (`surface` / `surface-2` / `surface-inset`), the
93
+ aurora gradient stops, a colorblind-safe `status` triad, and `grid-line`.
94
+ - A density model (`comfortable` / `compact`) via `--d-*` vars + `DensityProvider`
95
+ / `useDensity`, with a user-menu density toggle, plus the polished
96
+ skeleton / empty / error state set.
97
+ - Honest, token-driven chart primitives (`TimeSeriesChart`, `Sparkline`,
98
+ `RadialGauge` / aurora hero, `RequestWaterfall`, `UptimeRibbon`).
99
+ - A signature aurora moment per page: `PageHeader` paints its icon strokes with
100
+ the aurora gradient and adds a hairline; `Card` gains soft layered depth.
101
+
102
+ Shell + surfaces:
103
+
104
+ - The app shell adopts the elevation ramp (header `surface-2`, sidebar
105
+ `surface`, content on the ambient base).
106
+ - The system-health dashboard, health-check latency / single-run views, and the
107
+ SLO dashboard are reskinned onto the primitives (aurora confidence gauge,
108
+ honest p50/p95 latency, request waterfall, number-led status cards).
109
+
110
+ App-wide adoption + premium rework:
111
+
112
+ - Every plugin frontend adopts the tokens, status triad, density, and elevation.
113
+ - The highest-impact surfaces in each plugin are then redesigned to a premium
114
+ bar: real depth, number-led hierarchy, multi-encoded status (pill + dot +
115
+ accent stripe), and refined list/table density. Several plugins extract pure
116
+ tone/label/format logic into unit-tested modules.
117
+
118
+ Alerts:
119
+
120
+ - Every alert/callout is unified onto a single premium `Alert` (depth surface +
121
+ status-accent stripe + toned icon chip, variant-driven).
122
+
123
+ BREAKING CHANGE: the duplicate `InfoBanner` component (and its sub-components)
124
+ is removed; use `Alert` instead - it is a drop-in replacement with the same
125
+ variants and composable parts.
126
+
127
+ - 8cad340: fix: make data tables responsive on narrow viewports
128
+
129
+ The users, teams, and roles management tables (auth-frontend), the automation
130
+ run-history table (automation-frontend), and the integration provider
131
+ connections table (integration-frontend) previously overflowed horizontally on
132
+ phone-width (~375px) viewports. Each now uses the `ResponsiveTable` +
133
+ `MobileCardList` dual-layout primitive from `@checkstack/ui`: the existing table
134
+ renders unchanged on `sm` and up, with a stacked per-row card surfacing the key
135
+ fields and action buttons below `sm`. Shared per-row rendering (role checkboxes,
136
+ team/role/connection action buttons, connection status) was lifted into small
137
+ local components so both layouts stay in sync.
138
+
139
+ - 8cad340: Adopt the canonical `toastError` helper from `@checkstack/ui` for error toasts.
140
+
141
+ Error toasts that previously called `toast.error(extractErrorMessage(error, "Failed to X"))`
142
+ (or interpolated `Failed to X: ${extractErrorMessage(error)}` strings) now use
143
+ `toastError(toast, "Failed to X", error)`. This centralizes the
144
+ "Failed to <action>: <message>" voice and applies the shared 100-character
145
+ truncation. Error toasts that did not previously prefix the action now gain the
146
+ canonical prefix; success toasts and terse validation one-liners are unchanged.
147
+
148
+ - Updated dependencies [8cad340]
149
+ - Updated dependencies [8cad340]
150
+ - Updated dependencies [8cad340]
151
+ - Updated dependencies [8cad340]
152
+ - Updated dependencies [8cad340]
153
+ - Updated dependencies [8cad340]
154
+ - Updated dependencies [8cad340]
155
+ - Updated dependencies [8cad340]
156
+ - Updated dependencies [8cad340]
157
+ - Updated dependencies [8cad340]
158
+ - Updated dependencies [8cad340]
159
+ - Updated dependencies [8cad340]
160
+ - Updated dependencies [8cad340]
161
+ - Updated dependencies [8cad340]
162
+ - Updated dependencies [8cad340]
163
+ - Updated dependencies [8cad340]
164
+ - Updated dependencies [8cad340]
165
+ - Updated dependencies [8cad340]
166
+ - Updated dependencies [8cad340]
167
+ - Updated dependencies [8cad340]
168
+ - Updated dependencies [8cad340]
169
+ - Updated dependencies [8cad340]
170
+ - Updated dependencies [8cad340]
171
+ - Updated dependencies [8cad340]
172
+ - Updated dependencies [8cad340]
173
+ - Updated dependencies [8cad340]
174
+ - Updated dependencies [8cad340]
175
+ - @checkstack/auth-frontend@0.9.0
176
+ - @checkstack/ai-common@0.6.0
177
+ - @checkstack/ui@1.17.0
178
+ - @checkstack/gitops-frontend@0.6.0
179
+ - @checkstack/script-packages-frontend@0.4.0
180
+ - @checkstack/secrets-frontend@0.3.0
181
+ - @checkstack/common@0.17.0
182
+ - @checkstack/auth-common@0.11.0
183
+ - @checkstack/frontend-api@0.11.1
184
+ - @checkstack/catalog-common@2.4.2
185
+ - @checkstack/automation-common@0.7.1
186
+ - @checkstack/integration-common@0.9.2
187
+ - @checkstack/template-engine@0.4.6
188
+ - @checkstack/signal-frontend@0.2.6
189
+
3
190
  ## 0.7.1
4
191
 
5
192
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/automation-frontend",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -13,20 +13,20 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/auth-common": "0.10.0",
17
- "@checkstack/auth-frontend": "0.8.1",
18
- "@checkstack/automation-common": "0.7.0",
19
- "@checkstack/ai-common": "0.5.0",
20
- "@checkstack/catalog-common": "2.4.1",
21
- "@checkstack/common": "0.16.0",
22
- "@checkstack/frontend-api": "0.11.0",
23
- "@checkstack/gitops-frontend": "0.5.9",
24
- "@checkstack/integration-common": "0.9.1",
25
- "@checkstack/script-packages-frontend": "0.3.13",
26
- "@checkstack/secrets-frontend": "0.2.8",
27
- "@checkstack/signal-frontend": "0.2.5",
28
- "@checkstack/template-engine": "0.4.5",
29
- "@checkstack/ui": "1.16.2",
16
+ "@checkstack/auth-common": "0.11.0",
17
+ "@checkstack/auth-frontend": "0.9.1",
18
+ "@checkstack/automation-common": "0.8.0",
19
+ "@checkstack/ai-common": "0.6.0",
20
+ "@checkstack/catalog-common": "2.4.2",
21
+ "@checkstack/common": "0.17.0",
22
+ "@checkstack/frontend-api": "0.11.1",
23
+ "@checkstack/gitops-frontend": "0.6.1",
24
+ "@checkstack/integration-common": "0.9.2",
25
+ "@checkstack/script-packages-frontend": "0.4.1",
26
+ "@checkstack/secrets-frontend": "0.3.1",
27
+ "@checkstack/signal-frontend": "0.2.6",
28
+ "@checkstack/template-engine": "0.4.6",
29
+ "@checkstack/ui": "1.18.0",
30
30
  "@dnd-kit/core": "^6.3.1",
31
31
  "@dnd-kit/sortable": "^8.0.0",
32
32
  "@dnd-kit/utilities": "^3.2.2",
@@ -40,6 +40,6 @@
40
40
  "typescript": "^5.0.0",
41
41
  "@types/react": "^19.0.0",
42
42
  "@checkstack/tsconfig": "0.0.7",
43
- "@checkstack/scripts": "0.6.2"
43
+ "@checkstack/scripts": "0.6.3"
44
44
  }
45
45
  }
@@ -11,10 +11,10 @@ import {
11
11
  SelectItem,
12
12
  SelectTrigger,
13
13
  SelectValue,
14
- InfoBanner,
15
- InfoBannerIcon,
16
- InfoBannerContent,
17
- InfoBannerDescription,
14
+ Alert,
15
+ AlertIcon,
16
+ AlertContent,
17
+ AlertDescription,
18
18
  EmptyState,
19
19
  Button,
20
20
  } from "@checkstack/ui";
@@ -143,12 +143,12 @@ export const RunAsServiceAccountPicker: React.FC<
143
143
  }
144
144
  />
145
145
  ) : (
146
- <InfoBanner variant="info">
147
- <InfoBannerIcon>
146
+ <Alert variant="info">
147
+ <AlertIcon>
148
148
  <ShieldCheck className="h-4 w-4" />
149
- </InfoBannerIcon>
150
- <InfoBannerContent>
151
- <InfoBannerDescription>
149
+ </AlertIcon>
150
+ <AlertContent>
151
+ <AlertDescription>
152
152
  A service account is an application the automation runs as. It
153
153
  executes with that account's permissions (assigned via roles), so
154
154
  picking one is required.{" "}
@@ -161,9 +161,9 @@ export const RunAsServiceAccountPicker: React.FC<
161
161
  Learn more
162
162
  <ExternalLink className="h-3 w-3" />
163
163
  </a>
164
- </InfoBannerDescription>
165
- </InfoBannerContent>
166
- </InfoBanner>
164
+ </AlertDescription>
165
+ </AlertContent>
166
+ </Alert>
167
167
  )}
168
168
  </div>
169
169
  );
@@ -0,0 +1,75 @@
1
+ import React from "react";
2
+ import { AlertCircle } from "lucide-react";
3
+ import {
4
+ Button,
5
+ Popover,
6
+ PopoverContent,
7
+ PopoverTrigger,
8
+ } from "@checkstack/ui";
9
+ import type { BlockingIssue } from "../editor/blocking-issues";
10
+
11
+ export interface SaveBlockersSummaryProps {
12
+ /** Ordered list of everything blocking the Save button. */
13
+ blockers: BlockingIssue[];
14
+ /**
15
+ * Navigate to / focus the surface that owns a blocker. Wired by the page to
16
+ * focus the offending field or reveal the definition editor.
17
+ */
18
+ onResolve: (blocker: BlockingIssue) => void;
19
+ }
20
+
21
+ /**
22
+ * Compact "N issue(s) blocking save" affordance shown next to the Save button
23
+ * when the automation is invalid. Each row links to the offending field /
24
+ * section so a greyed-out Save is never a dead end.
25
+ *
26
+ * Renders nothing when there are no blockers, so the page can drop it next to
27
+ * Save unconditionally.
28
+ */
29
+ export const SaveBlockersSummary: React.FC<SaveBlockersSummaryProps> = ({
30
+ blockers,
31
+ onResolve,
32
+ }) => {
33
+ const [open, setOpen] = React.useState(false);
34
+ if (blockers.length === 0) return null;
35
+
36
+ const count = blockers.length;
37
+
38
+ return (
39
+ <Popover open={open} onOpenChange={setOpen}>
40
+ <PopoverTrigger asChild>
41
+ <Button
42
+ type="button"
43
+ variant="outline"
44
+ size="sm"
45
+ className="border-destructive/50 text-destructive hover:text-destructive"
46
+ >
47
+ <AlertCircle className="mr-1 h-4 w-4" />
48
+ {count} {count === 1 ? "issue" : "issues"} blocking
49
+ </Button>
50
+ </PopoverTrigger>
51
+ <PopoverContent align="end" className="w-80 p-0">
52
+ <div className="border-b px-3 py-2 text-xs font-medium text-muted-foreground">
53
+ Fix these to save
54
+ </div>
55
+ <ul className="max-h-64 overflow-y-auto py-1">
56
+ {blockers.map((blocker, index) => (
57
+ <li key={`${blocker.message}-${index}`}>
58
+ <button
59
+ type="button"
60
+ onClick={() => {
61
+ setOpen(false);
62
+ onResolve(blocker);
63
+ }}
64
+ className="flex w-full items-start gap-2 px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/50"
65
+ >
66
+ <AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-destructive" />
67
+ <span className="break-words">{blocker.message}</span>
68
+ </button>
69
+ </li>
70
+ ))}
71
+ </ul>
72
+ </PopoverContent>
73
+ </Popover>
74
+ );
75
+ };
@@ -152,6 +152,7 @@ export const ConditionEditor: React.FC<ConditionEditorProps> = ({
152
152
  value={typeof value === "string" ? value : ""}
153
153
  onChange={(next) => onChange(next)}
154
154
  placeholder="trigger.payload.severity == &quot;high&quot;"
155
+ mode="expression"
155
156
  completionProvider={completionProvider}
156
157
  />
157
158
  )}
@@ -246,6 +247,7 @@ const NumericStateBody: React.FC<{
246
247
  value={typeof ns.value === "string" ? ns.value : String(ns.value)}
247
248
  onChange={(next) => patch({ value: next })}
248
249
  placeholder="health.system.p95_latency_ms"
250
+ mode="expression"
249
251
  completionProvider={completionProvider}
250
252
  />
251
253
  </div>
@@ -83,7 +83,7 @@ export const RunReplayPicker: React.FC<RunReplayPickerProps> = ({
83
83
  <SelectContent>
84
84
  {runs.map((run) => (
85
85
  <SelectItem key={run.id} value={run.id}>
86
- {new Date(run.startedAt).toLocaleString()} — {run.status}
86
+ {new Date(run.startedAt).toLocaleString()} - {run.status}
87
87
  </SelectItem>
88
88
  ))}
89
89
  </SelectContent>
@@ -271,13 +271,14 @@ const TriggerCard: React.FC<{
271
271
  />
272
272
  </div>
273
273
  <div className="space-y-1">
274
- <Label className="text-xs">Filter template</Label>
274
+ <Label className="text-xs">Filter (expression)</Label>
275
275
  <TemplateValueInput
276
276
  value={value.filter ?? ""}
277
277
  onChange={(next) =>
278
278
  onChange({ ...value, filter: next || undefined })
279
279
  }
280
- placeholder="{{ trigger.payload.severity == &quot;high&quot; }}"
280
+ placeholder="trigger.payload.severity == &quot;high&quot;"
281
+ mode="expression"
281
282
  completionProvider={templateCompletion}
282
283
  disabled={disabled}
283
284
  />
@@ -464,6 +465,7 @@ const WindowInput: React.FC<{
464
465
  })
465
466
  }
466
467
  placeholder={`Leave blank to count ${defaultPartition}`}
468
+ mode="expression"
467
469
  completionProvider={completionProvider}
468
470
  disabled={disabled}
469
471
  />
@@ -280,10 +280,11 @@ export const RepeatActionBody: React.FC<{
280
280
  <div className="space-y-1">
281
281
  <Label className="text-xs">
282
282
  {kind === "for_each"
283
- ? "Iterable (renders to JSON array)"
284
- : "Condition template"}
283
+ ? "Iterable expression (renders to JSON array)"
284
+ : "Condition (expression)"}
285
285
  </Label>
286
286
  <TemplateValueInput
287
+ mode="expression"
287
288
  value={
288
289
  kind === "for_each"
289
290
  ? (value.repeat as { for_each: string }).for_each
@@ -386,7 +386,7 @@ export const WaitForTriggerActionBody: React.FC<{
386
386
  </div>
387
387
  <div className="grid gap-2 sm:grid-cols-2">
388
388
  <div className="space-y-1">
389
- <Label className="text-xs">Filter template</Label>
389
+ <Label className="text-xs">Filter (expression)</Label>
390
390
  <TemplateValueInput
391
391
  value={value.wait_for_trigger.filter ?? ""}
392
392
  onChange={(next) =>
@@ -398,7 +398,8 @@ export const WaitForTriggerActionBody: React.FC<{
398
398
  },
399
399
  })
400
400
  }
401
- placeholder="{{ trigger.payload.id == var.targetId }}"
401
+ placeholder="trigger.payload.id == var.targetId"
402
+ mode="expression"
402
403
  completionProvider={completionProvider}
403
404
  disabled={disabled}
404
405
  />
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tests for the save-blockers summary that explains why the automation Save
3
+ * button is disabled. The list must mirror the page's `canSave` predicate
4
+ * (name + run-as + definition validation) so the count and the disabled state
5
+ * never disagree.
6
+ */
7
+ import { describe, expect, it } from "bun:test";
8
+ import { summarizeBlockingIssues } from "./blocking-issues";
9
+
10
+ describe("summarizeBlockingIssues", () => {
11
+ it("returns no blockers when everything is valid", () => {
12
+ expect(
13
+ summarizeBlockingIssues({ definitionIssues: [] }),
14
+ ).toEqual([]);
15
+ });
16
+
17
+ it("includes the name error targeting the name field", () => {
18
+ const blockers = summarizeBlockingIssues({
19
+ nameError: "Name is required",
20
+ definitionIssues: [],
21
+ });
22
+ expect(blockers).toEqual([
23
+ { message: "Name is required", target: { kind: "field", field: "name" } },
24
+ ]);
25
+ });
26
+
27
+ it("includes the run-as error targeting the run-as field", () => {
28
+ const blockers = summarizeBlockingIssues({
29
+ runAsError: "A service account is required",
30
+ definitionIssues: [],
31
+ });
32
+ expect(blockers).toEqual([
33
+ {
34
+ message: "A service account is required",
35
+ target: { kind: "field", field: "runAs" },
36
+ },
37
+ ]);
38
+ });
39
+
40
+ it("formats a definition issue as `path: message`", () => {
41
+ const blockers = summarizeBlockingIssues({
42
+ definitionIssues: [
43
+ { path: ["triggers", 0, "kind"], message: "Required" },
44
+ ],
45
+ });
46
+ expect(blockers).toEqual([
47
+ { message: "triggers.0.kind: Required", target: { kind: "definition" } },
48
+ ]);
49
+ });
50
+
51
+ it("renders a top-level (pathless) definition issue as just its message", () => {
52
+ const blockers = summarizeBlockingIssues({
53
+ definitionIssues: [
54
+ { path: [], message: "An automation needs at least one trigger." },
55
+ ],
56
+ });
57
+ expect(blockers).toEqual([
58
+ {
59
+ message: "An automation needs at least one trigger.",
60
+ target: { kind: "definition" },
61
+ },
62
+ ]);
63
+ });
64
+
65
+ it("orders blockers name → run-as → definition issues", () => {
66
+ const blockers = summarizeBlockingIssues({
67
+ nameError: "Name is required",
68
+ runAsError: "A service account is required",
69
+ definitionIssues: [{ path: ["actions"], message: "bad" }],
70
+ });
71
+ expect(blockers.map((b) => b.target)).toEqual([
72
+ { kind: "field", field: "name" },
73
+ { kind: "field", field: "runAs" },
74
+ { kind: "definition" },
75
+ ]);
76
+ });
77
+ });
@@ -0,0 +1,68 @@
1
+ import type { DefinitionIssue } from "./editor-validation";
2
+
3
+ /**
4
+ * Where a blocking issue can be "fixed" from. Drives the click affordance in
5
+ * the save-blockers summary: metadata-field targets focus their input, while
6
+ * definition targets reveal the validated definition (visual editor).
7
+ */
8
+ export type BlockingIssueTarget =
9
+ | { kind: "field"; field: "name" | "runAs" }
10
+ | { kind: "definition" };
11
+
12
+ export interface BlockingIssue {
13
+ /** Human-readable, one-line description of what is incomplete. */
14
+ message: string;
15
+ /** Where the user should go to resolve it. */
16
+ target: BlockingIssueTarget;
17
+ }
18
+
19
+ /**
20
+ * Summarize everything currently blocking a save into a flat, ordered list of
21
+ * actionable items. Mirrors the `canSave` predicate in `AutomationEditPage`
22
+ * (name + run-as + definition validation), so the count shown next to the Save
23
+ * button always matches whether Save is enabled.
24
+ *
25
+ * Pure and DOM-free so it can be unit-tested in isolation; the page wires the
26
+ * resulting `target`s to focus/tab navigation.
27
+ */
28
+ export function summarizeBlockingIssues({
29
+ nameError,
30
+ runAsError,
31
+ definitionIssues,
32
+ }: {
33
+ nameError?: string;
34
+ runAsError?: string;
35
+ definitionIssues: DefinitionIssue[];
36
+ }): BlockingIssue[] {
37
+ const blockers: BlockingIssue[] = [];
38
+
39
+ if (nameError) {
40
+ blockers.push({ message: nameError, target: { kind: "field", field: "name" } });
41
+ }
42
+ if (runAsError) {
43
+ blockers.push({
44
+ message: runAsError,
45
+ target: { kind: "field", field: "runAs" },
46
+ });
47
+ }
48
+
49
+ for (const issue of definitionIssues) {
50
+ blockers.push({
51
+ message: formatDefinitionIssue(issue),
52
+ target: { kind: "definition" },
53
+ });
54
+ }
55
+
56
+ return blockers;
57
+ }
58
+
59
+ /**
60
+ * Render a definition validation issue as a single `path: message` line,
61
+ * matching how the visual editor surfaces unattributed issues. A pathless
62
+ * (top-level) issue shows just its message.
63
+ */
64
+ function formatDefinitionIssue(issue: DefinitionIssue): string {
65
+ return issue.path.length > 0
66
+ ? `${issue.path.join(".")}: ${issue.message}`
67
+ : issue.message;
68
+ }
@@ -79,7 +79,7 @@ export const CONDITION_KIND_META: Record<ConditionKind, ConditionKindMeta> = {
79
79
  expr: {
80
80
  kind: "expr",
81
81
  label: "expression",
82
- description: "Raw boolean expression — the escape hatch.",
82
+ description: "Raw boolean expression - the escape hatch.",
83
83
  icon: "Braces",
84
84
  group: "Advanced",
85
85
  },