@checkstack/automation-frontend 0.7.0 → 0.8.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 +173 -0
- package/package.json +16 -16
- package/src/components/RunAsServiceAccountPicker.tsx +12 -12
- package/src/components/SaveBlockersSummary.tsx +75 -0
- package/src/editor/RunReplayPicker.tsx +1 -1
- package/src/editor/blocking-issues.test.ts +77 -0
- package/src/editor/blocking-issues.ts +68 -0
- package/src/editor/condition-kind.ts +1 -1
- package/src/pages/AutomationEditPage.tsx +132 -32
- package/src/pages/AutomationListPage.tsx +241 -38
- package/src/pages/AutomationTemplatePickerPage.tsx +27 -31
- package/src/pages/RunDetailPage.tsx +89 -32
- package/src/pages/RunsPage.tsx +96 -76
- package/src/pages/run-duration.test.ts +36 -0
- package/src/pages/run-duration.ts +18 -0
- package/src/pages/run-status-pill.tsx +78 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,178 @@
|
|
|
1
1
|
# @checkstack/automation-frontend
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8cad340: feat: live run polling, optimistic automation toggle, and relative public-status freshness
|
|
8
|
+
|
|
9
|
+
Implements three loading/feedback UX findings from the read-only review.
|
|
10
|
+
|
|
11
|
+
- **Automation run detail goes live.** `RunDetailPage` now polls
|
|
12
|
+
`getRun` every 2s while the run is `running`/`waiting` and stops the
|
|
13
|
+
moment it reaches a terminal status, so a watched execution updates
|
|
14
|
+
its status badge and step timeline without a manual reload. A subtle
|
|
15
|
+
"Live" indicator shows in the header while polling.
|
|
16
|
+
- **Optimistic automation enable/disable.** The per-row toggle on
|
|
17
|
+
`AutomationListPage` now applies the documented optimistic pattern:
|
|
18
|
+
`onMutate` cancels in-flight refetches, snapshots, and flips the row
|
|
19
|
+
in the cache so the switch flips on click; `onError` rolls back from
|
|
20
|
+
the snapshot and surfaces an error toast; `onSettled` invalidates to
|
|
21
|
+
reconcile with server truth. The success toast is suppressed (the
|
|
22
|
+
switch flip is the feedback), per `optimistic-updates.md`.
|
|
23
|
+
- **Relative, visibly-live public-status freshness.** The public status
|
|
24
|
+
page renders "Updated x ago" as relative time (was a static absolute
|
|
25
|
+
timestamp) and ticks periodically so the wording stays honest. A small
|
|
26
|
+
refresh dot pulses on each successful 60s refetch (gated behind
|
|
27
|
+
`usePerformance().isLowPower`, falling back to a static dot on
|
|
28
|
+
low-power devices). The "auto-updates every minute" copy is unchanged.
|
|
29
|
+
|
|
30
|
+
BREAKING CHANGE: the automation enable/disable toggle no longer raises a
|
|
31
|
+
"<name> enabled/disabled" success toast; the optimistic switch flip is now
|
|
32
|
+
the sole success feedback (error toast retained on failure).
|
|
33
|
+
|
|
34
|
+
- 8cad340: Make data-dense tables mobile-friendly and align status colors with semantic tokens.
|
|
35
|
+
|
|
36
|
+
- 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.
|
|
37
|
+
- 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.
|
|
38
|
+
- 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.
|
|
39
|
+
|
|
40
|
+
- 8cad340: Explain why Save is disabled and guard against losing unsaved edits in the
|
|
41
|
+
automation and health-check editors.
|
|
42
|
+
|
|
43
|
+
- A greyed-out Save is no longer a dead end: both editors now render a
|
|
44
|
+
"N issue(s) blocking" affordance next to the Save button. Opening it
|
|
45
|
+
lists every blocker, and clicking one jumps to the offending field/section
|
|
46
|
+
(the automation Name / Run-as fields or the visual definition editor; the
|
|
47
|
+
health-check tree node that owns the issue). The existing validation logic is
|
|
48
|
+
unchanged - the blockers are just surfaced and made actionable.
|
|
49
|
+
- The first field of a fresh automation (Name) now auto-focuses so keyboard-first
|
|
50
|
+
users can type immediately.
|
|
51
|
+
- Both editors now use the shared `useUnsavedChanges` hook for unsaved-changes
|
|
52
|
+
protection: a native prompt on tab close / refresh plus an in-app
|
|
53
|
+
"Discard unsaved changes?" confirmation when navigating away mid-edit. The
|
|
54
|
+
health-check editor's previous hand-rolled `beforeunload` listener is migrated
|
|
55
|
+
to the shared hook; the automation editor gains dirty tracking and the same
|
|
56
|
+
guard.
|
|
57
|
+
|
|
58
|
+
### Patch Changes
|
|
59
|
+
|
|
60
|
+
- 8cad340: Design-system rework: a premium, consistent UI language across the platform.
|
|
61
|
+
|
|
62
|
+
Foundation (`@checkstack/ui` + the shared Tailwind preset):
|
|
63
|
+
|
|
64
|
+
- A token system wired into the shared preset so it generates app-wide: a
|
|
65
|
+
surface elevation ramp (`surface` / `surface-2` / `surface-inset`), the
|
|
66
|
+
aurora gradient stops, a colorblind-safe `status` triad, and `grid-line`.
|
|
67
|
+
- A density model (`comfortable` / `compact`) via `--d-*` vars + `DensityProvider`
|
|
68
|
+
/ `useDensity`, with a user-menu density toggle, plus the polished
|
|
69
|
+
skeleton / empty / error state set.
|
|
70
|
+
- Honest, token-driven chart primitives (`TimeSeriesChart`, `Sparkline`,
|
|
71
|
+
`RadialGauge` / aurora hero, `RequestWaterfall`, `UptimeRibbon`).
|
|
72
|
+
- A signature aurora moment per page: `PageHeader` paints its icon strokes with
|
|
73
|
+
the aurora gradient and adds a hairline; `Card` gains soft layered depth.
|
|
74
|
+
|
|
75
|
+
Shell + surfaces:
|
|
76
|
+
|
|
77
|
+
- The app shell adopts the elevation ramp (header `surface-2`, sidebar
|
|
78
|
+
`surface`, content on the ambient base).
|
|
79
|
+
- The system-health dashboard, health-check latency / single-run views, and the
|
|
80
|
+
SLO dashboard are reskinned onto the primitives (aurora confidence gauge,
|
|
81
|
+
honest p50/p95 latency, request waterfall, number-led status cards).
|
|
82
|
+
|
|
83
|
+
App-wide adoption + premium rework:
|
|
84
|
+
|
|
85
|
+
- Every plugin frontend adopts the tokens, status triad, density, and elevation.
|
|
86
|
+
- The highest-impact surfaces in each plugin are then redesigned to a premium
|
|
87
|
+
bar: real depth, number-led hierarchy, multi-encoded status (pill + dot +
|
|
88
|
+
accent stripe), and refined list/table density. Several plugins extract pure
|
|
89
|
+
tone/label/format logic into unit-tested modules.
|
|
90
|
+
|
|
91
|
+
Alerts:
|
|
92
|
+
|
|
93
|
+
- Every alert/callout is unified onto a single premium `Alert` (depth surface +
|
|
94
|
+
status-accent stripe + toned icon chip, variant-driven).
|
|
95
|
+
|
|
96
|
+
BREAKING CHANGE: the duplicate `InfoBanner` component (and its sub-components)
|
|
97
|
+
is removed; use `Alert` instead - it is a drop-in replacement with the same
|
|
98
|
+
variants and composable parts.
|
|
99
|
+
|
|
100
|
+
- 8cad340: fix: make data tables responsive on narrow viewports
|
|
101
|
+
|
|
102
|
+
The users, teams, and roles management tables (auth-frontend), the automation
|
|
103
|
+
run-history table (automation-frontend), and the integration provider
|
|
104
|
+
connections table (integration-frontend) previously overflowed horizontally on
|
|
105
|
+
phone-width (~375px) viewports. Each now uses the `ResponsiveTable` +
|
|
106
|
+
`MobileCardList` dual-layout primitive from `@checkstack/ui`: the existing table
|
|
107
|
+
renders unchanged on `sm` and up, with a stacked per-row card surfacing the key
|
|
108
|
+
fields and action buttons below `sm`. Shared per-row rendering (role checkboxes,
|
|
109
|
+
team/role/connection action buttons, connection status) was lifted into small
|
|
110
|
+
local components so both layouts stay in sync.
|
|
111
|
+
|
|
112
|
+
- 8cad340: Adopt the canonical `toastError` helper from `@checkstack/ui` for error toasts.
|
|
113
|
+
|
|
114
|
+
Error toasts that previously called `toast.error(extractErrorMessage(error, "Failed to X"))`
|
|
115
|
+
(or interpolated `Failed to X: ${extractErrorMessage(error)}` strings) now use
|
|
116
|
+
`toastError(toast, "Failed to X", error)`. This centralizes the
|
|
117
|
+
"Failed to <action>: <message>" voice and applies the shared 100-character
|
|
118
|
+
truncation. Error toasts that did not previously prefix the action now gain the
|
|
119
|
+
canonical prefix; success toasts and terse validation one-liners are unchanged.
|
|
120
|
+
|
|
121
|
+
- Updated dependencies [8cad340]
|
|
122
|
+
- Updated dependencies [8cad340]
|
|
123
|
+
- Updated dependencies [8cad340]
|
|
124
|
+
- Updated dependencies [8cad340]
|
|
125
|
+
- Updated dependencies [8cad340]
|
|
126
|
+
- Updated dependencies [8cad340]
|
|
127
|
+
- Updated dependencies [8cad340]
|
|
128
|
+
- Updated dependencies [8cad340]
|
|
129
|
+
- Updated dependencies [8cad340]
|
|
130
|
+
- Updated dependencies [8cad340]
|
|
131
|
+
- Updated dependencies [8cad340]
|
|
132
|
+
- Updated dependencies [8cad340]
|
|
133
|
+
- Updated dependencies [8cad340]
|
|
134
|
+
- Updated dependencies [8cad340]
|
|
135
|
+
- Updated dependencies [8cad340]
|
|
136
|
+
- Updated dependencies [8cad340]
|
|
137
|
+
- Updated dependencies [8cad340]
|
|
138
|
+
- Updated dependencies [8cad340]
|
|
139
|
+
- Updated dependencies [8cad340]
|
|
140
|
+
- Updated dependencies [8cad340]
|
|
141
|
+
- Updated dependencies [8cad340]
|
|
142
|
+
- Updated dependencies [8cad340]
|
|
143
|
+
- Updated dependencies [8cad340]
|
|
144
|
+
- Updated dependencies [8cad340]
|
|
145
|
+
- Updated dependencies [8cad340]
|
|
146
|
+
- Updated dependencies [8cad340]
|
|
147
|
+
- Updated dependencies [8cad340]
|
|
148
|
+
- @checkstack/auth-frontend@0.9.0
|
|
149
|
+
- @checkstack/ai-common@0.6.0
|
|
150
|
+
- @checkstack/ui@1.17.0
|
|
151
|
+
- @checkstack/gitops-frontend@0.6.0
|
|
152
|
+
- @checkstack/script-packages-frontend@0.4.0
|
|
153
|
+
- @checkstack/secrets-frontend@0.3.0
|
|
154
|
+
- @checkstack/common@0.17.0
|
|
155
|
+
- @checkstack/auth-common@0.11.0
|
|
156
|
+
- @checkstack/frontend-api@0.11.1
|
|
157
|
+
- @checkstack/catalog-common@2.4.2
|
|
158
|
+
- @checkstack/automation-common@0.7.1
|
|
159
|
+
- @checkstack/integration-common@0.9.2
|
|
160
|
+
- @checkstack/template-engine@0.4.6
|
|
161
|
+
- @checkstack/signal-frontend@0.2.6
|
|
162
|
+
|
|
163
|
+
## 0.7.1
|
|
164
|
+
|
|
165
|
+
### Patch Changes
|
|
166
|
+
|
|
167
|
+
- Updated dependencies [b1a5f3c]
|
|
168
|
+
- @checkstack/frontend-api@0.11.0
|
|
169
|
+
- @checkstack/auth-frontend@0.8.1
|
|
170
|
+
- @checkstack/catalog-common@2.4.1
|
|
171
|
+
- @checkstack/gitops-frontend@0.5.9
|
|
172
|
+
- @checkstack/script-packages-frontend@0.3.13
|
|
173
|
+
- @checkstack/secrets-frontend@0.2.8
|
|
174
|
+
- @checkstack/ui@1.16.2
|
|
175
|
+
|
|
3
176
|
## 0.7.0
|
|
4
177
|
|
|
5
178
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/automation-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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.
|
|
17
|
-
"@checkstack/auth-frontend": "0.
|
|
18
|
-
"@checkstack/automation-common": "0.7.
|
|
19
|
-
"@checkstack/ai-common": "0.
|
|
20
|
-
"@checkstack/catalog-common": "2.4.
|
|
21
|
-
"@checkstack/common": "0.
|
|
22
|
-
"@checkstack/frontend-api": "0.
|
|
23
|
-
"@checkstack/gitops-frontend": "0.
|
|
24
|
-
"@checkstack/integration-common": "0.9.
|
|
25
|
-
"@checkstack/script-packages-frontend": "0.
|
|
26
|
-
"@checkstack/secrets-frontend": "0.
|
|
27
|
-
"@checkstack/signal-frontend": "0.2.
|
|
28
|
-
"@checkstack/template-engine": "0.4.
|
|
29
|
-
"@checkstack/ui": "1.
|
|
16
|
+
"@checkstack/auth-common": "0.11.0",
|
|
17
|
+
"@checkstack/auth-frontend": "0.9.0",
|
|
18
|
+
"@checkstack/automation-common": "0.7.1",
|
|
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.0",
|
|
24
|
+
"@checkstack/integration-common": "0.9.2",
|
|
25
|
+
"@checkstack/script-packages-frontend": "0.4.0",
|
|
26
|
+
"@checkstack/secrets-frontend": "0.3.0",
|
|
27
|
+
"@checkstack/signal-frontend": "0.2.6",
|
|
28
|
+
"@checkstack/template-engine": "0.4.6",
|
|
29
|
+
"@checkstack/ui": "1.17.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.
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
<
|
|
147
|
-
<
|
|
146
|
+
<Alert variant="info">
|
|
147
|
+
<AlertIcon>
|
|
148
148
|
<ShieldCheck className="h-4 w-4" />
|
|
149
|
-
</
|
|
150
|
-
<
|
|
151
|
-
<
|
|
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
|
-
</
|
|
165
|
-
</
|
|
166
|
-
</
|
|
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
|
+
};
|
|
@@ -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()}
|
|
86
|
+
{new Date(run.startedAt).toLocaleString()} - {run.status}
|
|
87
87
|
</SelectItem>
|
|
88
88
|
))}
|
|
89
89
|
</SelectContent>
|
|
@@ -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
|
|
82
|
+
description: "Raw boolean expression - the escape hatch.",
|
|
83
83
|
icon: "Braces",
|
|
84
84
|
group: "Advanced",
|
|
85
85
|
},
|