@revenexx/studio-integrations 0.7.0 → 1.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 +416 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +4 -1
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +8 -2
- package/dist/runtime/components/Integrations/ExpressionInput.vue +5 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +16 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +1 -0
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +95 -10
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +34 -8
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +12 -8
- package/dist/runtime/components/Integrations/StickyNoteView.vue +45 -11
- package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +3 -3
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +12 -3
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +5 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +52 -11
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -2
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +59 -28
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +3 -4
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +37 -26
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -5
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +53 -14
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +2 -2
- package/dist/runtime/composables/useIntegrationsNodes.js +11 -1
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
- package/dist/runtime/composables/useIntegrationsTriggers.js +18 -7
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +25 -4
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/integrationNodeCatalog.d.ts +22 -1
- package/dist/runtime/data/integrationNodeCatalog.js +8 -4
- package/dist/runtime/data/nodeInputData.d.ts +58 -1
- package/dist/runtime/data/nodeInputData.js +30 -5
- package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
- package/dist/runtime/data/triggerGlyphs.js +11 -0
- package/dist/runtime/i18n/messages/de.json +20 -3
- package/dist/runtime/i18n/messages/en.json +20 -3
- package/dist/runtime/pages/integrations/credentials.vue +4 -1
- package/dist/runtime/pages/integrations/index.vue +16 -17
- package/dist/runtime/pages/integrations/templates/[slug].vue +13 -5
- package/dist/runtime/pages/integrations/workflows/[id].vue +193 -104
- package/dist/runtime/types/integrations.d.ts +25 -1
- package/dist/runtime/utils/cockpitSources.d.ts +22 -3
- package/dist/runtime/utils/credentialState.d.ts +6 -1
- package/dist/runtime/utils/credentialState.js +1 -1
- package/dist/runtime/utils/localize.d.ts +34 -0
- package/dist/runtime/utils/localize.js +22 -7
- package/dist/runtime/utils/pollBudget.d.ts +1 -1
- package/dist/runtime/utils/servedPaths.d.ts +22 -0
- package/dist/runtime/utils/servedPaths.js +3 -0
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, reactive, ref } from "vue";
|
|
3
3
|
import { marked } from "marked";
|
|
4
|
-
import { AltArrowRight as Chevron,
|
|
4
|
+
import { AltArrowRight as Chevron, CheckCircle as CheckCircle2, Key, RefreshCircle as Loader2, Shield as ShieldAlert, User } from "#solar-icons";
|
|
5
|
+
import { triggerGlyph as glyphForTrigger } from "../../../data/triggerGlyphs";
|
|
5
6
|
import {
|
|
6
7
|
Badge,
|
|
7
8
|
Button,
|
|
@@ -90,7 +91,6 @@ function firstSentence(text) {
|
|
|
90
91
|
const idx = text.indexOf(". ");
|
|
91
92
|
return idx === -1 ? text : text.slice(0, idx + 1);
|
|
92
93
|
}
|
|
93
|
-
const TRIGGER_ICON = { schedule: Clock, webhook: Webhook, manual: Play, event: Zap };
|
|
94
94
|
const triggerChips = computed(() => (tpl.value?.blob.triggers ?? []).map((trig) => {
|
|
95
95
|
let label = trig.type.charAt(0).toUpperCase() + trig.type.slice(1);
|
|
96
96
|
if (trig.type === "schedule" && trig.config?.cron) {
|
|
@@ -99,7 +99,7 @@ const triggerChips = computed(() => (tpl.value?.blob.triggers ?? []).map((trig)
|
|
|
99
99
|
}
|
|
100
100
|
if (trig.type === "webhook" && trig.config?.path) label = `${t("integration.templates.trigger.webhook")} \xB7 ${trig.config.path}`;
|
|
101
101
|
if (trig.type === "manual") label = t("integration.templates.trigger.manual");
|
|
102
|
-
return { key: trig.id, icon:
|
|
102
|
+
return { key: trig.id, icon: glyphForTrigger(trig.type), label };
|
|
103
103
|
}));
|
|
104
104
|
const variables = computed(() => Object.entries(tpl.value?.blob.variables ?? {}).map(([key, v]) => ({
|
|
105
105
|
key,
|
|
@@ -206,8 +206,16 @@ function levelDot(level) {
|
|
|
206
206
|
</PageHeader>
|
|
207
207
|
|
|
208
208
|
<div class="p-4 space-y-4">
|
|
209
|
-
<!-- Hero: the live workflow preview on the editor's dotted canvas
|
|
210
|
-
|
|
209
|
+
<!-- Hero: the live workflow preview on the editor's dotted canvas.
|
|
210
|
+
Named like the two panels below it, with `aria-label` because it
|
|
211
|
+
carries no heading to point at: it was the largest thing on the page
|
|
212
|
+
and the only one with no name at all, which also left it the one
|
|
213
|
+
surface drawing the miniature that nothing could address (PO-247). -->
|
|
214
|
+
<div
|
|
215
|
+
role="region"
|
|
216
|
+
:aria-label="t('integration.templates.detail.flow.ariaLabel')"
|
|
217
|
+
class="relative overflow-hidden rounded-lg border border-black/[0.06] bg-slate-50/70 [background-image:radial-gradient(circle,rgba(15,23,42,0.07)_1px,transparent_1px)] [background-size:14px_14px]"
|
|
218
|
+
>
|
|
211
219
|
<TemplateFlowPreview v-if="tpl.detailLoaded" :blob="tpl.blob" :height="400" class="px-4" />
|
|
212
220
|
<div v-else class="flex h-[400px] items-center justify-center">
|
|
213
221
|
<Loader2 :size="18" class="animate-spin text-muted-foreground/50" />
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
|
3
|
-
import { AltArrowLeft as ArrowLeft, AltArrowRight as Chevron, Copy as CopyIcon,
|
|
3
|
+
import { AltArrowLeft as ArrowLeft, AltArrowRight as Chevron, Copy as CopyIcon, RefreshCircle as Loader2, Play, Refresh as RetryIcon, Diskette as Save, SidebarMinimalistic as RailIcon } from "#solar-icons";
|
|
4
4
|
import {
|
|
5
5
|
AlertDialog,
|
|
6
|
+
AlertDialogCancel,
|
|
6
7
|
AlertDialogContent,
|
|
7
8
|
AlertDialogDescription,
|
|
8
9
|
AlertDialogFooter,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
SelectItem,
|
|
21
22
|
SelectTrigger,
|
|
22
23
|
SelectValue,
|
|
24
|
+
Switch,
|
|
23
25
|
Textarea
|
|
24
26
|
} from "@revenexx/studio";
|
|
25
27
|
import WorkflowEditor from "../../../components/Integrations/WorkflowEditor.vue";
|
|
@@ -41,7 +43,6 @@ const router = useRouter();
|
|
|
41
43
|
const { workflows, get, update, toggleActive } = useIntegrationsWorkflows();
|
|
42
44
|
const id = computed(() => Number(route.params.id));
|
|
43
45
|
const workflow = computed(() => workflows.value.find((w) => w.id === id.value) ?? null);
|
|
44
|
-
useHead({ title: () => t("integration.editor.head.title", { name: workflow.value?.name ?? t("integration.editor.head.untitled") }) });
|
|
45
46
|
const tab = ref("editor");
|
|
46
47
|
const tabs = computed(() => [
|
|
47
48
|
{ key: "editor", label: t("integration.editor.tab.editor") },
|
|
@@ -49,8 +50,39 @@ const tabs = computed(() => [
|
|
|
49
50
|
{ key: "runs", label: t("integration.editor.tab.runs") },
|
|
50
51
|
{ key: "dead-letters", label: t("integration.editor.tab.deadLetters") }
|
|
51
52
|
]);
|
|
52
|
-
const
|
|
53
|
-
const isDirty = computed(() =>
|
|
53
|
+
const draft = ref(null);
|
|
54
|
+
const isDirty = computed(() => draft.value !== null);
|
|
55
|
+
function draftFrom() {
|
|
56
|
+
if (draft.value) return draft.value;
|
|
57
|
+
const w = workflow.value;
|
|
58
|
+
if (!w) return null;
|
|
59
|
+
return {
|
|
60
|
+
name: w.name,
|
|
61
|
+
description: w.description ?? null,
|
|
62
|
+
executionMode: w.execution_mode ?? "async_only",
|
|
63
|
+
tags: [...w.blob.tags ?? []],
|
|
64
|
+
icon: w.blob.icon ?? null,
|
|
65
|
+
blob: JSON.parse(JSON.stringify(w.blob))
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function payloadBlob(d) {
|
|
69
|
+
const blob = JSON.parse(JSON.stringify(d.blob));
|
|
70
|
+
blob.name = d.name;
|
|
71
|
+
if (d.description) blob.description = d.description;
|
|
72
|
+
else delete blob.description;
|
|
73
|
+
if (d.tags.length) blob.tags = [...d.tags];
|
|
74
|
+
else delete blob.tags;
|
|
75
|
+
if (d.icon) blob.icon = d.icon;
|
|
76
|
+
else delete blob.icon;
|
|
77
|
+
return blob;
|
|
78
|
+
}
|
|
79
|
+
const shownName = computed(() => draft.value?.name ?? workflow.value?.name ?? "");
|
|
80
|
+
const draftedMode = computed(() => draft.value?.executionMode ?? workflow.value?.execution_mode ?? "async_only");
|
|
81
|
+
const shownIcon = computed(() => draft.value ? draft.value.icon : workflow.value?.blob.icon ?? null);
|
|
82
|
+
const shownTags = computed(() => draft.value ? draft.value.tags : [...workflow.value?.blob.tags ?? []]);
|
|
83
|
+
const shownDescription = computed(() => (draft.value ? draft.value.description : workflow.value?.description) ?? "");
|
|
84
|
+
const shownBlob = computed(() => draft.value?.blob ?? workflow.value?.blob ?? null);
|
|
85
|
+
useHead({ title: () => t("integration.editor.head.title", { name: shownName.value || t("integration.editor.head.untitled") }) });
|
|
54
86
|
const saving = ref(false);
|
|
55
87
|
const saveError = ref(null);
|
|
56
88
|
const validationWarnings = computed(() => workflow.value?.warnings);
|
|
@@ -76,7 +108,7 @@ onMounted(() => {
|
|
|
76
108
|
void loadCredentials();
|
|
77
109
|
});
|
|
78
110
|
const reauthCredentials = computed(() => {
|
|
79
|
-
const blob = JSON.stringify(
|
|
111
|
+
const blob = JSON.stringify(shownBlob.value ?? {});
|
|
80
112
|
return needsReauth.value.filter((c) => blob.includes(c.id));
|
|
81
113
|
});
|
|
82
114
|
const buildWorking = computed(() => workflow.value?.buildStatus === "building" || workflow.value?.buildStatus === "pending");
|
|
@@ -123,29 +155,67 @@ const buildNotice = computed(() => {
|
|
|
123
155
|
});
|
|
124
156
|
watch(id, async () => {
|
|
125
157
|
const w = await get(id.value);
|
|
126
|
-
if (!w) await
|
|
127
|
-
|
|
158
|
+
if (!w) await leaveTo("/integrations/workflows");
|
|
159
|
+
draft.value = null;
|
|
128
160
|
warningsExpanded.value = false;
|
|
129
161
|
if (tab.value === "runs") void loadRuns(1);
|
|
130
162
|
}, { immediate: true });
|
|
131
163
|
function onChange(blob) {
|
|
132
|
-
|
|
164
|
+
const d = draftFrom();
|
|
165
|
+
if (d) draft.value = { ...d, blob };
|
|
133
166
|
}
|
|
134
167
|
const editorResetKey = ref(0);
|
|
135
168
|
function discardChanges() {
|
|
136
|
-
|
|
169
|
+
draft.value = null;
|
|
137
170
|
editorResetKey.value++;
|
|
138
171
|
}
|
|
172
|
+
const leaveOpen = ref(false);
|
|
173
|
+
let leaveDecision = null;
|
|
174
|
+
function askBeforeLeaving() {
|
|
175
|
+
leaveOpen.value = true;
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
leaveDecision = resolve;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function answerLeave(leave) {
|
|
181
|
+
leaveOpen.value = false;
|
|
182
|
+
leaveDecision?.(leave);
|
|
183
|
+
leaveDecision = null;
|
|
184
|
+
}
|
|
185
|
+
let leavingOnPurpose = false;
|
|
186
|
+
async function leaveTo(path) {
|
|
187
|
+
leavingOnPurpose = true;
|
|
188
|
+
try {
|
|
189
|
+
await router.push(path);
|
|
190
|
+
} finally {
|
|
191
|
+
leavingOnPurpose = false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function guardTheDraft(to, from) {
|
|
195
|
+
if (to.path === from.path) return true;
|
|
196
|
+
if (leavingOnPurpose) return true;
|
|
197
|
+
if (!isDirty.value) return true;
|
|
198
|
+
return await askBeforeLeaving();
|
|
199
|
+
}
|
|
200
|
+
onBeforeRouteLeave(guardTheDraft);
|
|
201
|
+
onBeforeRouteUpdate(guardTheDraft);
|
|
202
|
+
function warnBeforeUnload(e) {
|
|
203
|
+
if (!isDirty.value) return;
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
e.returnValue = "unsaved";
|
|
206
|
+
}
|
|
207
|
+
onMounted(() => window.addEventListener("beforeunload", warnBeforeUnload));
|
|
208
|
+
onUnmounted(() => window.removeEventListener("beforeunload", warnBeforeUnload));
|
|
139
209
|
const duplicateOpen = ref(false);
|
|
140
210
|
const duplicateTarget = computed(() => workflow.value ? { id: workflow.value.id, name: workflow.value.name } : null);
|
|
141
211
|
async function onDuplicated(copy) {
|
|
142
212
|
toast.success(t("integration.editor.toast.duplicated", { name: copy.name }));
|
|
143
|
-
await
|
|
213
|
+
await leaveTo(`/integrations/workflows/${copy.id}`);
|
|
144
214
|
}
|
|
145
215
|
async function onRevisionRestored() {
|
|
146
216
|
if (!workflow.value) return;
|
|
147
217
|
await get(workflow.value.id);
|
|
148
|
-
|
|
218
|
+
draft.value = null;
|
|
149
219
|
}
|
|
150
220
|
const { trigger: triggerRun, loadForWorkflow, fetchRun, retryRun } = useIntegrationsRuns();
|
|
151
221
|
const running = ref(false);
|
|
@@ -250,7 +320,7 @@ async function run(mode) {
|
|
|
250
320
|
void loadRuns(1);
|
|
251
321
|
}
|
|
252
322
|
}
|
|
253
|
-
const payloadDriven = computed(() => payloadFieldsFromBlob(
|
|
323
|
+
const payloadDriven = computed(() => payloadFieldsFromBlob(shownBlob.value ?? { nodes: [] }).length > 0);
|
|
254
324
|
async function runFromEditor() {
|
|
255
325
|
if (!workflow.value) return;
|
|
256
326
|
if (payloadDriven.value) {
|
|
@@ -462,72 +532,32 @@ const EXECUTION_MODES = computed(() => [
|
|
|
462
532
|
{ value: "sync_only", label: t("integration.editor.executionMode.sync.label"), hint: t("integration.editor.executionMode.sync.hint") },
|
|
463
533
|
{ value: "caller_decides", label: t("integration.editor.executionMode.callerDecides.label"), hint: t("integration.editor.executionMode.callerDecides.hint") }
|
|
464
534
|
]);
|
|
465
|
-
const executionModeDraft = ref("async_only");
|
|
466
|
-
watch(workflow, (w) => {
|
|
467
|
-
if (w) executionModeDraft.value = w.execution_mode ?? "async_only";
|
|
468
|
-
}, { immediate: true });
|
|
469
|
-
const savingMode = ref(false);
|
|
470
535
|
function modeLabel(m) {
|
|
471
536
|
return m === "sync_only" ? t("integration.editor.executionMode.sync.short") : m === "caller_decides" ? t("integration.editor.executionMode.callerDecides.label") : t("integration.editor.executionMode.async.short");
|
|
472
537
|
}
|
|
473
|
-
async function saveExecutionMode(mode) {
|
|
474
|
-
if (!workflow.value || mode === workflow.value.execution_mode) return;
|
|
475
|
-
const prev = workflow.value.execution_mode ?? "async_only";
|
|
476
|
-
executionModeDraft.value = mode;
|
|
477
|
-
savingMode.value = true;
|
|
478
|
-
saveError.value = null;
|
|
479
|
-
try {
|
|
480
|
-
await update(workflow.value.id, {
|
|
481
|
-
name: workflow.value.name,
|
|
482
|
-
description: workflow.value.description,
|
|
483
|
-
blob: workflow.value.blob,
|
|
484
|
-
active: workflow.value.active,
|
|
485
|
-
executionMode: mode
|
|
486
|
-
});
|
|
487
|
-
await get(workflow.value.id);
|
|
488
|
-
} catch (e) {
|
|
489
|
-
executionModeDraft.value = prev;
|
|
490
|
-
saveError.value = e.message || t("integration.editor.executionMode.error.generic");
|
|
491
|
-
} finally {
|
|
492
|
-
savingMode.value = false;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
const savingMeta = ref(false);
|
|
496
538
|
const tagSuggestions = computed(() => {
|
|
497
539
|
const all = /* @__PURE__ */ new Set();
|
|
498
540
|
for (const wf of workflows.value) for (const t2 of wf.blob.tags ?? []) all.add(t2);
|
|
499
541
|
return [...all].sort();
|
|
500
542
|
});
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
executionMode: workflow.value.execution_mode
|
|
520
|
-
});
|
|
521
|
-
await get(workflow.value.id);
|
|
522
|
-
dirtyBlob.value = null;
|
|
523
|
-
toast.success(t("integration.editor.appearance.toast.saved"));
|
|
524
|
-
} catch (e) {
|
|
525
|
-
const msg = e.message || t("integration.editor.appearance.error.generic");
|
|
526
|
-
saveError.value = msg;
|
|
527
|
-
toast.error(msg);
|
|
528
|
-
} finally {
|
|
529
|
-
savingMeta.value = false;
|
|
530
|
-
}
|
|
543
|
+
const settingsValues = computed(() => ({
|
|
544
|
+
name: shownName.value,
|
|
545
|
+
description: shownDescription.value,
|
|
546
|
+
tags: [...shownTags.value],
|
|
547
|
+
icon: shownIcon.value,
|
|
548
|
+
executionMode: draftedMode.value
|
|
549
|
+
}));
|
|
550
|
+
function applySettings(values) {
|
|
551
|
+
const d = draftFrom();
|
|
552
|
+
if (!d) return;
|
|
553
|
+
draft.value = {
|
|
554
|
+
...d,
|
|
555
|
+
name: values.name,
|
|
556
|
+
description: values.description || null,
|
|
557
|
+
executionMode: values.executionMode,
|
|
558
|
+
tags: [...values.tags],
|
|
559
|
+
icon: values.icon
|
|
560
|
+
};
|
|
531
561
|
}
|
|
532
562
|
async function onToggleActive() {
|
|
533
563
|
const wf = workflow.value;
|
|
@@ -549,18 +579,19 @@ async function onToggleActive() {
|
|
|
549
579
|
}
|
|
550
580
|
}
|
|
551
581
|
async function save() {
|
|
552
|
-
|
|
582
|
+
const d = draft.value;
|
|
583
|
+
if (!workflow.value || !d) return;
|
|
553
584
|
saving.value = true;
|
|
554
585
|
saveError.value = null;
|
|
555
586
|
try {
|
|
556
587
|
await update(workflow.value.id, {
|
|
557
|
-
name:
|
|
558
|
-
description:
|
|
559
|
-
blob:
|
|
588
|
+
name: d.name,
|
|
589
|
+
description: d.description,
|
|
590
|
+
blob: payloadBlob(d),
|
|
560
591
|
active: workflow.value.active,
|
|
561
|
-
executionMode:
|
|
592
|
+
executionMode: d.executionMode
|
|
562
593
|
});
|
|
563
|
-
|
|
594
|
+
draft.value = null;
|
|
564
595
|
} catch (e) {
|
|
565
596
|
const issues = apiValidationIssues(e);
|
|
566
597
|
if (issues.length && workflow.value.active) {
|
|
@@ -579,21 +610,22 @@ async function save() {
|
|
|
579
610
|
}
|
|
580
611
|
}
|
|
581
612
|
async function confirmDeactivate() {
|
|
582
|
-
|
|
613
|
+
const d = draft.value;
|
|
614
|
+
if (!workflow.value || !d) {
|
|
583
615
|
deactivationOpen.value = false;
|
|
584
616
|
return;
|
|
585
617
|
}
|
|
586
618
|
saving.value = true;
|
|
587
619
|
try {
|
|
588
620
|
await update(workflow.value.id, {
|
|
589
|
-
name:
|
|
590
|
-
description:
|
|
591
|
-
blob:
|
|
621
|
+
name: d.name,
|
|
622
|
+
description: d.description,
|
|
623
|
+
blob: payloadBlob(d),
|
|
592
624
|
active: false,
|
|
593
|
-
executionMode:
|
|
625
|
+
executionMode: d.executionMode
|
|
594
626
|
});
|
|
595
627
|
await get(workflow.value.id);
|
|
596
|
-
|
|
628
|
+
draft.value = null;
|
|
597
629
|
deactivationOpen.value = false;
|
|
598
630
|
toast.success(t("integration.editor.deactivate.toast.savedPaused"));
|
|
599
631
|
} catch (e) {
|
|
@@ -619,7 +651,7 @@ function formatDateTime(iso) {
|
|
|
619
651
|
remaining height below the page header. -->
|
|
620
652
|
<div class="flex h-full flex-col">
|
|
621
653
|
<template v-if="workflow">
|
|
622
|
-
<PageHeader :title="
|
|
654
|
+
<PageHeader :title="shownName">
|
|
623
655
|
<template #title>
|
|
624
656
|
<span class="inline-flex min-w-0 items-center gap-1.5">
|
|
625
657
|
<NuxtLink to="/integrations/workflows" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary">{{ t("integration.editor.breadcrumb.workflows") }}</NuxtLink>
|
|
@@ -627,7 +659,7 @@ function formatDateTime(iso) {
|
|
|
627
659
|
<!-- Run open: the breadcrumb extends — name and Runs become crumbs
|
|
628
660
|
back into the workflow surface. -->
|
|
629
661
|
<template v-if="openRunId">
|
|
630
|
-
<button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('editor')">{{
|
|
662
|
+
<button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('editor')">{{ shownName }}</button>
|
|
631
663
|
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
632
664
|
<button type="button" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('runs')">{{ t("integration.editor.breadcrumb.runs") }}</button>
|
|
633
665
|
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
@@ -637,9 +669,15 @@ function formatDateTime(iso) {
|
|
|
637
669
|
</template>
|
|
638
670
|
<template v-else>
|
|
639
671
|
<span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
640
|
-
<WorkflowIconView :icon="
|
|
672
|
+
<WorkflowIconView :icon="shownIcon" :size="16" />
|
|
641
673
|
</span>
|
|
642
|
-
<span class="truncate">{{
|
|
674
|
+
<span class="truncate">{{ shownName }}</span>
|
|
675
|
+
<!-- The SAVED mode, beside a drafted name, and the difference is not an
|
|
676
|
+
oversight. A name changes nothing about what happens when Run is
|
|
677
|
+
pressed, so the header may show what the operator just entered. The
|
|
678
|
+
dispatch mode decides what a run does — and a run goes to the
|
|
679
|
+
workflow the platform holds, not to the draft. A badge following the
|
|
680
|
+
draft would read SYNC over a button that fires an async run. -->
|
|
643
681
|
<Badge variant="outline" class="ml-1.5 border text-xs font-medium uppercase text-muted-foreground" :title="t('integration.editor.header.executionMode.title', { mode: modeLabel(workflow.execution_mode) })">
|
|
644
682
|
{{ modeLabel(workflow.execution_mode) }}
|
|
645
683
|
</Badge>
|
|
@@ -707,17 +745,37 @@ function formatDateTime(iso) {
|
|
|
707
745
|
<Save v-else :size="12" />
|
|
708
746
|
{{ saving ? t("integration.editor.action.save.saving") : t("integration.editor.action.save.label") }}
|
|
709
747
|
</Button>
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
748
|
+
<!-- Activation is a standing state, not a transport control. The play
|
|
749
|
+
symbol belongs to Run, 600 px away in the canvas's control pane,
|
|
750
|
+
where pressing it makes one thing happen now; arming a workflow's
|
|
751
|
+
triggers looks like nothing happening at all. So this states the
|
|
752
|
+
state in the words the workflows list uses — Active, Paused — and
|
|
753
|
+
the switch beside it is the flip (PO-254).
|
|
754
|
+
|
|
755
|
+
The blocked reason has two homes because a disabled control shows
|
|
756
|
+
no native tooltip in Chrome: the title sits on the wrapper, which
|
|
757
|
+
is not disabled, and `aria-describedby` puts the same sentence on
|
|
758
|
+
the switch for anyone who never hovers. Sighted users also have
|
|
759
|
+
the amber banner directly below, which says it at length. -->
|
|
760
|
+
<div
|
|
761
|
+
class="flex h-7 items-center gap-2 rounded-md border border-black/[0.08] px-2"
|
|
715
762
|
:title="workflow.active ? t('integration.editor.action.pause.title') : isInvalid ? t('integration.editor.action.activate.title.invalid', validationWarnings?.length ?? 0) : t('integration.editor.action.activate.title')"
|
|
716
|
-
@click="onToggleActive"
|
|
717
763
|
>
|
|
718
|
-
<
|
|
719
|
-
|
|
720
|
-
|
|
764
|
+
<Switch
|
|
765
|
+
:aria-label="t('integration.editor.action.state.label')"
|
|
766
|
+
:aria-describedby="!workflow.active && isInvalid ? 'wf-activation-blocked' : void 0"
|
|
767
|
+
class="data-[state=checked]:bg-emerald-600 data-[state=unchecked]:bg-slate-300"
|
|
768
|
+
:model-value="workflow.active"
|
|
769
|
+
:disabled="!workflow.active && isInvalid"
|
|
770
|
+
@update:model-value="onToggleActive"
|
|
771
|
+
/>
|
|
772
|
+
<span class="text-sm" :class="workflow.active ? 'font-medium text-emerald-700' : 'text-muted-foreground'">
|
|
773
|
+
{{ workflow.active ? t("integration.workflows.table.status.lifecycle.active") : t("integration.workflows.table.status.lifecycle.paused") }}
|
|
774
|
+
</span>
|
|
775
|
+
<span v-if="!workflow.active && isInvalid" id="wf-activation-blocked" class="sr-only">
|
|
776
|
+
{{ t("integration.editor.action.activate.title.invalid", validationWarnings?.length ?? 0) }}
|
|
777
|
+
</span>
|
|
778
|
+
</div>
|
|
721
779
|
</template>
|
|
722
780
|
</template>
|
|
723
781
|
|
|
@@ -739,8 +797,8 @@ function formatDateTime(iso) {
|
|
|
739
797
|
</div>
|
|
740
798
|
|
|
741
799
|
<!-- Manifest validation (PO-186): an inactive workflow with warnings can't
|
|
742
|
-
be activated. The banner names what's incomplete; the
|
|
743
|
-
stays
|
|
800
|
+
be activated. The banner names what's incomplete; the header's state
|
|
801
|
+
switch stays blocked until the next save recomputes an empty warnings set.
|
|
744
802
|
Amber (not red) — the work is saved fine, it just isn't runnable yet.
|
|
745
803
|
One line per NODE, listing its fields (PO-209): the messages behind
|
|
746
804
|
them ride along as the line's title, and the line leads to the node. -->
|
|
@@ -823,9 +881,10 @@ function formatDateTime(iso) {
|
|
|
823
881
|
:key="editorResetKey"
|
|
824
882
|
ref="editorRef"
|
|
825
883
|
:workflow="workflow"
|
|
884
|
+
:draft="draft?.blob ?? null"
|
|
826
885
|
:run-busy="running || syncLoading || waitingForBuild"
|
|
827
886
|
:run-waiting="waitingForBuild"
|
|
828
|
-
:overlay-open="settingsOpen || duplicateOpen || syncOpen || runPayloadOpen || deactivationOpen"
|
|
887
|
+
:overlay-open="settingsOpen || duplicateOpen || syncOpen || runPayloadOpen || deactivationOpen || leaveOpen"
|
|
829
888
|
@change="onChange"
|
|
830
889
|
@run="runFromEditor"
|
|
831
890
|
@toggle-active="onToggleActive"
|
|
@@ -870,7 +929,7 @@ function formatDateTime(iso) {
|
|
|
870
929
|
|
|
871
930
|
<!-- History — n8n-style: read-only canvas preview + revision list. -->
|
|
872
931
|
<div v-else-if="tab === 'history'" class="flex-1 min-h-0">
|
|
873
|
-
<WorkflowHistoryPanel :workflow="workflow" @restored="onRevisionRestored" />
|
|
932
|
+
<WorkflowHistoryPanel :workflow="workflow" :draft-pending="isDirty" @restored="onRevisionRestored" />
|
|
874
933
|
</div>
|
|
875
934
|
</template>
|
|
876
935
|
|
|
@@ -971,16 +1030,46 @@ function formatDateTime(iso) {
|
|
|
971
1030
|
v-model:open="settingsOpen"
|
|
972
1031
|
:workflow="workflow"
|
|
973
1032
|
:modes="EXECUTION_MODES"
|
|
974
|
-
:
|
|
975
|
-
:saving-mode="savingMode"
|
|
976
|
-
:saving-meta="savingMeta"
|
|
1033
|
+
:current="settingsValues"
|
|
977
1034
|
:tag-suggestions="tagSuggestions"
|
|
978
|
-
@
|
|
979
|
-
@save-meta="saveWorkflowMeta"
|
|
1035
|
+
@apply="applySettings"
|
|
980
1036
|
/>
|
|
981
1037
|
|
|
982
1038
|
<WorkflowDuplicateDialog v-model:open="duplicateOpen" :workflow="duplicateTarget" @duplicated="onDuplicated" />
|
|
983
1039
|
|
|
1040
|
+
<!-- Leaving the workflow with unsaved work.
|
|
1041
|
+
Staying is the default in the only sense that decides anything: it holds the
|
|
1042
|
+
focus when the dialog opens, so Enter or Space keeps the work. That is what
|
|
1043
|
+
`AlertDialogCancel` is for — the dialog focuses its cancel element, and with
|
|
1044
|
+
none declared it focuses the first tabbable button instead, which was
|
|
1045
|
+
"Discard and leave". A breadcrumb click followed by Enter then threw away the
|
|
1046
|
+
afternoon this dialog exists to protect.
|
|
1047
|
+
`as-child` so the studio's own Button is that element rather than a second one
|
|
1048
|
+
beside it. Escape means the same thing, through `update:open`; a click beside
|
|
1049
|
+
the dialog does nothing at all, which an alert dialog is entitled to. -->
|
|
1050
|
+
<AlertDialog :open="leaveOpen" @update:open="(v) => {
|
|
1051
|
+
if (!v) answerLeave(false);
|
|
1052
|
+
}">
|
|
1053
|
+
<AlertDialogContent>
|
|
1054
|
+
<AlertDialogHeader>
|
|
1055
|
+
<AlertDialogTitle>{{ t("integration.editor.leave.dialog.title") }}</AlertDialogTitle>
|
|
1056
|
+
<AlertDialogDescription>
|
|
1057
|
+
{{ t("integration.editor.leave.dialog.description") }}
|
|
1058
|
+
</AlertDialogDescription>
|
|
1059
|
+
</AlertDialogHeader>
|
|
1060
|
+
<AlertDialogFooter>
|
|
1061
|
+
<Button variant="outline" @click="answerLeave(true)">
|
|
1062
|
+
{{ t("integration.editor.leave.dialog.discard") }}
|
|
1063
|
+
</Button>
|
|
1064
|
+
<AlertDialogCancel as-child>
|
|
1065
|
+
<Button @click="answerLeave(false)">
|
|
1066
|
+
{{ t("integration.editor.leave.dialog.stay") }}
|
|
1067
|
+
</Button>
|
|
1068
|
+
</AlertDialogCancel>
|
|
1069
|
+
</AlertDialogFooter>
|
|
1070
|
+
</AlertDialogContent>
|
|
1071
|
+
</AlertDialog>
|
|
1072
|
+
|
|
984
1073
|
<!-- Active-save blocked by manifest validation: keep the work by saving it
|
|
985
1074
|
as paused. Cancelling persists nothing (the 422 already rolled back). -->
|
|
986
1075
|
<AlertDialog :open="deactivationOpen" @update:open="(v) => {
|
|
@@ -318,7 +318,24 @@ export interface NodeManifest {
|
|
|
318
318
|
title?: LocalizedString;
|
|
319
319
|
category?: string;
|
|
320
320
|
}>;
|
|
321
|
-
|
|
321
|
+
/**
|
|
322
|
+
* The named ports a node accepts data on, keyed by port name — the mirror of
|
|
323
|
+
* `outputs`.
|
|
324
|
+
*
|
|
325
|
+
* **A node that accepts nothing arrives as `[]`, not `{}`**, which is why the
|
|
326
|
+
* empty array is in the type: the service encodes an empty PHP array as a JSON
|
|
327
|
+
* array, so the empty case is a different JSON *type* from the populated one. It
|
|
328
|
+
* is `never[]` rather than `NodeManifestInput[]` because a *populated* array is
|
|
329
|
+
* not a shape the service produces and not one anything here would read — only
|
|
330
|
+
* `[]` is real. Read the key through `inputPortsForManifest`, which handles both
|
|
331
|
+
* forms once.
|
|
332
|
+
*
|
|
333
|
+
* **Optional here, required there.** Every published manifest carries it — the
|
|
334
|
+
* service's schema lists it among the required keys — but the studio also builds
|
|
335
|
+
* manifest objects in tests and has to tolerate one that predates a field or
|
|
336
|
+
* omits it, so the type does not claim more than the studio can rely on.
|
|
337
|
+
*/
|
|
338
|
+
inputs?: Record<string, NodeManifestInput> | never[];
|
|
322
339
|
/** Empty marks a terminal node (PO-201): the run ends there, so the canvas
|
|
323
340
|
* draws no output handle and no edge may leave it. */
|
|
324
341
|
outputs: NodeManifestOutput[];
|
|
@@ -328,6 +345,13 @@ export interface NodeManifest {
|
|
|
328
345
|
name: string;
|
|
329
346
|
label: string | null;
|
|
330
347
|
};
|
|
348
|
+
/**
|
|
349
|
+
* Whether this version's code was built and can therefore be executed
|
|
350
|
+
* (editor-side enrichment from `GET /nodes`; not manifest data). Absent means
|
|
351
|
+
* an API that does not report it — treated as executable, so an older host
|
|
352
|
+
* does not make every node disappear.
|
|
353
|
+
*/
|
|
354
|
+
__hasBundle?: boolean;
|
|
331
355
|
}
|
|
332
356
|
export interface SecretRecord {
|
|
333
357
|
key: string;
|
|
@@ -40,9 +40,18 @@
|
|
|
40
40
|
* That is measured, not read off the regex: unimport's own `injectImports`, run
|
|
41
41
|
* over this file, injects all five — and over the same file with the declarations
|
|
42
42
|
* rewritten as `declare function`, injects nothing. Repeat it that way if the
|
|
43
|
-
* spelling ever has to change.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* spelling ever has to change.
|
|
44
|
+
*
|
|
45
|
+
* **Since PO-271 something does stand guard over it, incidentally rather than by
|
|
46
|
+
* design.** The end-to-end suite runs against a host that auto-imports the same five
|
|
47
|
+
* names (`playground/app/composables/cockpitFakes.ts`), so the injection has to work
|
|
48
|
+
* *there* for `specs/credential-managed-source.md` AC-2 to pass at all: rewrite these
|
|
49
|
+
* as `declare function`, and unimport skips the injection in the playground exactly
|
|
50
|
+
* as it would in the cockpit, all five names are undefined, the panel disables
|
|
51
|
+
* itself, and AC-2 goes red. AC-1 would stay green, which is why this is worth
|
|
52
|
+
* writing down — the half of the spec that is about a host *without* the sources
|
|
53
|
+
* cannot see the difference. There is still no test asserting the build tool's
|
|
54
|
+
* behaviour directly, and there deliberately is not one.
|
|
46
55
|
*/
|
|
47
56
|
/**
|
|
48
57
|
* The shapes this module uses.
|
|
@@ -148,6 +157,16 @@ export interface CockpitMailCredential {
|
|
|
148
157
|
* says nothing about why. `null` is what the caller turns into "the managed
|
|
149
158
|
* sources are unavailable here", with the manual fields left in place.
|
|
150
159
|
*
|
|
160
|
+
* **This is not a test for being inside a cockpit, and PO-223 learned that the hard
|
|
161
|
+
* way.** The dashboard read `!== null` as "am I in a cockpit" for its four shortcuts
|
|
162
|
+
* into host-owned surfaces, and the standalone dev studio answered yes: it provides
|
|
163
|
+
* all five names as inert stand-ins on purpose
|
|
164
|
+
* (`integrations/studio-ui/app/composables/consoleStubs.ts`) so that the managed
|
|
165
|
+
* credential panels render their empty states rather than nothing. What this answers
|
|
166
|
+
* is whether the five names are in scope — exactly what the managed sources need to
|
|
167
|
+
* know, and nothing beyond it. A caller asking anything else has to ask its own
|
|
168
|
+
* question; that dashboard now asks the router.
|
|
169
|
+
*
|
|
151
170
|
* Must be called from a component's setup, like the composables it calls.
|
|
152
171
|
*/
|
|
153
172
|
export declare function cockpitSources(): CockpitSources | null;
|
|
@@ -16,7 +16,12 @@ export interface CredentialStateInfo {
|
|
|
16
16
|
label: string;
|
|
17
17
|
/** Badge/pill classes for this state. */
|
|
18
18
|
toneClass: string;
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* What the last test said, whether it passed or failed. A credential type that
|
|
21
|
+
* reached something can name it — which host, which environment, which plan —
|
|
22
|
+
* and that is exactly what an operator cannot see anywhere else once a node
|
|
23
|
+
* stops asking them to configure it.
|
|
24
|
+
*/
|
|
20
25
|
message?: string | null;
|
|
21
26
|
/** When the underlying result was recorded (ISO). */
|
|
22
27
|
at?: string | null;
|
|
@@ -23,7 +23,7 @@ export function credentialState(c) {
|
|
|
23
23
|
return { key: "needs_reauth", label: t(MESSAGE_KEY.needs_reauth), toneClass: TONE.needs_reauth, at: c.last_test_at };
|
|
24
24
|
}
|
|
25
25
|
if (c.last_test_ok === true) {
|
|
26
|
-
return { key: "connected", label: t(MESSAGE_KEY.connected), toneClass: TONE.connected, at: c.last_test_at };
|
|
26
|
+
return { key: "connected", label: t(MESSAGE_KEY.connected), toneClass: TONE.connected, message: c.last_test_message, at: c.last_test_at };
|
|
27
27
|
}
|
|
28
28
|
if (c.last_test_ok === false) {
|
|
29
29
|
return { key: "failed", label: t(MESSAGE_KEY.failed), toneClass: TONE.failed, message: c.last_test_message, at: c.last_test_at };
|