@revenexx/studio-integrations 1.4.1 → 2.0.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 +162 -0
- package/README.md +1 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +65 -44
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +20 -6
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +0 -34
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +2 -52
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +0 -34
- package/dist/runtime/i18n/messages/de.json +5 -11
- package/dist/runtime/i18n/messages/en.json +5 -11
- package/dist/runtime/pages/integrations/credentials.vue +17 -5
- package/dist/runtime/pages/integrations/schedules.vue +11 -12
- package/dist/runtime/pages/integrations/workflows/[id].vue +0 -80
- package/dist/runtime/utils/cockpitSources.d.ts +68 -1
- package/dist/runtime/utils/credentialState.d.ts +15 -0
- package/dist/runtime/utils/credentialState.js +4 -0
- package/package.json +3 -3
- package/dist/runtime/utils/errorPathAdvice.d.ts +0 -60
- package/dist/runtime/utils/errorPathAdvice.js +0 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,167 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 8397d7b: Every studio list is read as a table, its search box states its own name, and the Runs
|
|
8
|
+
filter works again on `@revenexx/studio-shared` 0.3.2 (PO-233).
|
|
9
|
+
|
|
10
|
+
**A row of any studio list used to be an unnamed sequence of values.** The shared table
|
|
11
|
+
rendered anonymous boxes, so nothing connected a column heading to the cell beneath it:
|
|
12
|
+
a schedule read as _"Order sync, Daily at 03:00, Europe/Berlin, in 18m, 1h ago, live"_,
|
|
13
|
+
with nothing saying which of the two times was the next run. Taking `studio-shared`
|
|
14
|
+
0.3.2 gives every list — Workflows, Runs, Schedules, Credentials, Secrets and the Audit
|
|
15
|
+
Log — `table`, `row`, `columnheader` and `cell`, so each value is announced under the
|
|
16
|
+
heading it belongs to. The search above each list now states its name on the field
|
|
17
|
+
itself, in the words of its own placeholder, instead of leaving the browser to work one
|
|
18
|
+
out from that placeholder — which is a fallback rather than a name, and a documented
|
|
19
|
+
failure pattern when it is the only one.
|
|
20
|
+
|
|
21
|
+
**Two cells on the Schedules page lost the labels they carried.** They were there so a
|
|
22
|
+
test could find them, and a name on a cell is read _instead of_ its contents — so with
|
|
23
|
+
the semantics in place they announced "Next run" where the time should be. The heading
|
|
24
|
+
above the column says which time it is now, which is what it is for.
|
|
25
|
+
|
|
26
|
+
**The Runs filter is rebuilt as two selects.** In 0.3.2 the shared filter trigger opens
|
|
27
|
+
a popover rather than a dropdown menu, and the sub-menus this page put inside it had no
|
|
28
|
+
menu to attach to: they rendered nothing at all, so the control was there and the
|
|
29
|
+
narrowing was gone. Outcome and trigger kind are now chosen from a select each, `All`
|
|
30
|
+
included, and the narrowing reaches the platform exactly as before.
|
|
31
|
+
|
|
32
|
+
**A host also owes the shared table two new catalogue entries.** `studio-shared`
|
|
33
|
+
translates through the host's own `shared.*` catalogue and renders the key itself when
|
|
34
|
+
an entry is missing, and 0.3.2 asks for `shared.datatable.toolbar.filter.apply` and
|
|
35
|
+
`…toolbar.filter.reset` — the two buttons of the filter panel, which a host without
|
|
36
|
+
them draws as the key names. The cockpit already carries both, in either language; any
|
|
37
|
+
other host has to add them. (`…toolbar.search.aria.label` is asked for too, but no list
|
|
38
|
+
in this module reaches it: the table names the search box after its own placeholder
|
|
39
|
+
unless a surface says otherwise, and all six pass a placeholder.)
|
|
40
|
+
|
|
41
|
+
**This is a major for two of the reasons [`docs/versioning.md`](docs/versioning.md)
|
|
42
|
+
names.** The peer floor on `@revenexx/studio` rises from `*` to `^0.2.3`, so a host on
|
|
43
|
+
the old range can no longer install — and four accessible names are removed rather than
|
|
44
|
+
renamed: `Next run`, `Last run`, `When it fires` and `Cron expression`, which anything
|
|
45
|
+
addressing a schedules cell by name was using. Both are things a host has to react to.
|
|
46
|
+
|
|
47
|
+
**Hosts must be on `@revenexx/studio` ^0.2.3.** The peer range was `*` and is now the
|
|
48
|
+
version that carries the search field's `label` prop; below it the name falls through to
|
|
49
|
+
a wrapper and the box stays anonymous.
|
|
50
|
+
|
|
51
|
+
- c651aac: The editor no longer points out an error output nobody connected. The run says it instead
|
|
52
|
+
(PO-479).
|
|
53
|
+
|
|
54
|
+
**What goes.** A strip above the canvas read _"{n} node routes failures to an output with
|
|
55
|
+
nothing attached — nothing handles the failure there."_, named each step, and offered to
|
|
56
|
+
attach a **Stop and Error** step to the port in one click. The strip, that action, and the
|
|
57
|
+
five promises in `specs/workflow-advice.md` are withdrawn.
|
|
58
|
+
|
|
59
|
+
**Why, and what an author loses.** The strip and the platform behaviour it described
|
|
60
|
+
shipped together under PO-443. The platform half is the one that can speak with authority:
|
|
61
|
+
a failure routed to an output nothing leaves now **ends the run**, naming the step, what it
|
|
62
|
+
failed with, and which output had nowhere to send it — the `node-failure-handling` spec of
|
|
63
|
+
`revenexx/integrations`, AC-8. Before PO-443 such a failure vanished and the run was
|
|
64
|
+
recorded as a success, which is the world the strip was built for and which no longer
|
|
65
|
+
exists. What is genuinely lost is the timing: an author used to be told while drawing, and
|
|
66
|
+
is now told by the first run that actually fails.
|
|
67
|
+
|
|
68
|
+
**`revenexx:stop-and-error` is untouched.** It is still in the palette, still what all eight
|
|
69
|
+
shipped templates put behind each of their twenty-five error ports, and still the right
|
|
70
|
+
thing to attach. Only the editor's one-click attachment is gone.
|
|
71
|
+
|
|
72
|
+
**A major, for the reason [`docs/versioning.md`](docs/versioning.md) gives.** Seven strings
|
|
73
|
+
are removed rather than renamed, and three of them are accessible names or an action label
|
|
74
|
+
that a host or a test could be addressing:
|
|
75
|
+
|
|
76
|
+
| Removed | What it was |
|
|
77
|
+
| ------------------------------------------ | ------------------------------------------- |
|
|
78
|
+
| `integration.editor.errorPath.advice` | the strip's own sentence |
|
|
79
|
+
| `integration.editor.errorPath.action.stop` | _Stop the run on error_ |
|
|
80
|
+
| `integration.editor.errorPath.ariaReveal` | accessible name of the step link |
|
|
81
|
+
| `integration.editor.errorPath.ariaFix` | accessible name of the attach action |
|
|
82
|
+
| `integration.editor.errorPath.more` | _…and {n} more_ |
|
|
83
|
+
| `integration.editor.errorPath.message` | the pre-filled message on the attached step |
|
|
84
|
+
| `integration.editor.errorPath.toast.added` | the toast after attaching |
|
|
85
|
+
|
|
86
|
+
Nothing else in the editor moves: the validation banner, the reauth strip and the build
|
|
87
|
+
notice keep their place above the canvas, and the activation gate is unchanged — the strip
|
|
88
|
+
never touched it, which was the whole point of it.
|
|
89
|
+
|
|
90
|
+
### Patch Changes
|
|
91
|
+
|
|
92
|
+
- a79380d: The dialog that imports a platform SFTP account can be drawn by any host built to the
|
|
93
|
+
written contract, and its wait is now a promise with a test behind it (PO-387).
|
|
94
|
+
|
|
95
|
+
**The studio's written contract for a host was five members behind what this dialog
|
|
96
|
+
reads.** `src/runtime/utils/cockpitSources.ts` says what the module expects of a cockpit
|
|
97
|
+
host, and its `sftp` entry declared neither `loading` nor `activeTenantSlug`, made
|
|
98
|
+
`load`'s tenant argument required where the cockpit makes it optional, and left `server`
|
|
99
|
+
and `home_directory` off an account. The dialog read all five. The cockpit has provided
|
|
100
|
+
all five throughout, so **nothing was ever wrong for an operator** — what was wrong is
|
|
101
|
+
that a host built to the contract instead of to the cockpit threw
|
|
102
|
+
`TypeError: Cannot read properties of undefined` while drawing the dialog. Vue's error
|
|
103
|
+
handler turns that into a console message and unmounts the tree, so the page went blank
|
|
104
|
+
with no error anything could catch.
|
|
105
|
+
|
|
106
|
+
**The consequence was that no test could reach the surface at all.** The playground's
|
|
107
|
+
stand-in for the cockpit is built from the contract, so switching it on crashed the
|
|
108
|
+
dialog and leaving it off gave a dialog that never waits — the only two states the suite
|
|
109
|
+
could produce. The waiting announcement PO-251 added here was therefore recorded as a
|
|
110
|
+
gap rather than a criterion. The contract and the stand-in now carry all five members,
|
|
111
|
+
the dialog takes its type from the contract rather than declaring its own, and
|
|
112
|
+
`specs/node-configuration.md` carries the wait as AC-33 with a test that fails when the
|
|
113
|
+
announcement is taken away.
|
|
114
|
+
|
|
115
|
+
**A failed fetch no longer reads as a tenant with no accounts.** The surrounding
|
|
116
|
+
application catches its own fetch errors, so the studio is handed an empty list and no
|
|
117
|
+
exception — and the dialog drew the same "No active platform SFTP accounts. Create one
|
|
118
|
+
under Integration Studio → SFTP first." for both. One of those two asks the operator to
|
|
119
|
+
go and create an account; the other does not, and nothing on screen let them tell which
|
|
120
|
+
they were looking at. The contract gains the `error` the cockpit has always exposed —
|
|
121
|
+
**optional**, because the standalone dev studio's stand-ins provide the other four
|
|
122
|
+
members and not that one, and a dialog that insisted on it would lose that host the
|
|
123
|
+
whole surface. Where it is absent the dialog keeps the empty state it showed before.
|
|
124
|
+
That is AC-34, and it adds one string in both languages: **"The platform SFTP accounts could not be loaded:
|
|
125
|
+
{message}"** / "Die Plattform-SFTP-Konten konnten nicht geladen werden: {message}".
|
|
126
|
+
|
|
127
|
+
**A patch.** `CockpitSftpAccount` gains two required members, which
|
|
128
|
+
[`docs/versioning.md`](docs/versioning.md) would put in the major column as a narrowed
|
|
129
|
+
type — but only for somebody who can reach it. Nobody can: `exports` in `package.json`
|
|
130
|
+
declares `"."` alone and no `typesVersions`, so every other subpath is sealed and
|
|
131
|
+
`require.resolve` of the file answers `ERR_PACKAGE_PATH_NOT_EXPORTED`. The type ships
|
|
132
|
+
inside the tarball and is importable from nowhere. The dialog reads exactly what it read
|
|
133
|
+
before, so a host that worked goes on working unchanged.
|
|
134
|
+
|
|
135
|
+
- 8975a1f: The Credentials list offers the provider step to a credential that never took it, not
|
|
136
|
+
only to one whose authorisation expired (PO-478).
|
|
137
|
+
|
|
138
|
+
**A three-legged credential is created before its provider is asked.** The dialog closes
|
|
139
|
+
on the creation, and the window that asks for consent is opened after that — so when
|
|
140
|
+
that second step fails, there is no dialog left to say so. What remained was a
|
|
141
|
+
credential that cannot work, wearing the same _Not Connected_ badge as one nobody has
|
|
142
|
+
tested, with nothing in its row to finish what was started. The only way on was to
|
|
143
|
+
delete it and make it again.
|
|
144
|
+
|
|
145
|
+
**The row now offers the step wherever a three-legged credential is not known to be
|
|
146
|
+
connected**, and the wording says which of the two it is: **Connect** where nothing has
|
|
147
|
+
ever proved the credential connected, **Reconnect** where it was working and stopped —
|
|
148
|
+
an authorisation that expired, or a connection test that failed. The amber stays on the
|
|
149
|
+
expired one, which is the case this surface has always marked for attention. A
|
|
150
|
+
credential whose access data is a key, a host or a password never carries either, which
|
|
151
|
+
is what keeps the offer from becoming background noise.
|
|
152
|
+
|
|
153
|
+
It is generous by exactly one case: a credential that _was_ authorised but never tested
|
|
154
|
+
also carries the offer, because nothing the service returns tells that apart from one
|
|
155
|
+
that was never authorised. Taking the step again is the same window the operator was
|
|
156
|
+
expecting, so the cost of that guess is a click.
|
|
157
|
+
|
|
158
|
+
Promised behaviour: `specs/credentials.md` AC-2. The `## Gaps` entry in
|
|
159
|
+
`specs/credential-create.md` that asked what this surface says when the step after the
|
|
160
|
+
close fails is answered, and now points here.
|
|
161
|
+
|
|
162
|
+
- c787791: No product change: a spec's `## Gaps` entry and a test comment now say only what was
|
|
163
|
+
measured about `canvas-navigation` AC-4 (PO-479).
|
|
164
|
+
|
|
3
165
|
## 1.4.1
|
|
4
166
|
|
|
5
167
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ component styling silently breaks:
|
|
|
74
74
|
Adjust the relative paths to reach `node_modules` from your CSS file.
|
|
75
75
|
|
|
76
76
|
## Peers
|
|
77
|
-
`@revenexx/studio
|
|
77
|
+
`@revenexx/studio` ^0.2.3, `@revenexx/studio-shared` ^0.3.2 (module form), `nuxt` ^4, `vue` ^3.5,
|
|
78
78
|
`@vue-flow/*` + `@dagrejs/dagre` (workflow editor), `lucide-vue-next`; register
|
|
79
79
|
`@solar-icons/nuxt` in the host.
|
|
80
80
|
|
package/dist/module.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import { computed, onMounted, onUnmounted, ref } from "vue";
|
|
3
3
|
import {
|
|
4
4
|
Badge,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue
|
|
10
10
|
} from "@revenexx/studio";
|
|
11
11
|
import { DEFAULT_RUN_DIRECTION, DEFAULT_RUN_SORT } from "../../utils/runFeedQuery";
|
|
12
12
|
import { RUN_STATUS_COLUMN_WIDTH, RUN_STATUS_WORDS, normalisedRunStatus, runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
@@ -85,8 +85,9 @@ function setStatus(word) {
|
|
|
85
85
|
function setTrigger(kind) {
|
|
86
86
|
emit("narrow", { triggerType: kind ?? "" });
|
|
87
87
|
}
|
|
88
|
-
const
|
|
89
|
-
const
|
|
88
|
+
const ALL = "all";
|
|
89
|
+
const statusChoice = computed(() => chosenStatus.value[0] ?? ALL);
|
|
90
|
+
const triggerChoice = computed(() => props.triggerType || ALL);
|
|
90
91
|
const searchText = ref("");
|
|
91
92
|
function onTableState(state) {
|
|
92
93
|
searchText.value = state.search;
|
|
@@ -171,46 +172,66 @@ const columns = computed(() => [
|
|
|
171
172
|
(`filterTabs`) cannot be used here, because it compares the rows in hand and
|
|
172
173
|
those are one page.
|
|
173
174
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
175
|
+
It is no longer the same gesture as the other lists. Theirs tick values and
|
|
176
|
+
apply them together; this one picks one value per dimension and applies it on
|
|
177
|
+
the spot, because each pick is a request to the platform and staging them
|
|
178
|
+
would only delay the same round trip. So the shared narrowing helper does not
|
|
179
|
+
reach this page and `runs.spec.ts` carries its own — said in both places
|
|
180
|
+
rather than left to be discovered (`e2e/support/studio-table.ts`).
|
|
181
|
+
|
|
182
|
+
What is still NOT copied is the count beside each option: the built-in counts
|
|
183
|
+
the rows in hand, and counting a page here would put a number next to
|
|
184
|
+
"failed" that describes this page rather than the history the option narrows.
|
|
181
185
|
-->
|
|
182
186
|
<template v-if="serverMode" #filter-menu>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
187
|
+
<!--
|
|
188
|
+
Two selects rather than the sub-menus this slot used to hold. Since
|
|
189
|
+
`studio-shared` 0.3.2 the filter trigger opens a popover instead of a
|
|
190
|
+
dropdown menu, and a `DropdownMenuSub` outside a menu root has nothing to
|
|
191
|
+
attach to: it renders nothing at all, so the narrowing was there in the
|
|
192
|
+
markup and gone from the screen. A select is what the new surface is built
|
|
193
|
+
for — it sizes `[data-slot=select-trigger]` to its full width — and it
|
|
194
|
+
keeps what the menu promised, the widest setting included.
|
|
195
|
+
-->
|
|
196
|
+
<div class="space-y-1">
|
|
197
|
+
<span class="text-xs font-medium text-muted-foreground">{{ t("integration.runs.table.column.status") }}</span>
|
|
198
|
+
<Select
|
|
199
|
+
:model-value="statusChoice"
|
|
200
|
+
@update:model-value="(v) => setStatus(v === ALL ? null : String(v))"
|
|
201
|
+
>
|
|
202
|
+
<SelectTrigger :aria-label="t('integration.runs.table.column.status')" class="h-8 text-sm">
|
|
203
|
+
<SelectValue />
|
|
204
|
+
</SelectTrigger>
|
|
205
|
+
<SelectContent>
|
|
206
|
+
<SelectItem :value="ALL">
|
|
207
|
+
{{ t("integration.runs.table.filter.all") }}
|
|
208
|
+
</SelectItem>
|
|
209
|
+
<SelectItem v-for="word in RUN_STATUS_WORDS" :key="word" :value="word">
|
|
210
|
+
{{ runStatusLabel(word) }}
|
|
211
|
+
</SelectItem>
|
|
212
|
+
</SelectContent>
|
|
213
|
+
</Select>
|
|
214
|
+
</div>
|
|
198
215
|
|
|
199
|
-
<
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
</
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
216
|
+
<div class="space-y-1">
|
|
217
|
+
<span class="text-xs font-medium text-muted-foreground">{{ t("integration.runs.table.column.trigger") }}</span>
|
|
218
|
+
<Select
|
|
219
|
+
:model-value="triggerChoice"
|
|
220
|
+
@update:model-value="(v) => setTrigger(v === ALL ? null : String(v))"
|
|
221
|
+
>
|
|
222
|
+
<SelectTrigger :aria-label="t('integration.runs.table.column.trigger')" class="h-8 text-sm">
|
|
223
|
+
<SelectValue />
|
|
224
|
+
</SelectTrigger>
|
|
225
|
+
<SelectContent>
|
|
226
|
+
<SelectItem :value="ALL">
|
|
227
|
+
{{ t("integration.runs.table.filter.all") }}
|
|
228
|
+
</SelectItem>
|
|
229
|
+
<SelectItem v-for="kind in TRIGGER_TYPE_WORDS" :key="kind" :value="kind">
|
|
230
|
+
{{ triggerTypeLabel(kind) }}
|
|
231
|
+
</SelectItem>
|
|
232
|
+
</SelectContent>
|
|
233
|
+
</Select>
|
|
234
|
+
</div>
|
|
214
235
|
</template>
|
|
215
236
|
|
|
216
237
|
<template #cell-status="{ row: run }">
|
|
@@ -17,13 +17,24 @@ const sftp = typeof useConsoleSftp === "function" ? useConsoleSftp() : {
|
|
|
17
17
|
const { create } = useIntegrationsCredentials();
|
|
18
18
|
const busyId = ref(null);
|
|
19
19
|
const error = ref(null);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const loadPending = ref(false);
|
|
21
|
+
let openCycle = 0;
|
|
22
|
+
watch(open, async (o) => {
|
|
23
|
+
if (!o) return;
|
|
24
|
+
const cycle = ++openCycle;
|
|
25
|
+
error.value = null;
|
|
26
|
+
loadPending.value = true;
|
|
27
|
+
try {
|
|
28
|
+
await sftp.load();
|
|
29
|
+
} finally {
|
|
30
|
+
if (cycle === openCycle) loadPending.value = false;
|
|
24
31
|
}
|
|
25
32
|
});
|
|
26
33
|
const activeAccounts = computed(() => sftp.accounts.value.filter((a) => a.status === "active" && a.login_username));
|
|
34
|
+
const waiting = computed(() => (loadPending.value || sftp.loading.value) && !activeAccounts.value.length);
|
|
35
|
+
const loadFailed = computed(
|
|
36
|
+
() => !waiting.value && !activeAccounts.value.length && sftp.activeTenantSlug.value && sftp.error?.value ? t("integration.credentials.sftpImport.state.loadFailed", { message: sftp.error.value }) : null
|
|
37
|
+
);
|
|
27
38
|
async function importAccount(account) {
|
|
28
39
|
const tenant = sftp.activeTenantSlug.value;
|
|
29
40
|
if (!tenant || !account.login_username) return;
|
|
@@ -69,13 +80,16 @@ async function importAccount(account) {
|
|
|
69
80
|
limit written down there (PO-251). -->
|
|
70
81
|
<LoadingAnnouncement
|
|
71
82
|
:label="t('integration.credentials.sftpImport.title')"
|
|
72
|
-
:loading="
|
|
83
|
+
:loading="waiting"
|
|
73
84
|
:text="t('integration.credentials.sftpImport.state.loading')"
|
|
74
85
|
/>
|
|
75
86
|
|
|
76
|
-
<div v-if="
|
|
87
|
+
<div v-if="waiting" aria-hidden="true" class="space-y-2">
|
|
77
88
|
<div v-for="n in 3" :key="`sk-${n}`" class="h-9 rounded-md bg-black/[0.07] animate-pulse" />
|
|
78
89
|
</div>
|
|
90
|
+
<p v-else-if="loadFailed" class="py-4 text-center text-sm text-rose-600">
|
|
91
|
+
{{ loadFailed }}
|
|
92
|
+
</p>
|
|
79
93
|
<p v-else-if="!activeAccounts.length" class="py-4 text-center text-sm text-muted-foreground">
|
|
80
94
|
{{ t("integration.credentials.sftpImport.empty") }}
|
|
81
95
|
</p>
|
|
@@ -42,42 +42,8 @@ type __VLS_Props = {
|
|
|
42
42
|
* id that is on the canvas is in the blob by construction.
|
|
43
43
|
*/
|
|
44
44
|
declare function revealNode(id: string): void;
|
|
45
|
-
/**
|
|
46
|
-
* Point at a node without opening it — for a reader who has to look at the
|
|
47
|
-
* card's *surroundings* rather than at its settings.
|
|
48
|
-
*
|
|
49
|
-
* The deliberate difference from {@link revealNode} is the missing selection,
|
|
50
|
-
* and with it the missing settings lightbox. That is the whole reason there are
|
|
51
|
-
* two functions: the validation banner sends someone to a field at fault, and
|
|
52
|
-
* the dialog is where that field is. The error-path advice sends them to a port
|
|
53
|
-
* with nothing on it, and the question they have to answer — where should this
|
|
54
|
-
* failure go? — is answered by the graph around the card, which a dialog over
|
|
55
|
-
* the canvas covers up.
|
|
56
|
-
*
|
|
57
|
-
* Marked with the same pulse a newly created card gets, because the job is the
|
|
58
|
-
* same one: say which of thirty cards is the one meant.
|
|
59
|
-
*/
|
|
60
|
-
declare function showNodeOnCanvas(id: string): void;
|
|
61
|
-
/**
|
|
62
|
-
* Hang a Stop-and-Error step off a node's error output and wire it up
|
|
63
|
-
* (specs/workflow-advice.md AC-3).
|
|
64
|
-
*
|
|
65
|
-
* The advice strip can tell an author their error output leads nowhere; on its
|
|
66
|
-
* own that is a reprimand without a remedy, because the repair — drop a node,
|
|
67
|
-
* pick the right one, connect it, write an expression quoting the failing step's
|
|
68
|
-
* message — is four steps of tedium they have to already know. Every template we
|
|
69
|
-
* ship does exactly this, twenty-five times over, so the shape is settled and
|
|
70
|
-
* worth one click.
|
|
71
|
-
*
|
|
72
|
-
* Leaves the result unsaved and unopened, like every other insertion here: the
|
|
73
|
-
* new step is highlighted and the author decides whether to keep it. Deleting it
|
|
74
|
-
* is the undo, because this editor has no undo stack to hook into.
|
|
75
|
-
*/
|
|
76
|
-
declare function attachErrorStop(sourceNodeId: string, port: string): void;
|
|
77
45
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {
|
|
78
46
|
revealNode: typeof revealNode;
|
|
79
|
-
showNodeOnCanvas: typeof showNodeOnCanvas;
|
|
80
|
-
attachErrorStop: typeof attachErrorStop;
|
|
81
47
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
82
48
|
run: () => any;
|
|
83
49
|
toggleActive: () => any;
|
|
@@ -51,8 +51,7 @@ import { toast } from "vue-sonner";
|
|
|
51
51
|
import WorkflowEdgeView from "./WorkflowEdgeView.vue";
|
|
52
52
|
import { layoutWorkflowNodes } from "./workflowLayout";
|
|
53
53
|
import { defaultsFor } from "../../utils/configDefaults";
|
|
54
|
-
import {
|
|
55
|
-
import { TRIGGER_TEMPLATES, findManifest, newestExecutableVersion } from "../../data/integrationNodeCatalog";
|
|
54
|
+
import { TRIGGER_TEMPLATES, findManifest } from "../../data/integrationNodeCatalog";
|
|
56
55
|
import { isTerminalManifest, portsForNode } from "../../data/nodeInputData";
|
|
57
56
|
import { computeLoopBodies, iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
|
|
58
57
|
import { inputHandleId, outputHandleId, portFromHandleId } from "../../data/handleId";
|
|
@@ -865,17 +864,7 @@ function revealNode(id) {
|
|
|
865
864
|
selectedId.value = id;
|
|
866
865
|
}
|
|
867
866
|
}
|
|
868
|
-
|
|
869
|
-
const folded = collapsedGroupOf().get(id);
|
|
870
|
-
if (folded) patchGroup(folded.id, { collapsed: false });
|
|
871
|
-
if (blob.value.nodes.some((n) => n.id === id)) {
|
|
872
|
-
markBorn(id);
|
|
873
|
-
syncFromBlob();
|
|
874
|
-
}
|
|
875
|
-
if (paneReady.value) centerOnNode(id);
|
|
876
|
-
else pendingReveal.value = id;
|
|
877
|
-
}
|
|
878
|
-
defineExpose({ revealNode, showNodeOnCanvas, attachErrorStop });
|
|
867
|
+
defineExpose({ revealNode });
|
|
879
868
|
const selectedUpstreamOutputs = computed(() => {
|
|
880
869
|
const id = selectedId.value;
|
|
881
870
|
if (!id) return [];
|
|
@@ -1108,45 +1097,6 @@ function spliceIntoEdge(edgeId, payload) {
|
|
|
1108
1097
|
syncFromBlob();
|
|
1109
1098
|
push();
|
|
1110
1099
|
}
|
|
1111
|
-
const STOP_AND_ERROR_SLUG = "revenexx:stop-and-error";
|
|
1112
|
-
function attachErrorStop(sourceNodeId, port) {
|
|
1113
|
-
const source = blob.value.nodes.find((n) => n.id === sourceNodeId);
|
|
1114
|
-
const version = newestExecutableVersion(STOP_AND_ERROR_SLUG);
|
|
1115
|
-
const manifest = version ? findManifest(STOP_AND_ERROR_SLUG, version) : null;
|
|
1116
|
-
if (!source || !manifest) return;
|
|
1117
|
-
const at = nodePosition(sourceNodeId);
|
|
1118
|
-
const id = `n-${Date.now()}`;
|
|
1119
|
-
blob.value.nodes.push({
|
|
1120
|
-
id,
|
|
1121
|
-
nodeSlug: manifest.slug,
|
|
1122
|
-
nodeVersion: manifest.version,
|
|
1123
|
-
config: {
|
|
1124
|
-
...defaultsFor(manifest.config ?? []),
|
|
1125
|
-
// The template convention verbatim: name the step that failed, then quote
|
|
1126
|
-
// its own message, so the run record says what went wrong rather than only
|
|
1127
|
-
// that something did. `errorCode` keeps the node's own default.
|
|
1128
|
-
message: t("integration.editor.errorPath.message", {
|
|
1129
|
-
label: nodeDisplayLabel(source),
|
|
1130
|
-
nodeId: source.id,
|
|
1131
|
-
port
|
|
1132
|
-
})
|
|
1133
|
-
},
|
|
1134
|
-
// Below the source and on the grid, the way the templates place theirs.
|
|
1135
|
-
// `freeSpotNear` keeps two of them off each other.
|
|
1136
|
-
position: freeSpotNear({ x: at.x, y: at.y + 180 })
|
|
1137
|
-
});
|
|
1138
|
-
blob.value.edges.push({
|
|
1139
|
-
id: `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
1140
|
-
from: { nodeId: sourceNodeId, output: port },
|
|
1141
|
-
to: { nodeId: id, input: void 0 }
|
|
1142
|
-
});
|
|
1143
|
-
markBorn(id);
|
|
1144
|
-
syncFromBlob();
|
|
1145
|
-
if (paneReady.value) centerOnNode(id);
|
|
1146
|
-
else pendingReveal.value = id;
|
|
1147
|
-
push();
|
|
1148
|
-
toast.info(t("integration.editor.errorPath.toast.added", { label: nodeDisplayLabel(source) }));
|
|
1149
|
-
}
|
|
1150
1100
|
function nodeDims(id) {
|
|
1151
1101
|
const d = flow.findNode(id)?.dimensions;
|
|
1152
1102
|
return d && d.width > 0 ? d : { width: 220, height: 64 };
|
|
@@ -42,42 +42,8 @@ type __VLS_Props = {
|
|
|
42
42
|
* id that is on the canvas is in the blob by construction.
|
|
43
43
|
*/
|
|
44
44
|
declare function revealNode(id: string): void;
|
|
45
|
-
/**
|
|
46
|
-
* Point at a node without opening it — for a reader who has to look at the
|
|
47
|
-
* card's *surroundings* rather than at its settings.
|
|
48
|
-
*
|
|
49
|
-
* The deliberate difference from {@link revealNode} is the missing selection,
|
|
50
|
-
* and with it the missing settings lightbox. That is the whole reason there are
|
|
51
|
-
* two functions: the validation banner sends someone to a field at fault, and
|
|
52
|
-
* the dialog is where that field is. The error-path advice sends them to a port
|
|
53
|
-
* with nothing on it, and the question they have to answer — where should this
|
|
54
|
-
* failure go? — is answered by the graph around the card, which a dialog over
|
|
55
|
-
* the canvas covers up.
|
|
56
|
-
*
|
|
57
|
-
* Marked with the same pulse a newly created card gets, because the job is the
|
|
58
|
-
* same one: say which of thirty cards is the one meant.
|
|
59
|
-
*/
|
|
60
|
-
declare function showNodeOnCanvas(id: string): void;
|
|
61
|
-
/**
|
|
62
|
-
* Hang a Stop-and-Error step off a node's error output and wire it up
|
|
63
|
-
* (specs/workflow-advice.md AC-3).
|
|
64
|
-
*
|
|
65
|
-
* The advice strip can tell an author their error output leads nowhere; on its
|
|
66
|
-
* own that is a reprimand without a remedy, because the repair — drop a node,
|
|
67
|
-
* pick the right one, connect it, write an expression quoting the failing step's
|
|
68
|
-
* message — is four steps of tedium they have to already know. Every template we
|
|
69
|
-
* ship does exactly this, twenty-five times over, so the shape is settled and
|
|
70
|
-
* worth one click.
|
|
71
|
-
*
|
|
72
|
-
* Leaves the result unsaved and unopened, like every other insertion here: the
|
|
73
|
-
* new step is highlighted and the author decides whether to keep it. Deleting it
|
|
74
|
-
* is the undo, because this editor has no undo stack to hook into.
|
|
75
|
-
*/
|
|
76
|
-
declare function attachErrorStop(sourceNodeId: string, port: string): void;
|
|
77
45
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {
|
|
78
46
|
revealNode: typeof revealNode;
|
|
79
|
-
showNodeOnCanvas: typeof showNodeOnCanvas;
|
|
80
|
-
attachErrorStop: typeof attachErrorStop;
|
|
81
47
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
82
48
|
run: () => any;
|
|
83
49
|
toggleActive: () => any;
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"integration.audit.table.column.actor": "Akteur",
|
|
29
29
|
"integration.audit.table.column.resource": "Ressource",
|
|
30
30
|
"integration.audit.table.column.when": "Wann",
|
|
31
|
-
"integration.audit.toast.copyFailed": "Kopieren fehlgeschlagen",
|
|
32
31
|
"integration.audit.toast.copied": "Kopiert",
|
|
32
|
+
"integration.audit.toast.copyFailed": "Kopieren fehlgeschlagen",
|
|
33
33
|
"integration.canvas.badge.trigger": "Trigger",
|
|
34
34
|
"integration.canvas.color.amber": "Bernstein",
|
|
35
35
|
"integration.canvas.color.blue": "Blau",
|
|
@@ -170,6 +170,8 @@
|
|
|
170
170
|
"integration.credentials.error.oauthConnect": "OAuth-Verbindung konnte nicht gestartet werden.",
|
|
171
171
|
"integration.credentials.error.save": "Zugangsdaten konnten nicht gespeichert werden.",
|
|
172
172
|
"integration.credentials.error.testFailed": "Verbindungstest fehlgeschlagen.",
|
|
173
|
+
"integration.credentials.list.action.connect": "Verbinden",
|
|
174
|
+
"integration.credentials.list.action.connect.aria": "{name} verbinden",
|
|
173
175
|
"integration.credentials.list.action.delete.aria": "{name} löschen",
|
|
174
176
|
"integration.credentials.list.action.new": "Zugangsdaten anlegen",
|
|
175
177
|
"integration.credentials.list.action.reconnect": "Erneut verbinden",
|
|
@@ -244,6 +246,7 @@
|
|
|
244
246
|
"integration.credentials.sftpImport.empty": "Keine aktiven Plattform-SFTP-Konten. Zuerst eines unter Integration Studio → SFTP anlegen.",
|
|
245
247
|
"integration.credentials.sftpImport.error.importFailed": "Import fehlgeschlagen.",
|
|
246
248
|
"integration.credentials.sftpImport.error.readPassword": "Das Kontopasswort konnte nicht gelesen werden.",
|
|
249
|
+
"integration.credentials.sftpImport.state.loadFailed": "Die Plattform-SFTP-Konten konnten nicht geladen werden: {message}",
|
|
247
250
|
"integration.credentials.sftpImport.state.loading": "SFTP-Konten werden geladen…",
|
|
248
251
|
"integration.credentials.sftpImport.title": "Plattform-SFTP-Konto importieren",
|
|
249
252
|
"integration.credentials.shared.action.testConnection": "Verbindung testen",
|
|
@@ -377,13 +380,6 @@
|
|
|
377
380
|
"integration.editor.dropGhost.label": "Loslassen zum Hinzufügen",
|
|
378
381
|
"integration.editor.emptyState.hint": "Ziehen Sie einen Trigger und eine Aktion aus dem Panel links.",
|
|
379
382
|
"integration.editor.emptyState.title": "Leere Arbeitsfläche",
|
|
380
|
-
"integration.editor.errorPath.action.stop": "Fehlerausgang stoppen lassen",
|
|
381
|
-
"integration.editor.errorPath.advice": "{n} Node leitet Fehler auf einen Ausgang, an dem nichts hängt — der Fehler wird dort nicht behandelt. | {n} Nodes leiten Fehler auf einen Ausgang, an dem nichts hängt — der Fehler wird dort nicht behandelt.",
|
|
382
|
-
"integration.editor.errorPath.ariaFix": "Ausgang „{port}“ von {label} auf Stop und Fehler legen",
|
|
383
|
-
"integration.editor.errorPath.ariaReveal": "{label} auf der Canvas zeigen — der Ausgang „{port}“ ist nicht verbunden",
|
|
384
|
-
"integration.editor.errorPath.message": "{label} fehlgeschlagen: ${{ nodes.{nodeId}.outputs.{port}.message }}",
|
|
385
|
-
"integration.editor.errorPath.more": "…und {n} weitere",
|
|
386
|
-
"integration.editor.errorPath.toast.added": "Stop und Fehler an {label} gehängt — noch nicht gespeichert.",
|
|
387
383
|
"integration.editor.event.toast.noneReceived": "Noch kein {topic}-Event empfangen.",
|
|
388
384
|
"integration.editor.executionMode.async.hint": "Runs werden immer in die Warteschlange gestellt; der Trigger antwortet sofort.",
|
|
389
385
|
"integration.editor.executionMode.async.label": "Nur asynchron",
|
|
@@ -983,10 +979,8 @@
|
|
|
983
979
|
"integration.runs.toast.action.requested": "{action} angefordert",
|
|
984
980
|
"integration.runs.toast.action.started": "Run #{id} gestartet",
|
|
985
981
|
"integration.runs.toast.terminate.confirm": "Run #{id} terminieren?",
|
|
986
|
-
"integration.schedules.cell.cronExpression.aria": "Cron-Ausdruck",
|
|
987
982
|
"integration.schedules.cell.customSchedule": "Individueller Zeitplan",
|
|
988
983
|
"integration.schedules.cell.noSchedule": "Kein Zeitplan festgelegt",
|
|
989
|
-
"integration.schedules.cell.whenItFires.aria": "Wann er ausgelöst wird",
|
|
990
984
|
"integration.schedules.cron.dailyAt": "Täglich um {time}",
|
|
991
985
|
"integration.schedules.cron.day.daily": "Täglich",
|
|
992
986
|
"integration.schedules.cron.day.weekdays": "Wochentags",
|
|
@@ -1229,6 +1223,7 @@
|
|
|
1229
1223
|
"integration.workflows.state.action.remove.hint": "Das Entfernen nimmt diesem Workflow den Zugriff. Die gespeicherten Einträge bleiben bestehen.",
|
|
1230
1224
|
"integration.workflows.state.dialog.description": "Was sich dieser Workflow zwischen Läufen merken darf. Er erreicht ausschließlich die hier deklarierten Namespaces — alles andere wird beim Lauf abgewiesen.",
|
|
1231
1225
|
"integration.workflows.state.dialog.title": "Zustands-Namespaces",
|
|
1226
|
+
"integration.workflows.state.empty": "Dieser Workflow merkt sich noch nichts zwischen Läufen.",
|
|
1232
1227
|
"integration.workflows.state.error.invalidName": "Kleinbuchstaben, Ziffern und . - _, beginnend mit einem Buchstaben — zum Beispiel article oder crm.customers.",
|
|
1233
1228
|
"integration.workflows.state.error.nameExists": "Dieser Workflow deklariert diesen Namespace bereits.",
|
|
1234
1229
|
"integration.workflows.state.field.newName.label": "Namespace deklarieren",
|
|
@@ -1246,7 +1241,6 @@
|
|
|
1246
1241
|
"integration.workflows.state.role.mapping.hint": "Verknüpft je eine ID auf beiden Seiten, damit ein späterer Lauf weiß, ob er anlegen oder aktualisieren muss.",
|
|
1247
1242
|
"integration.workflows.state.role.mapping.label": "Zuordnung",
|
|
1248
1243
|
"integration.workflows.state.row.aria": "Zustands-Namespace {namespace}",
|
|
1249
|
-
"integration.workflows.state.empty": "Dieser Workflow merkt sich noch nichts zwischen Läufen.",
|
|
1250
1244
|
"integration.workflows.state.visibility.private.hint": "Nur dieser Workflow liest und schreibt ihn, und er wird mit dem Workflow gelöscht.",
|
|
1251
1245
|
"integration.workflows.state.visibility.private.label": "Diesen Workflow",
|
|
1252
1246
|
"integration.workflows.state.visibility.shared.hint": "Jeder Workflow dieses Mandanten liest und schreibt diese Einträge, und der Namespace bleibt bestehen, wenn dieser Workflow gelöscht wird.",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"integration.audit.table.column.actor": "Actor",
|
|
29
29
|
"integration.audit.table.column.resource": "Resource",
|
|
30
30
|
"integration.audit.table.column.when": "When",
|
|
31
|
-
"integration.audit.toast.copyFailed": "Could not copy",
|
|
32
31
|
"integration.audit.toast.copied": "Copied",
|
|
32
|
+
"integration.audit.toast.copyFailed": "Could not copy",
|
|
33
33
|
"integration.canvas.badge.trigger": "Trigger",
|
|
34
34
|
"integration.canvas.color.amber": "Amber",
|
|
35
35
|
"integration.canvas.color.blue": "Blue",
|
|
@@ -170,6 +170,8 @@
|
|
|
170
170
|
"integration.credentials.error.oauthConnect": "Could not start OAuth connect.",
|
|
171
171
|
"integration.credentials.error.save": "Could not save credential.",
|
|
172
172
|
"integration.credentials.error.testFailed": "Connection test failed.",
|
|
173
|
+
"integration.credentials.list.action.connect": "Connect",
|
|
174
|
+
"integration.credentials.list.action.connect.aria": "Connect {name}",
|
|
173
175
|
"integration.credentials.list.action.delete.aria": "Delete {name}",
|
|
174
176
|
"integration.credentials.list.action.new": "New credential",
|
|
175
177
|
"integration.credentials.list.action.reconnect": "Reconnect",
|
|
@@ -244,6 +246,7 @@
|
|
|
244
246
|
"integration.credentials.sftpImport.empty": "No active platform SFTP accounts. Create one under Integration Studio → SFTP first.",
|
|
245
247
|
"integration.credentials.sftpImport.error.importFailed": "Import failed.",
|
|
246
248
|
"integration.credentials.sftpImport.error.readPassword": "Could not read the account password.",
|
|
249
|
+
"integration.credentials.sftpImport.state.loadFailed": "The platform SFTP accounts could not be loaded: {message}",
|
|
247
250
|
"integration.credentials.sftpImport.state.loading": "Loading the SFTP accounts…",
|
|
248
251
|
"integration.credentials.sftpImport.title": "Import platform SFTP account",
|
|
249
252
|
"integration.credentials.shared.action.testConnection": "Connect",
|
|
@@ -377,13 +380,6 @@
|
|
|
377
380
|
"integration.editor.dropGhost.label": "Drop to add",
|
|
378
381
|
"integration.editor.emptyState.hint": "Drag a trigger and an action from the panel on the left.",
|
|
379
382
|
"integration.editor.emptyState.title": "Empty canvas",
|
|
380
|
-
"integration.editor.errorPath.action.stop": "Stop the run on error",
|
|
381
|
-
"integration.editor.errorPath.advice": "{n} node routes failures to an output with nothing attached — nothing handles the failure there. | {n} nodes route failures to an output with nothing attached — nothing handles the failure there.",
|
|
382
|
-
"integration.editor.errorPath.ariaFix": "Route {label}'s \"{port}\" output to Stop and Error",
|
|
383
|
-
"integration.editor.errorPath.ariaReveal": "Show {label} on the canvas — its \"{port}\" output is not connected",
|
|
384
|
-
"integration.editor.errorPath.message": "{label} failed: ${{ nodes.{nodeId}.outputs.{port}.message }}",
|
|
385
|
-
"integration.editor.errorPath.more": "…and {n} more",
|
|
386
|
-
"integration.editor.errorPath.toast.added": "Stop and Error attached to {label} — not saved yet.",
|
|
387
383
|
"integration.editor.event.toast.noneReceived": "No {topic} event received yet.",
|
|
388
384
|
"integration.editor.executionMode.async.hint": "Runs are always queued; the trigger returns immediately.",
|
|
389
385
|
"integration.editor.executionMode.async.label": "Async only",
|
|
@@ -983,10 +979,8 @@
|
|
|
983
979
|
"integration.runs.toast.action.requested": "{action} requested",
|
|
984
980
|
"integration.runs.toast.action.started": "Started run #{id}",
|
|
985
981
|
"integration.runs.toast.terminate.confirm": "Terminate run #{id}?",
|
|
986
|
-
"integration.schedules.cell.cronExpression.aria": "Cron expression",
|
|
987
982
|
"integration.schedules.cell.customSchedule": "Custom schedule",
|
|
988
983
|
"integration.schedules.cell.noSchedule": "No schedule set",
|
|
989
|
-
"integration.schedules.cell.whenItFires.aria": "When it fires",
|
|
990
984
|
"integration.schedules.cron.dailyAt": "Daily at {time}",
|
|
991
985
|
"integration.schedules.cron.day.daily": "Daily",
|
|
992
986
|
"integration.schedules.cron.day.weekdays": "Weekdays",
|
|
@@ -1229,6 +1223,7 @@
|
|
|
1229
1223
|
"integration.workflows.state.action.remove.hint": "Removing the declaration takes this workflow's access away. The stored entries stay where they are.",
|
|
1230
1224
|
"integration.workflows.state.dialog.description": "What this workflow may remember between runs. It can only reach the namespaces declared here — anything else is refused when the workflow runs.",
|
|
1231
1225
|
"integration.workflows.state.dialog.title": "State namespaces",
|
|
1226
|
+
"integration.workflows.state.empty": "This workflow remembers nothing between runs yet.",
|
|
1232
1227
|
"integration.workflows.state.error.invalidName": "Use lowercase letters, digits and . - _, starting with a letter — for example article or crm.customers.",
|
|
1233
1228
|
"integration.workflows.state.error.nameExists": "This workflow already declares that namespace.",
|
|
1234
1229
|
"integration.workflows.state.field.newName.label": "Declare a namespace",
|
|
@@ -1246,7 +1241,6 @@
|
|
|
1246
1241
|
"integration.workflows.state.role.mapping.hint": "Correlates an id on each side, so a later run knows whether to create or update.",
|
|
1247
1242
|
"integration.workflows.state.role.mapping.label": "Mapping",
|
|
1248
1243
|
"integration.workflows.state.row.aria": "State namespace {namespace}",
|
|
1249
|
-
"integration.workflows.state.empty": "This workflow remembers nothing between runs yet.",
|
|
1250
1244
|
"integration.workflows.state.visibility.private.hint": "Only this workflow reads and writes it, and it is deleted along with the workflow.",
|
|
1251
1245
|
"integration.workflows.state.visibility.private.label": "This workflow",
|
|
1252
1246
|
"integration.workflows.state.visibility.shared.hint": "Every workflow in this tenant reads and writes these entries, and the namespace stays when this workflow is deleted.",
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
} from "@revenexx/studio";
|
|
33
33
|
import { toast } from "../../utils/notify";
|
|
34
34
|
import { apiRefusal, errorReason } from "../../utils/apiError";
|
|
35
|
-
import { CREDENTIAL_STATES, credentialState, credentialStateLabel } from "../../utils/credentialState";
|
|
35
|
+
import { CREDENTIAL_STATES, credentialState, credentialStateLabel, providerStepWord } from "../../utils/credentialState";
|
|
36
36
|
import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
|
|
37
37
|
import { resolveLocalized } from "../../utils/localize";
|
|
38
38
|
const t = useIntegrationsT();
|
|
@@ -51,6 +51,17 @@ function typeName(slug) {
|
|
|
51
51
|
function isOAuthAuthcode(t2) {
|
|
52
52
|
return t2?.auth_kind === "oauth2-authcode";
|
|
53
53
|
}
|
|
54
|
+
function providerStep(c) {
|
|
55
|
+
if (!isOAuthAuthcode(typeFor(c.credential_type_slug))) return null;
|
|
56
|
+
const state = credentialState(c).key;
|
|
57
|
+
const word = providerStepWord(state);
|
|
58
|
+
if (!word) return null;
|
|
59
|
+
return {
|
|
60
|
+
label: t(`integration.credentials.list.action.${word}`),
|
|
61
|
+
aria: t(`integration.credentials.list.action.${word}.aria`, { name: c.name }),
|
|
62
|
+
expired: state === "needs_reauth"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
54
65
|
function formatDateTime(iso) {
|
|
55
66
|
if (!iso) return "\u2014";
|
|
56
67
|
try {
|
|
@@ -306,15 +317,16 @@ async function confirmDelete() {
|
|
|
306
317
|
<template #cell-actions="{ row: c }">
|
|
307
318
|
<div class="flex items-center justify-end gap-2 text-sm" @click.stop>
|
|
308
319
|
<Button
|
|
309
|
-
v-if="
|
|
320
|
+
v-if="providerStep(c)"
|
|
310
321
|
variant="outline"
|
|
311
322
|
size="sm"
|
|
312
|
-
class="h-7
|
|
323
|
+
class="h-7"
|
|
324
|
+
:class="providerStep(c).expired ? 'text-amber-600' : ''"
|
|
313
325
|
:disabled="connecting === c.id"
|
|
314
|
-
:aria-label="
|
|
326
|
+
:aria-label="providerStep(c).aria"
|
|
315
327
|
@click="connect(c)"
|
|
316
328
|
>
|
|
317
|
-
{{
|
|
329
|
+
{{ providerStep(c).label }}
|
|
318
330
|
</Button>
|
|
319
331
|
<button type="button" class="flex size-7 items-center justify-center rounded text-foreground/40 hover:text-rose-600 hover:bg-rose-500/10" :title="t('integration.credentials.list.action.delete.aria', { name: c.name })" :aria-label="t('integration.credentials.list.action.delete.aria', { name: c.name })" @click="pendingDelete = c">
|
|
320
332
|
<TrashBin :size="13" />
|
|
@@ -147,16 +147,16 @@ const columns = computed(() => [
|
|
|
147
147
|
at several named times is a long sentence, and half of one is
|
|
148
148
|
the wrong half. Its title is the expression, so there is no
|
|
149
149
|
tooltip left to carry the rest of the words. -->
|
|
150
|
-
<div v-if="describeCron(e.cron)" :
|
|
150
|
+
<div v-if="describeCron(e.cron)" :title="e.cron ?? void 0" class="text-sm leading-snug">
|
|
151
151
|
{{ describeCron(e.cron) }}
|
|
152
152
|
</div>
|
|
153
153
|
<template v-else-if="e.cron">
|
|
154
|
-
<div
|
|
154
|
+
<div class="text-sm truncate">
|
|
155
155
|
{{ t("integration.schedules.cell.customSchedule") }}
|
|
156
156
|
</div>
|
|
157
|
-
<code
|
|
157
|
+
<code class="block font-mono text-xs text-muted-foreground truncate">{{ e.cron }}</code>
|
|
158
158
|
</template>
|
|
159
|
-
<div v-else
|
|
159
|
+
<div v-else class="text-sm text-muted-foreground truncate">
|
|
160
160
|
{{ t("integration.schedules.cell.noSchedule") }}
|
|
161
161
|
</div>
|
|
162
162
|
</div>
|
|
@@ -170,12 +170,13 @@ const columns = computed(() => [
|
|
|
170
170
|
</template>
|
|
171
171
|
|
|
172
172
|
<!--
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
Neither time cell carries a name of its own, and that is the fix rather
|
|
174
|
+
than an omission. Since studio-shared 0.3.2 the cell is a `role="cell"`
|
|
175
|
+
under a `columnheader`, so a screen reader reads "Next run, in 5h" from
|
|
176
|
+
the table itself. A name written here would be read *instead of* the
|
|
177
|
+
value — the row announced "Next run" where the time should be — so the
|
|
178
|
+
labels these cells used to carry to be addressable by a test were
|
|
179
|
+
removed with the semantics that replaced them (PO-233).
|
|
179
180
|
-->
|
|
180
181
|
<!--
|
|
181
182
|
The instant the platform holds, shown as a distance from now, with the
|
|
@@ -187,7 +188,6 @@ const columns = computed(() => [
|
|
|
187
188
|
-->
|
|
188
189
|
<template #cell-nextRun="{ row: e }">
|
|
189
190
|
<span
|
|
190
|
-
:aria-label="t('integration.schedules.table.column.nextRun')"
|
|
191
191
|
:title="e.nextRunLocal ?? void 0"
|
|
192
192
|
class="text-sm tabular-nums"
|
|
193
193
|
>{{ formatRelative(e.nextRunAt, e.timezone) }}</span>
|
|
@@ -202,7 +202,6 @@ const columns = computed(() => [
|
|
|
202
202
|
-->
|
|
203
203
|
<template #cell-lastRun="{ row: e }">
|
|
204
204
|
<span
|
|
205
|
-
:aria-label="t('integration.schedules.table.column.lastRun')"
|
|
206
205
|
:title="e.lastRunAt ?? void 0"
|
|
207
206
|
class="text-sm tabular-nums text-muted-foreground"
|
|
208
207
|
>{{ lastRunText(e) }}</span>
|
|
@@ -46,8 +46,6 @@ import { BUILD_SETTLE_POLL_MS, BUILD_SETTLE_TIMEOUT_MS, createBuildSettle, isBui
|
|
|
46
46
|
import { splitRunOutputs } from "../../../utils/runOutputs";
|
|
47
47
|
import { isTerminalRun, unwrapRun } from "../../../utils/runEnvelope";
|
|
48
48
|
import { findingsSummary, groupFindingsByNode, nodeDisplayLabel } from "../../../utils/validationFindings";
|
|
49
|
-
import { unwiredErrorPorts } from "../../../utils/errorPathAdvice";
|
|
50
|
-
import { newestExecutableVersion } from "../../../data/integrationNodeCatalog";
|
|
51
49
|
import { payloadFieldsFromBlob } from "../../../data/payloadForm";
|
|
52
50
|
import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
|
|
53
51
|
const t = useIntegrationsT();
|
|
@@ -110,27 +108,6 @@ async function revealNode(nodeId) {
|
|
|
110
108
|
await nextTick();
|
|
111
109
|
editorRef.value?.revealNode(nodeId);
|
|
112
110
|
}
|
|
113
|
-
const errorPathAdvice = computed(() => {
|
|
114
|
-
const blob = shownBlob.value;
|
|
115
|
-
if (!blob) return [];
|
|
116
|
-
const byId = new Map(blob.nodes.map((n) => [n.id, n]));
|
|
117
|
-
return unwiredErrorPorts(blob).flatMap((a) => {
|
|
118
|
-
const node = byId.get(a.nodeId);
|
|
119
|
-
return node ? [{ ...a, label: nodeDisplayLabel(node) }] : [];
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
const adviceExpanded = ref(false);
|
|
123
|
-
const canAttachErrorStop = computed(() => newestExecutableVersion("revenexx:stop-and-error") !== null);
|
|
124
|
-
async function showNodeOnCanvas(nodeId) {
|
|
125
|
-
tab.value = "editor";
|
|
126
|
-
await nextTick();
|
|
127
|
-
editorRef.value?.showNodeOnCanvas(nodeId);
|
|
128
|
-
}
|
|
129
|
-
async function attachErrorStop(nodeId, port) {
|
|
130
|
-
tab.value = "editor";
|
|
131
|
-
await nextTick();
|
|
132
|
-
editorRef.value?.attachErrorStop(nodeId, port);
|
|
133
|
-
}
|
|
134
111
|
const { load: loadNodeCatalog } = useIntegrationsNodes();
|
|
135
112
|
onMounted(() => {
|
|
136
113
|
void loadNodeCatalog();
|
|
@@ -200,7 +177,6 @@ watch(id, async () => {
|
|
|
200
177
|
if (!w) await leaveTo("/integrations/workflows");
|
|
201
178
|
draft.value = null;
|
|
202
179
|
warningsExpanded.value = false;
|
|
203
|
-
adviceExpanded.value = false;
|
|
204
180
|
if (tab.value === "runs") void loadRuns(1);
|
|
205
181
|
settleBuildStatus();
|
|
206
182
|
}, { immediate: true });
|
|
@@ -1017,62 +993,6 @@ function formatDateTime(iso) {
|
|
|
1017
993
|
</ul>
|
|
1018
994
|
</div>
|
|
1019
995
|
|
|
1020
|
-
<!-- Error-path advice (PO-443, specs/workflow-advice.md).
|
|
1021
|
-
Slate, NOT amber: amber already means "fix this before it runs" twice
|
|
1022
|
-
over in this editor (the reauth strip, the validation banner) and on
|
|
1023
|
-
the node cards, so a third would leave the colour meaning nothing.
|
|
1024
|
-
This blocks nothing — the activation control stays operable while it
|
|
1025
|
-
shows, which is the point of it not being a finding.
|
|
1026
|
-
|
|
1027
|
-
Editor tab only, unlike the validation banner above: every line here
|
|
1028
|
-
acts on the canvas — reveal a card, attach a step to it — so over the
|
|
1029
|
-
run list or the history preview it offers moves to a canvas that is not
|
|
1030
|
-
on screen, and its node names read as part of whatever is. That is not
|
|
1031
|
-
hypothetical: it put a current node's name over a preview of an
|
|
1032
|
-
archived revision (workflow-history.md AC-3). -->
|
|
1033
|
-
<div
|
|
1034
|
-
v-if="tab === 'editor' && !openRunId && errorPathAdvice.length"
|
|
1035
|
-
class="border-b border-slate-500/20 bg-slate-500/5 px-4 py-2 text-sm text-slate-700"
|
|
1036
|
-
>
|
|
1037
|
-
<p class="font-medium">
|
|
1038
|
-
{{ t("integration.editor.errorPath.advice", errorPathAdvice.length) }}
|
|
1039
|
-
</p>
|
|
1040
|
-
<ul
|
|
1041
|
-
class="mt-1 space-y-0.5"
|
|
1042
|
-
:class="adviceExpanded ? 'max-h-36 overflow-y-auto pr-1' : ''"
|
|
1043
|
-
>
|
|
1044
|
-
<li
|
|
1045
|
-
v-for="a in adviceExpanded ? errorPathAdvice : errorPathAdvice.slice(0, WARN_COLLAPSED)"
|
|
1046
|
-
:key="`${a.nodeId}-${a.port}`"
|
|
1047
|
-
class="flex items-baseline gap-1.5"
|
|
1048
|
-
>
|
|
1049
|
-
<span class="select-none text-slate-400">•</span>
|
|
1050
|
-
<button
|
|
1051
|
-
type="button"
|
|
1052
|
-
class="text-left font-medium underline decoration-transparent underline-offset-2 hover:decoration-slate-700"
|
|
1053
|
-
:aria-label="t('integration.editor.errorPath.ariaReveal', { label: a.label, port: a.port })"
|
|
1054
|
-
@click="showNodeOnCanvas(a.nodeId)"
|
|
1055
|
-
>
|
|
1056
|
-
{{ a.label }}
|
|
1057
|
-
</button>
|
|
1058
|
-
<button
|
|
1059
|
-
v-if="canAttachErrorStop"
|
|
1060
|
-
type="button"
|
|
1061
|
-
class="text-slate-600 underline underline-offset-2 hover:text-slate-900"
|
|
1062
|
-
:aria-label="t('integration.editor.errorPath.ariaFix', { label: a.label, port: a.port })"
|
|
1063
|
-
@click="attachErrorStop(a.nodeId, a.port)"
|
|
1064
|
-
>
|
|
1065
|
-
{{ t("integration.editor.errorPath.action.stop") }}
|
|
1066
|
-
</button>
|
|
1067
|
-
</li>
|
|
1068
|
-
<li v-if="!adviceExpanded && errorPathAdvice.length > WARN_COLLAPSED" class="pl-3">
|
|
1069
|
-
<button type="button" class="text-slate-600 underline underline-offset-2 hover:text-slate-900" @click="adviceExpanded = true">
|
|
1070
|
-
{{ t("integration.editor.errorPath.more", { n: errorPathAdvice.length - WARN_COLLAPSED }) }}
|
|
1071
|
-
</button>
|
|
1072
|
-
</li>
|
|
1073
|
-
</ul>
|
|
1074
|
-
</div>
|
|
1075
|
-
|
|
1076
996
|
<!-- Only the FAILED build state banners (its message matters); the
|
|
1077
997
|
working states live as the header chip — and inside a run the
|
|
1078
998
|
workflow's build state is noise, so nothing shows at all. -->
|
|
@@ -65,6 +65,23 @@
|
|
|
65
65
|
* Two members exist here only because the code reads them off values the cockpit
|
|
66
66
|
* may hand back as `null`: `getCredential` and `createCredential` both resolve to
|
|
67
67
|
* `MailCredential | null`, and the callers have to say what they do about it.
|
|
68
|
+
*
|
|
69
|
+
* **What "narrowed to the members it reads" costs when it is wrong, measured on
|
|
70
|
+
* `sftp` (PO-387).** This declaration was five members behind what
|
|
71
|
+
* `SftpAccountImportDialog.vue` actually reads — `loading`, `activeTenantSlug`,
|
|
72
|
+
* an optional rather than required argument to `load`, and `server` and
|
|
73
|
+
* `home_directory` on an account. The cockpit has had all five throughout
|
|
74
|
+
* (`app/composables/useConsoleSftp.ts`, `app/types/consoleSftp.ts`), so nothing
|
|
75
|
+
* was ever broken for an operator; what was broken is that the playground's
|
|
76
|
+
* double is built from *this* file, so a host honouring the written contract
|
|
77
|
+
* threw `TypeError: Cannot read properties of undefined (reading 'value')` while
|
|
78
|
+
* drawing that dialog. **Vue's error handler turns such a throw into a
|
|
79
|
+
* `console.error` and unmounts the tree, so no `pageerror` fires** — the suite's
|
|
80
|
+
* usual way of catching this class (`e2e/credential-managed-source.spec.ts`,
|
|
81
|
+
* `pageErrors`) cannot see it, and the only evidence is a blank page. The dialog
|
|
82
|
+
* now takes its type from here (`Pick<CockpitSources['sftp'], …>`) rather than
|
|
83
|
+
* declaring its own, so the two cannot disagree again; nothing yet compares
|
|
84
|
+
* either against the cockpit, which is [PO-310](https://linear.app/revenexx/issue/PO-310).
|
|
68
85
|
*/
|
|
69
86
|
export interface CockpitSources {
|
|
70
87
|
tenants: {
|
|
@@ -102,7 +119,53 @@ export interface CockpitSources {
|
|
|
102
119
|
accounts: {
|
|
103
120
|
value: CockpitSftpAccount[];
|
|
104
121
|
};
|
|
105
|
-
|
|
122
|
+
/**
|
|
123
|
+
* True while the cockpit's **account fetch** is in flight — which is narrower
|
|
124
|
+
* than "this source is busy", and a caller that needs the wider question has
|
|
125
|
+
* to keep its own answer beside this one.
|
|
126
|
+
*
|
|
127
|
+
* `useConsoleSftp.load()` resolves the tenant above the flag
|
|
128
|
+
* (`if (!tenantSlug) await loadTenants()` sits before `loading.value = true`),
|
|
129
|
+
* and leaves by two paths that never set it at all: no tenant resolved, and a
|
|
130
|
+
* load already in flight for another surface. `SftpAccountImportDialog.vue`
|
|
131
|
+
* learned this the expensive way — it drew its "there are none, go and create
|
|
132
|
+
* one" empty state throughout the tenants fetch on every first open.
|
|
133
|
+
*/
|
|
134
|
+
loading: {
|
|
135
|
+
value: boolean;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Why the last account fetch failed, or `null`.
|
|
139
|
+
*
|
|
140
|
+
* The cockpit catches its own fetch errors — `load` never rejects — so this
|
|
141
|
+
* is the **only** way a caller learns the list is missing rather than empty.
|
|
142
|
+
* Without it a failed load and a tenant that owns no account are the same
|
|
143
|
+
* screen, and that screen tells the operator to go and create one.
|
|
144
|
+
*
|
|
145
|
+
* **Optional, and the only member here that is.** The standalone dev studio's
|
|
146
|
+
* stand-ins provide the other four and not this one
|
|
147
|
+
* (`integrations/studio-ui/app/composables/consoleStubs.ts`), so a required
|
|
148
|
+
* member would be this file's own defect a second time — PO-387 is exactly a
|
|
149
|
+
* dialog that dereferenced what a host did not have. Its absence costs only
|
|
150
|
+
* the message: a caller reading it with `?.` falls back to the empty state,
|
|
151
|
+
* which is what such a host showed before. A host that means to keep
|
|
152
|
+
* `specs/node-configuration.md` AC-34 has to provide it.
|
|
153
|
+
*
|
|
154
|
+
* It is also **not a verdict on the load that just returned.** It is a
|
|
155
|
+
* `useState` singleton shared by every surface in the host, and both early
|
|
156
|
+
* returns described under `loading` leave it at whatever it already held —
|
|
157
|
+
* so it can be stale and can be somebody else's. `SftpAccountImportDialog.vue`
|
|
158
|
+
* shows how much has to be true around it before it is worth repeating.
|
|
159
|
+
*/
|
|
160
|
+
error?: {
|
|
161
|
+
value: string | null;
|
|
162
|
+
};
|
|
163
|
+
/** The tenant the accounts belong to — the org's only one, or `null`. */
|
|
164
|
+
activeTenantSlug: {
|
|
165
|
+
value: string | null;
|
|
166
|
+
};
|
|
167
|
+
/** The argument is **optional**: omitted, the cockpit falls back to `activeTenantSlug`. */
|
|
168
|
+
load: (tenant?: string | null) => Promise<unknown>;
|
|
106
169
|
/** `null` while the account is still provisioning (the Console answers 404). */
|
|
107
170
|
revealPassword: (tenant: string, id: number) => Promise<{
|
|
108
171
|
username: string;
|
|
@@ -131,6 +194,10 @@ export interface CockpitSftpAccount {
|
|
|
131
194
|
id: number;
|
|
132
195
|
/** The gateway login. `null` until the account finished provisioning. */
|
|
133
196
|
login_username: string | null;
|
|
197
|
+
/** The gateway host to connect to. */
|
|
198
|
+
server: string | null;
|
|
199
|
+
/** Where on the gateway the account lands; shown instead of `server` when set. */
|
|
200
|
+
home_directory: string | null;
|
|
134
201
|
status: string;
|
|
135
202
|
is_active: boolean;
|
|
136
203
|
error_message: string | null;
|
|
@@ -28,6 +28,21 @@ export interface CredentialStateInfo {
|
|
|
28
28
|
}
|
|
29
29
|
type TestableCredential = Pick<Credential, 'status' | 'last_test_ok' | 'last_test_at' | 'last_test_message'>;
|
|
30
30
|
/** Every state a credential can be in, in the order a reader is offered them. */
|
|
31
|
+
/**
|
|
32
|
+
* Which of the two words a three-legged credential's provider step is offered under,
|
|
33
|
+
* or `null` where its state gives no reason to offer one at all.
|
|
34
|
+
*
|
|
35
|
+
* The distinction is whether anything has ever proved this credential connected. Only
|
|
36
|
+
* a credential nothing has — never tested, and for an `oauth2-authcode` type that is
|
|
37
|
+
* what a provider window which failed to open leaves behind — is offered the step as
|
|
38
|
+
* *connecting*. One whose test failed, or whose authorisation expired, was working at
|
|
39
|
+
* some point, so it is offered *reconnecting*: the word says which of the two happened
|
|
40
|
+
* (PO-478).
|
|
41
|
+
*
|
|
42
|
+
* It answers for a state rather than a credential because both surfaces that offer the
|
|
43
|
+
* step derive that state first, and the word may not disagree between them.
|
|
44
|
+
*/
|
|
45
|
+
export declare function providerStepWord(state: CredentialConnState): 'connect' | 'reconnect' | null;
|
|
31
46
|
export declare const CREDENTIAL_STATES: readonly ["connected", "failed", "needs_reauth", "untested"];
|
|
32
47
|
/**
|
|
33
48
|
* What a state is called for the reader, keyed rather than derived from a
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { useIntegrationsT } from "../composables/useIntegrationsT.js";
|
|
2
|
+
export function providerStepWord(state) {
|
|
3
|
+
if (state === "connected") return null;
|
|
4
|
+
return state === "untested" ? "connect" : "reconnect";
|
|
5
|
+
}
|
|
2
6
|
export const CREDENTIAL_STATES = ["connected", "failed", "needs_reauth", "untested"];
|
|
3
7
|
const MESSAGE_KEY = {
|
|
4
8
|
connected: "integration.credentials.state.connected",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/studio-integrations",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
|
|
6
6
|
"repository": {
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@nuxt/kit": "^4.0.0",
|
|
29
|
-
"@revenexx/studio-shared": "^0.3.
|
|
29
|
+
"@revenexx/studio-shared": "^0.3.2",
|
|
30
30
|
"@solar-icons/nuxt": "^1.2.0",
|
|
31
31
|
"marked": "^18.0.5"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@dagrejs/dagre": "^3.0.0",
|
|
35
|
-
"@revenexx/studio": "
|
|
35
|
+
"@revenexx/studio": "^0.2.3",
|
|
36
36
|
"@tiptap/core": "^3.28.0",
|
|
37
37
|
"@tiptap/extension-document": "^3.28.0",
|
|
38
38
|
"@tiptap/extension-history": "^3.28.0",
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { WorkflowBlob } from '../types/integrations.js';
|
|
2
|
-
/**
|
|
3
|
-
* Nodes that route failures to an error output nothing is connected to
|
|
4
|
-
* (specs/workflow-advice.md).
|
|
5
|
-
*
|
|
6
|
-
* A step that fails on a path its author drew is handled. A step that fails on a
|
|
7
|
-
* path nobody drew is not: the engine ends the run there
|
|
8
|
-
* (`specs/node-failure-handling.md` AC-8 in the service repository), and before
|
|
9
|
-
* it did, the failure vanished and the run reported success. Either way the
|
|
10
|
-
* author never chose it — they simply never saw the port.
|
|
11
|
-
*
|
|
12
|
-
* ## Why this is advice and not a finding
|
|
13
|
-
*
|
|
14
|
-
* The backend's validation findings feed the activation gate AND an
|
|
15
|
-
* unconditional refusal to start a run. A finding here would therefore stop
|
|
16
|
-
* every workflow with an unwired error port from running at all — turning
|
|
17
|
-
* "delivers nothing quietly" into "does not start", for authors who never asked
|
|
18
|
-
* for either. Computing it in the editor makes that impossible rather than
|
|
19
|
-
* merely unlikely: there is no value for a blocking consumer to read.
|
|
20
|
-
*
|
|
21
|
-
* What the client cannot do is be authoritative — the catalogue may not be
|
|
22
|
-
* loaded, and a workflow built through the API never passes here at all. For
|
|
23
|
-
* advice that is the right trade; for a gate it would not be, which is why the
|
|
24
|
-
* gate stays where the manifests are.
|
|
25
|
-
*
|
|
26
|
-
* ## Deliberately not in `validationFindings.ts`
|
|
27
|
-
*
|
|
28
|
-
* Everything there is a backend finding addressed by blob path, and the node
|
|
29
|
-
* card counts those as *incomplete settings*. An unwired port is neither. Sent
|
|
30
|
-
* through `groupFindingsByNode` it would type-check perfectly and quietly
|
|
31
|
-
* corrupt two counts — the banner's and the card badge's.
|
|
32
|
-
*/
|
|
33
|
-
export interface ErrorPathAdvice {
|
|
34
|
-
/** Blob node id — not an index, so it survives a node being added or removed. */
|
|
35
|
-
nodeId: string;
|
|
36
|
-
/** The error output with nothing attached. */
|
|
37
|
-
port: string;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Every unwired error output in the blob, in node order.
|
|
41
|
-
*
|
|
42
|
-
* Runs against the blob the author is *editing*, not the saved one, so wiring a
|
|
43
|
-
* port clears its line in the same tick — no save, and no second copy of this
|
|
44
|
-
* rule to decide when the advice went stale.
|
|
45
|
-
*
|
|
46
|
-
* **Resolved against whichever manifest version the catalogue has.**
|
|
47
|
-
* `findManifest` falls back to another version of the same slug when the pinned
|
|
48
|
-
* one is absent, and that fallback is accepted here: a node pinned to a version
|
|
49
|
-
* nobody serves any more is better described by a sibling version than by
|
|
50
|
-
* silence. The cost is real though — where the versions disagree about their
|
|
51
|
-
* ports, the advice describes the sibling's, so a port could be named that the
|
|
52
|
-
* author's own node does not have.
|
|
53
|
-
*
|
|
54
|
-
* **Reads the manifest's static outputs only.** A node whose ports are named at
|
|
55
|
-
* author time (`sourceFromConfig` / `resolveOutputs`) cannot be judged from the
|
|
56
|
-
* declaration alone, so nothing is claimed about it — the same forbearance the
|
|
57
|
-
* engine shows. No shipped node has a dynamic error port today; if one appears,
|
|
58
|
-
* this is where it would silently escape notice.
|
|
59
|
-
*/
|
|
60
|
-
export declare function unwiredErrorPorts(blob: Pick<WorkflowBlob, 'nodes' | 'edges'>): ErrorPathAdvice[];
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { findManifest } from "../data/integrationNodeCatalog.js";
|
|
2
|
-
export function unwiredErrorPorts(blob) {
|
|
3
|
-
const advice = [];
|
|
4
|
-
for (const node of blob.nodes) {
|
|
5
|
-
const manifest = findManifest(node.nodeSlug, node.nodeVersion);
|
|
6
|
-
if (!manifest) continue;
|
|
7
|
-
const errorPorts = (manifest.outputs ?? []).filter((o) => o.kind === "error" && o.name).map((o) => o.name);
|
|
8
|
-
if (!errorPorts.length) continue;
|
|
9
|
-
if (!isReachable(node, blob, errorPorts)) continue;
|
|
10
|
-
for (const port of errorPorts) {
|
|
11
|
-
const wired = blob.edges.some((e) => e.from.nodeId === node.id && e.from.output === port);
|
|
12
|
-
if (!wired) advice.push({ nodeId: node.id, port });
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return advice;
|
|
16
|
-
}
|
|
17
|
-
function isReachable(node, blob, errorPorts) {
|
|
18
|
-
return blob.edges.some((e) => e.to.nodeId === node.id) || blob.edges.some((e) => e.from.nodeId === node.id && !errorPorts.includes(e.from.output));
|
|
19
|
-
}
|