@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
|
@@ -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.",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, onMounted, reactive, ref } from "vue";
|
|
2
|
+
import { computed, nextTick, onMounted, reactive, ref } from "vue";
|
|
3
3
|
import {
|
|
4
4
|
RefreshCircle as Loader2,
|
|
5
5
|
AddCircle as Plus,
|
|
@@ -31,8 +31,8 @@ import {
|
|
|
31
31
|
SelectValue
|
|
32
32
|
} from "@revenexx/studio";
|
|
33
33
|
import { toast } from "../../utils/notify";
|
|
34
|
-
import {
|
|
35
|
-
import { CREDENTIAL_STATES, credentialState, credentialStateLabel } from "../../utils/credentialState";
|
|
34
|
+
import { apiRefusal, errorReason } from "../../utils/apiError";
|
|
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 {
|
|
@@ -92,19 +103,36 @@ const form = reactive({ name: "", config: {} });
|
|
|
92
103
|
const saving = ref(false);
|
|
93
104
|
const testing = ref(false);
|
|
94
105
|
const testResult = ref(null);
|
|
95
|
-
const
|
|
96
|
-
const fieldErrors = ref({});
|
|
106
|
+
const saveRefusal = ref(null);
|
|
97
107
|
const sourceActive = ref(false);
|
|
98
108
|
const isEdit = computed(() => editing.value !== null);
|
|
99
109
|
function fieldsOf(t2) {
|
|
100
110
|
return t2?.fields ?? [];
|
|
101
111
|
}
|
|
112
|
+
const drawnKeys = computed(() => [
|
|
113
|
+
"name",
|
|
114
|
+
...sourceActive.value ? [] : fieldsOf(pickedType.value).map((f) => `config.${f.key}`)
|
|
115
|
+
]);
|
|
116
|
+
const refusal = computed(() => apiRefusal(saveRefusal.value, drawnKeys.value));
|
|
117
|
+
const fieldErrors = computed(() => refusal.value.atFields);
|
|
118
|
+
const summaryReasons = computed(() => {
|
|
119
|
+
if (!saveRefusal.value) return [];
|
|
120
|
+
if (refusal.value.unshown.length) return refusal.value.unshown.map((u) => u.message);
|
|
121
|
+
if (Object.keys(refusal.value.atFields).length) return [];
|
|
122
|
+
return [errorReason(saveRefusal.value, t("integration.credentials.error.save"))];
|
|
123
|
+
});
|
|
102
124
|
function configFieldAria(key) {
|
|
103
125
|
return fieldErrors.value[`config.${key}`] ? { "aria-invalid": "true", "aria-describedby": `cred-field-${key}-error` } : {};
|
|
104
126
|
}
|
|
127
|
+
async function focusFirstRefusedField() {
|
|
128
|
+
await nextTick();
|
|
129
|
+
const key = drawnKeys.value.find((k) => k in fieldErrors.value);
|
|
130
|
+
if (!key) return;
|
|
131
|
+
const id = key === "name" ? "cred-name" : `cred-field-${key.slice("config.".length)}`;
|
|
132
|
+
document.getElementById(id)?.focus();
|
|
133
|
+
}
|
|
105
134
|
function clearRefusal() {
|
|
106
|
-
|
|
107
|
-
fieldErrors.value = {};
|
|
135
|
+
saveRefusal.value = null;
|
|
108
136
|
}
|
|
109
137
|
function openCreate(t2) {
|
|
110
138
|
editing.value = null;
|
|
@@ -176,9 +204,9 @@ async function submit() {
|
|
|
176
204
|
}
|
|
177
205
|
}
|
|
178
206
|
} catch (e) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
toast.error(
|
|
207
|
+
saveRefusal.value = e;
|
|
208
|
+
void focusFirstRefusedField();
|
|
209
|
+
toast.error(errorReason(e, t("integration.credentials.error.save")));
|
|
182
210
|
} finally {
|
|
183
211
|
saving.value = false;
|
|
184
212
|
}
|
|
@@ -289,15 +317,16 @@ async function confirmDelete() {
|
|
|
289
317
|
<template #cell-actions="{ row: c }">
|
|
290
318
|
<div class="flex items-center justify-end gap-2 text-sm" @click.stop>
|
|
291
319
|
<Button
|
|
292
|
-
v-if="
|
|
320
|
+
v-if="providerStep(c)"
|
|
293
321
|
variant="outline"
|
|
294
322
|
size="sm"
|
|
295
|
-
class="h-7
|
|
323
|
+
class="h-7"
|
|
324
|
+
:class="providerStep(c).expired ? 'text-amber-600' : ''"
|
|
296
325
|
:disabled="connecting === c.id"
|
|
297
|
-
:aria-label="
|
|
326
|
+
:aria-label="providerStep(c).aria"
|
|
298
327
|
@click="connect(c)"
|
|
299
328
|
>
|
|
300
|
-
{{
|
|
329
|
+
{{ providerStep(c).label }}
|
|
301
330
|
</Button>
|
|
302
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">
|
|
303
332
|
<TrashBin :size="13" />
|
|
@@ -351,7 +380,10 @@ async function confirmDelete() {
|
|
|
351
380
|
<IntegrationsCredentialRevenexxSource
|
|
352
381
|
v-if="hasRevenexxSource(pickedType?.slug)"
|
|
353
382
|
:type="pickedType"
|
|
354
|
-
@update:active="(v) =>
|
|
383
|
+
@update:active="(v) => {
|
|
384
|
+
sourceActive = v;
|
|
385
|
+
clearRefusal();
|
|
386
|
+
}"
|
|
355
387
|
@apply="(c2) => Object.assign(form.config, c2)"
|
|
356
388
|
/>
|
|
357
389
|
|
|
@@ -438,14 +470,16 @@ async function confirmDelete() {
|
|
|
438
470
|
|
|
439
471
|
</div>
|
|
440
472
|
|
|
441
|
-
<!-- The save was refused
|
|
442
|
-
button sits in the footer below, so a summary at
|
|
443
|
-
scrolls — five fields already do — would be a
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
gateway error —
|
|
447
|
-
<div v-if="
|
|
448
|
-
|
|
473
|
+
<!-- The save was refused for something no field shows. Outside the scrolling
|
|
474
|
+
form on purpose: the Save button sits in the footer below, so a summary at
|
|
475
|
+
the end of a form that scrolls — five fields already do — would be a
|
|
476
|
+
refusal rendered under the fold. What is marked at a field is not repeated
|
|
477
|
+
here; what has no field to be marked at — a service-side field, a conflict,
|
|
478
|
+
a gateway error — would otherwise be silent. -->
|
|
479
|
+
<div v-if="summaryReasons.length && pickedType" role="alert" class="space-y-1 rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
|
|
480
|
+
<!-- Keyed by position: two undrawn keys can be refused with the same
|
|
481
|
+
sentence, and a duplicate key is how one of them goes unrendered. -->
|
|
482
|
+
<p v-for="(reason, i) in summaryReasons" :key="i">{{ reason }}</p>
|
|
449
483
|
</div>
|
|
450
484
|
|
|
451
485
|
<DialogFooter v-if="pickedType" class="sm:justify-between">
|
|
@@ -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. -->
|
|
@@ -35,21 +35,48 @@ export declare function apiValidationIssues(err: unknown): {
|
|
|
35
35
|
path: string;
|
|
36
36
|
message: string;
|
|
37
37
|
}[];
|
|
38
|
+
/** One message the service keyed to a field, as it keyed it. */
|
|
39
|
+
export interface RefusalMessage {
|
|
40
|
+
key: string;
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A refusal split by what the form on screen can show of it.
|
|
45
|
+
*
|
|
46
|
+
* `atFields` is what goes under an input, keyed the way the form draws it; `unshown`
|
|
47
|
+
* is everything left over — the reasons that have no input to stand at, in the order
|
|
48
|
+
* the service listed them.
|
|
49
|
+
*/
|
|
50
|
+
export interface Refusal {
|
|
51
|
+
atFields: Record<string, string>;
|
|
52
|
+
unshown: RefusalMessage[];
|
|
53
|
+
}
|
|
38
54
|
/**
|
|
39
|
-
*
|
|
55
|
+
* A rejection split into the messages a form can show at a field and the messages it
|
|
56
|
+
* cannot, given the keys that form actually draws.
|
|
40
57
|
*
|
|
41
58
|
* The counterpart to `apiValidationIssues` above, for the ordinary case that has
|
|
42
59
|
* nothing to do with the workflow blob: a form field the service refused, named the
|
|
43
60
|
* way the form names it. Laravel answers a `422` with `errors` keyed by field and a
|
|
44
|
-
* list of messages per key; the first message is the one a person reads,
|
|
45
|
-
* the one kept.
|
|
61
|
+
* list of messages per key; at a field the first message is the one a person reads,
|
|
62
|
+
* so that is the one kept.
|
|
63
|
+
*
|
|
64
|
+
* **`drawn` is the form's own list, in the order the form draws it** — the name, then
|
|
65
|
+
* the fields of the chosen type, and nothing while those are hidden behind another
|
|
66
|
+
* source. A key absent from it has no input to be shown at, whether because no form
|
|
67
|
+
* draws that field (`config` as a whole, a field the type never declared, a key that
|
|
68
|
+
* is not a field at all) or because this form is currently drawing something else.
|
|
69
|
+
* Both cases go to `unshown`, which is what the surface states below the form: a
|
|
70
|
+
* refusal nobody can see is the same as no answer at all (PO-275, PO-477).
|
|
71
|
+
*
|
|
72
|
+
* A container that is only at fault because one of its members is — `config` beside
|
|
73
|
+
* `config.authKey`, which the service reports as two — is dropped wherever the member
|
|
74
|
+
* is reported too: it says the same thing twice, and the member carries it either way.
|
|
46
75
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* else about a field they then have to find (PO-275). Keys that address the blob are
|
|
50
|
-
* left out: those belong to `apiValidationIssues`, which resolves them to nodes.
|
|
76
|
+
* Keys that address the blob are left out: those belong to `apiValidationIssues`,
|
|
77
|
+
* which resolves them to nodes. No credential surface can receive one.
|
|
51
78
|
*/
|
|
52
|
-
export declare function
|
|
79
|
+
export declare function apiRefusal(err: unknown, drawn: readonly string[]): Refusal;
|
|
53
80
|
/**
|
|
54
81
|
* The reason a rejection gives, without the count of everything else it found.
|
|
55
82
|
*
|