@revenexx/studio-integrations 1.4.0 → 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 +193 -0
- package/README.md +1 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +61 -29
- 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 +57 -23
- package/dist/runtime/pages/integrations/schedules.vue +11 -12
- package/dist/runtime/pages/integrations/workflows/[id].vue +0 -80
- package/dist/runtime/utils/apiError.d.ts +35 -8
- package/dist/runtime/utils/apiError.js +12 -12
- 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,198 @@
|
|
|
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
|
+
|
|
165
|
+
## 1.4.1
|
|
166
|
+
|
|
167
|
+
### Patch Changes
|
|
168
|
+
|
|
169
|
+
- 7bb1913: A refused credential save states each reason once: at its field, or below the form
|
|
170
|
+
when no field can show it (PO-477).
|
|
171
|
+
|
|
172
|
+
**What a field can carry is marked there and nowhere else.** Until now the service's
|
|
173
|
+
lead reason was also repeated in a red box above the Save button, so the one sentence
|
|
174
|
+
the operator had already read was the one thing on the surface they had to read twice
|
|
175
|
+
— and it crowded out the reasons that had no other place to appear.
|
|
176
|
+
|
|
177
|
+
**What no field can carry is what the box is for now.** The service keys a refusal by
|
|
178
|
+
validation rule, and several of those shapes name nothing either dialog draws an input
|
|
179
|
+
for: the access data taken as a whole, a field the credential type never handed out, a
|
|
180
|
+
field hidden while a managed revenexx resource is being reused, or a refusal that is
|
|
181
|
+
not field-keyed at all, such as a conflict or a gateway error. Any of them was silent
|
|
182
|
+
unless the service happened to lead with it. All of them are now stated below the form,
|
|
183
|
+
every message of them, still without the service's count of everything else it found.
|
|
184
|
+
|
|
185
|
+
**The refusal also puts the operator in the first marked field.** The box was what
|
|
186
|
+
guaranteed a refusal could not be rendered below the fold of a form that scrolls; the
|
|
187
|
+
focus is what guarantees it now, and it leaves the cursor where the correction has to
|
|
188
|
+
be typed.
|
|
189
|
+
|
|
190
|
+
Both dialogs read the refusal through one seam, so the create-and-edit dialog on
|
|
191
|
+
Credentials and the credential detail dialog answer the same way.
|
|
192
|
+
|
|
193
|
+
Promised behaviour: `specs/credential-create.md` AC-8 and AC-11,
|
|
194
|
+
`specs/credential-detail.md` AC-6.
|
|
195
|
+
|
|
3
196
|
## 1.4.0
|
|
4
197
|
|
|
5
198
|
### Minor 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, reactive, ref, watch } from "vue";
|
|
2
|
+
import { computed, nextTick, reactive, ref, watch } from "vue";
|
|
3
3
|
import {
|
|
4
4
|
CheckCircle as CheckCircle2,
|
|
5
5
|
CloseCircle as XCircle,
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
SelectValue
|
|
31
31
|
} from "@revenexx/studio";
|
|
32
32
|
import { toast } from "../../utils/notify";
|
|
33
|
-
import {
|
|
33
|
+
import { apiRefusal, errorReason } from "../../utils/apiError";
|
|
34
34
|
import { credentialState, relativeTime } from "../../utils/credentialState";
|
|
35
35
|
import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
|
|
36
36
|
import { resolveLocalized } from "../../utils/localize";
|
|
@@ -53,14 +53,31 @@ const saving = ref(false);
|
|
|
53
53
|
const deleting = ref(false);
|
|
54
54
|
const testing = ref(false);
|
|
55
55
|
const liveTest = ref(null);
|
|
56
|
-
const
|
|
57
|
-
const
|
|
56
|
+
const saveRefusal = ref(null);
|
|
57
|
+
const drawnKeys = computed(() => [
|
|
58
|
+
"name",
|
|
59
|
+
...sourceActive.value ? [] : fieldsOf(props.type).map((f) => `config.${f.key}`)
|
|
60
|
+
]);
|
|
61
|
+
const refusal = computed(() => apiRefusal(saveRefusal.value, drawnKeys.value));
|
|
62
|
+
const fieldErrors = computed(() => refusal.value.atFields);
|
|
63
|
+
const summaryReasons = computed(() => {
|
|
64
|
+
if (!saveRefusal.value) return [];
|
|
65
|
+
if (refusal.value.unshown.length) return refusal.value.unshown.map((u) => u.message);
|
|
66
|
+
if (Object.keys(refusal.value.atFields).length) return [];
|
|
67
|
+
return [errorReason(saveRefusal.value, t("integration.credentials.error.save"))];
|
|
68
|
+
});
|
|
58
69
|
function configFieldAria(key) {
|
|
59
70
|
return fieldErrors.value[`config.${key}`] ? { "aria-invalid": "true", "aria-describedby": `detail-field-${key}-error` } : {};
|
|
60
71
|
}
|
|
72
|
+
async function focusFirstRefusedField() {
|
|
73
|
+
await nextTick();
|
|
74
|
+
const key = drawnKeys.value.find((k) => k in fieldErrors.value);
|
|
75
|
+
if (!key) return;
|
|
76
|
+
const id = key === "name" ? "detail-cred-name" : `detail-field-${key.slice("config.".length)}`;
|
|
77
|
+
document.getElementById(id)?.focus();
|
|
78
|
+
}
|
|
61
79
|
function clearRefusal() {
|
|
62
|
-
|
|
63
|
-
fieldErrors.value = {};
|
|
80
|
+
saveRefusal.value = null;
|
|
64
81
|
}
|
|
65
82
|
const connecting = ref(false);
|
|
66
83
|
const typeName = computed(() => resolveLocalized(props.type?.name) || props.credential?.credential_type_slug || "");
|
|
@@ -103,6 +120,7 @@ async function runTest() {
|
|
|
103
120
|
if (!props.credential) return;
|
|
104
121
|
testing.value = true;
|
|
105
122
|
liveTest.value = null;
|
|
123
|
+
clearRefusal();
|
|
106
124
|
try {
|
|
107
125
|
liveTest.value = await test(props.credential.id);
|
|
108
126
|
await load(true);
|
|
@@ -121,9 +139,9 @@ async function save() {
|
|
|
121
139
|
emit("update:open", false);
|
|
122
140
|
toast.success(t("integration.credentials.toast.updated"));
|
|
123
141
|
} catch (e) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
toast.error(
|
|
142
|
+
saveRefusal.value = e;
|
|
143
|
+
void focusFirstRefusedField();
|
|
144
|
+
toast.error(errorReason(e, t("integration.credentials.error.save")));
|
|
127
145
|
} finally {
|
|
128
146
|
saving.value = false;
|
|
129
147
|
}
|
|
@@ -182,8 +200,11 @@ async function connect() {
|
|
|
182
200
|
<!-- Standard cockpit tab bar (violet underline) -->
|
|
183
201
|
<TabBar :model-value="tab" :tabs="tabs" @update:model-value="(k) => tab = k" />
|
|
184
202
|
|
|
185
|
-
<!-- Tab 1: Credentials
|
|
186
|
-
|
|
203
|
+
<!-- Tab 1: Credentials. A `template` rather than a wrapping element: the
|
|
204
|
+
scrolling pane and the block below it are siblings, as they were when the
|
|
205
|
+
pane was the whole tab, so nothing about the spacing around them moves. -->
|
|
206
|
+
<template v-if="tab === 'credentials'">
|
|
207
|
+
<div class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
|
|
187
208
|
<!-- Connection state — the headline answer: are these credentials correct? -->
|
|
188
209
|
<div class="rounded-lg border p-3" :class="state.toneClass">
|
|
189
210
|
<div class="flex items-center gap-2">
|
|
@@ -243,7 +264,10 @@ async function connect() {
|
|
|
243
264
|
<IntegrationsCredentialRevenexxSource
|
|
244
265
|
v-if="hasRevenexxSource(type?.slug)"
|
|
245
266
|
:type="type"
|
|
246
|
-
@update:active="(v) =>
|
|
267
|
+
@update:active="(v) => {
|
|
268
|
+
sourceActive = v;
|
|
269
|
+
clearRefusal();
|
|
270
|
+
}"
|
|
247
271
|
@apply="(c) => Object.assign(form.config, c)"
|
|
248
272
|
/>
|
|
249
273
|
|
|
@@ -313,27 +337,35 @@ async function connect() {
|
|
|
313
337
|
{{ liveTest.ok ? liveTest.message ?? t("integration.credentials.shared.test.ok") : liveTest.error ?? liveTest.message ?? t("integration.credentials.shared.test.failed") }}
|
|
314
338
|
</div>
|
|
315
339
|
|
|
316
|
-
|
|
317
|
-
the field where the service keyed it. -->
|
|
318
|
-
<div v-if="saveError" role="alert" class="rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
|
|
319
|
-
{{ saveError }}
|
|
320
|
-
</div>
|
|
340
|
+
</div>
|
|
321
341
|
|
|
322
|
-
<div class="flex items-center gap-2 pt-1">
|
|
323
|
-
<div class="flex-1" />
|
|
324
|
-
<Button variant="outline" :disabled="testing || saving" @click="runTest">
|
|
325
|
-
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
326
|
-
<TestIcon v-else :size="14" class="mr-1" />
|
|
327
|
-
{{ t("integration.credentials.shared.action.testConnection") }}
|
|
328
|
-
</Button>
|
|
329
|
-
<Button :disabled="saving || !form.name.trim()" @click="save">
|
|
330
|
-
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
331
|
-
{{ t("integration.common.action.save") }}
|
|
332
|
-
</Button>
|
|
333
|
-
</div>
|
|
334
342
|
</div>
|
|
335
343
|
|
|
344
|
+
<!-- Outside the scrolling pane on purpose, the way the create dialog has it:
|
|
345
|
+
a refusal puts the operator in the first marked field, which scrolls that
|
|
346
|
+
pane back to the top — and a summary rendered at the end of the form would
|
|
347
|
+
be carried out of sight by the very focus meant to make the refusal
|
|
348
|
+
visible. What is marked at a field is not repeated here; what has no field
|
|
349
|
+
to be marked at would otherwise be silent. -->
|
|
350
|
+
<div v-if="summaryReasons.length" role="alert" class="mt-3 space-y-1 rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
|
|
351
|
+
<!-- Keyed by position: two undrawn keys can be refused with the same
|
|
352
|
+
sentence, and a duplicate key is how one of them goes unrendered. -->
|
|
353
|
+
<p v-for="(reason, i) in summaryReasons" :key="i">{{ reason }}</p>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<div class="flex items-center gap-2 pt-3">
|
|
357
|
+
<div class="flex-1" />
|
|
358
|
+
<Button variant="outline" :disabled="testing || saving" @click="runTest">
|
|
359
|
+
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
360
|
+
<TestIcon v-else :size="14" class="mr-1" />
|
|
361
|
+
{{ t("integration.credentials.shared.action.testConnection") }}
|
|
362
|
+
</Button>
|
|
363
|
+
<Button :disabled="saving || !form.name.trim()" @click="save">
|
|
364
|
+
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
365
|
+
{{ t("integration.common.action.save") }}
|
|
366
|
+
</Button>
|
|
336
367
|
</div>
|
|
368
|
+
</template>
|
|
337
369
|
|
|
338
370
|
<!-- Tab 2: Details -->
|
|
339
371
|
<div v-else class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
|
|
@@ -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;
|