@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
|
@@ -18,20 +18,20 @@ export function apiValidationIssues(err) {
|
|
|
18
18
|
}
|
|
19
19
|
return issues;
|
|
20
20
|
}
|
|
21
|
-
export function
|
|
22
|
-
|
|
21
|
+
export function apiRefusal(err, drawn) {
|
|
22
|
+
const refusal = { atFields: {}, unshown: [] };
|
|
23
|
+
if (!(err instanceof IntegrationsApiError) || err.status !== 422) return refusal;
|
|
23
24
|
const errors = err.body?.errors;
|
|
24
|
-
if (!errors || typeof errors !== "object") return
|
|
25
|
-
const
|
|
26
|
-
for (const
|
|
27
|
-
if (
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (Object.keys(out).some((other) => other.startsWith(`${key}.`))) delete out[key];
|
|
25
|
+
if (!errors || typeof errors !== "object") return refusal;
|
|
26
|
+
const keys = Object.keys(errors).filter((key) => key !== "blob" && !key.startsWith("blob."));
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
if (keys.some((other) => other.startsWith(`${key}.`))) continue;
|
|
29
|
+
const messages = (Array.isArray(errors[key]) ? errors[key] : [errors[key]]).filter((message) => message !== void 0 && message !== null).map(String);
|
|
30
|
+
if (!messages.length) continue;
|
|
31
|
+
if (drawn.includes(key)) refusal.atFields[key] = messages[0];
|
|
32
|
+
else refusal.unshown.push(...messages.map((message) => ({ key, message })));
|
|
33
33
|
}
|
|
34
|
-
return
|
|
34
|
+
return refusal;
|
|
35
35
|
}
|
|
36
36
|
export function errorReason(err, fallback) {
|
|
37
37
|
const raw = err?.message?.trim();
|
|
@@ -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
|
-
}
|