@revenexx/studio-integrations 1.3.0 → 1.4.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 +426 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue +53 -0
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/EditorTip.vue +2 -15
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +10 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +94 -49
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +10 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +37 -60
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
- package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
- package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +165 -56
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
- package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
- package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
- package/dist/runtime/composables/useCredentialTypes.js +3 -9
- package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
- package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
- package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
- package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
- package/dist/runtime/composables/useIntegrationsTemplates.js +5 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -7
- package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
- package/dist/runtime/data/canvasShortcuts.js +68 -0
- package/dist/runtime/i18n/messages/de.json +30 -16
- package/dist/runtime/i18n/messages/en.json +30 -16
- package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
- package/dist/runtime/pages/integrations/workflows/[id].vue +128 -33
- package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
- package/dist/runtime/utils/apiEnvelope.js +20 -0
- package/dist/runtime/utils/configDefaults.d.ts +18 -0
- package/dist/runtime/utils/configDefaults.js +7 -0
- package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
- package/dist/runtime/utils/errorPathAdvice.js +19 -0
- package/dist/runtime/utils/notify.d.ts +2 -2
- package/dist/runtime/utils/runEnvelope.d.ts +12 -0
- package/dist/runtime/utils/runEnvelope.js +2 -1
- package/dist/runtime/utils/selectOptions.d.ts +48 -0
- package/dist/runtime/utils/selectOptions.js +19 -0
- package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
- package/dist/runtime/utils/shortcutLabel.js +48 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed,
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
3
|
import { marked } from "marked";
|
|
4
4
|
import { AltArrowRight as Chevron, CheckCircle as CheckCircle2, Key, RefreshCircle as Loader2, Shield as ShieldAlert, User } from "#solar-icons";
|
|
5
5
|
import { triggerGlyph as glyphForTrigger } from "../../../data/triggerGlyphs";
|
|
@@ -9,27 +9,19 @@ import {
|
|
|
9
9
|
Dialog,
|
|
10
10
|
DialogContent,
|
|
11
11
|
DialogDescription,
|
|
12
|
-
DialogFooter,
|
|
13
12
|
DialogHeader,
|
|
14
|
-
DialogTitle
|
|
15
|
-
Input,
|
|
16
|
-
Label,
|
|
17
|
-
Select,
|
|
18
|
-
SelectContent,
|
|
19
|
-
SelectItem,
|
|
20
|
-
SelectTrigger,
|
|
21
|
-
SelectValue
|
|
13
|
+
DialogTitle
|
|
22
14
|
} from "@revenexx/studio";
|
|
23
15
|
import TemplateFlowPreview from "../../../components/Integrations/TemplateFlowPreview.vue";
|
|
16
|
+
import TemplateInstallStep from "../../../components/Integrations/TemplateInstallStep.vue";
|
|
17
|
+
import LoadingAnnouncement from "../../../components/Integrations/LoadingAnnouncement.vue";
|
|
24
18
|
import NodeMark from "../../../components/Integrations/NodeMark.vue";
|
|
25
19
|
import { templateCategoryLabel, templatePackageLabel } from "../../../data/integrationTemplates";
|
|
26
20
|
import { describeCron } from "../../../utils/schedule";
|
|
27
|
-
import { errorReason } from "../../../utils/apiError";
|
|
28
|
-
const SELECT_LATER = "__later__";
|
|
29
21
|
const t = useIntegrationsT();
|
|
30
22
|
const route = useRoute();
|
|
31
23
|
const router = useRouter();
|
|
32
|
-
const { get, load, loaded, fetchDetail, requirements
|
|
24
|
+
const { get, load, loaded, fetchDetail, requirements } = useIntegrationsTemplates();
|
|
33
25
|
const { load: loadNodeCatalog, findManifest, resolveLocalized } = useIntegrationsNodes();
|
|
34
26
|
const { keys: secretKeys, load: loadSecrets } = useIntegrationsSecrets();
|
|
35
27
|
const { ofType, load: loadCredentials } = useIntegrationsCredentials();
|
|
@@ -137,39 +129,13 @@ const setupReady = computed(() => missingSecrets.value === 0 && missingCredentia
|
|
|
137
129
|
const packageLabel = computed(() => tpl.value ? templatePackageLabel(tpl.value) : null);
|
|
138
130
|
const categoryLabel = computed(() => templateCategoryLabel(tpl.value?.category ?? ""));
|
|
139
131
|
const installOpen = ref(false);
|
|
140
|
-
const
|
|
141
|
-
const installError = ref(null);
|
|
142
|
-
const name = ref("");
|
|
143
|
-
const bindings = reactive({});
|
|
144
|
-
function nodesFor(usedBy) {
|
|
145
|
-
return [...new Set(usedBy.map((u) => u.nodeId))];
|
|
146
|
-
}
|
|
132
|
+
const stepLoading = ref(false);
|
|
147
133
|
function openInstall() {
|
|
148
134
|
if (!tpl.value) return;
|
|
149
|
-
name.value = tpl.value.name;
|
|
150
|
-
for (const k of Object.keys(bindings)) delete bindings[k];
|
|
151
|
-
installError.value = null;
|
|
152
135
|
installOpen.value = true;
|
|
153
136
|
}
|
|
154
|
-
async function
|
|
155
|
-
|
|
156
|
-
installing.value = true;
|
|
157
|
-
installError.value = null;
|
|
158
|
-
try {
|
|
159
|
-
const credentialBindings = {};
|
|
160
|
-
for (const [nodeId, credId] of Object.entries(bindings)) {
|
|
161
|
-
if (credId && credId !== SELECT_LATER) credentialBindings[nodeId] = credId;
|
|
162
|
-
}
|
|
163
|
-
const id = await instantiate(tpl.value.slug, {
|
|
164
|
-
name: name.value.trim() || void 0,
|
|
165
|
-
credentialBindings
|
|
166
|
-
});
|
|
167
|
-
if (id) await router.push(`/integrations/workflows/${id}`);
|
|
168
|
-
} catch (e) {
|
|
169
|
-
installError.value = errorReason(e, t("integration.templates.detail.install.error.fallback"));
|
|
170
|
-
} finally {
|
|
171
|
-
installing.value = false;
|
|
172
|
-
}
|
|
137
|
+
async function installed(id) {
|
|
138
|
+
await router.push(`/integrations/workflows/${id}`);
|
|
173
139
|
}
|
|
174
140
|
const PUBLISHER_KIND_LABEL_KEY = {
|
|
175
141
|
revenexx: "integration.templates.detail.publisher.kind.revenexx",
|
|
@@ -471,7 +437,13 @@ function levelDot(level) {
|
|
|
471
437
|
{{ t("integration.templates.detail.state.notFound") }}
|
|
472
438
|
</div>
|
|
473
439
|
|
|
474
|
-
|
|
440
|
+
<LoadingAnnouncement
|
|
441
|
+
:label="t('integration.templates.detail.action.useTemplate')"
|
|
442
|
+
:loading="stepLoading"
|
|
443
|
+
:text="t('integration.templates.detail.install.state.loading')"
|
|
444
|
+
/>
|
|
445
|
+
|
|
446
|
+
<!-- Install: the shared step, in this page's dialog ------------------- -->
|
|
475
447
|
<Dialog v-model:open="installOpen">
|
|
476
448
|
<DialogContent class="sm:max-w-lg">
|
|
477
449
|
<DialogHeader>
|
|
@@ -481,95 +453,13 @@ function levelDot(level) {
|
|
|
481
453
|
</DialogDescription>
|
|
482
454
|
</DialogHeader>
|
|
483
455
|
|
|
484
|
-
<
|
|
485
|
-
<
|
|
486
|
-
<
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
<div class="space-y-2">
|
|
492
|
-
<div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
|
|
493
|
-
{{ t("integration.templates.detail.credentials.title") }}
|
|
494
|
-
</div>
|
|
495
|
-
<p v-if="!credentialReqs.length" class="text-sm text-muted-foreground italic">
|
|
496
|
-
{{ t("integration.templates.detail.install.noCredentials") }}
|
|
497
|
-
</p>
|
|
498
|
-
<div v-for="req in credentialReqs" :key="req.slug" class="space-y-2">
|
|
499
|
-
<div class="flex items-center gap-2">
|
|
500
|
-
<span class="text-sm font-medium">{{ req.name ?? req.slug }}</span>
|
|
501
|
-
<Badge v-if="!req.registered" variant="outline" class="bg-rose-500/10 border-rose-500/20 text-rose-700 text-sm">
|
|
502
|
-
{{ t("integration.templates.detail.install.notRegistered") }}
|
|
503
|
-
</Badge>
|
|
504
|
-
<Badge v-else-if="!ofType(req.slug).length" variant="outline" class="bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm">
|
|
505
|
-
{{ t("integration.templates.detail.install.noneYet") }}
|
|
506
|
-
</Badge>
|
|
507
|
-
</div>
|
|
508
|
-
|
|
509
|
-
<p v-if="!ofType(req.slug).length" class="rounded-md border-l-2 border-black/[0.08] bg-muted/30 px-2.5 py-1.5 text-sm text-muted-foreground">
|
|
510
|
-
{{ t("integration.templates.detail.install.noCredentialsOfType") }}
|
|
511
|
-
<NuxtLink to="/integrations/credentials" class="text-primary hover:underline">{{ t("integration.templates.detail.install.createOneLink") }}</NuxtLink>
|
|
512
|
-
{{ t("integration.templates.detail.install.orInstallLater") }}
|
|
513
|
-
</p>
|
|
514
|
-
<div v-for="nodeId in nodesFor(req.usedBy)" v-else :key="nodeId" class="space-y-1">
|
|
515
|
-
<Label :for="`tpl-cred-${nodeId}`" class="text-xs text-muted-foreground font-mono">{{ t("integration.templates.detail.install.nodeLabel", { id: nodeId }) }}</Label>
|
|
516
|
-
<Select
|
|
517
|
-
:model-value="bindings[nodeId] ?? SELECT_LATER"
|
|
518
|
-
@update:model-value="(v2) => {
|
|
519
|
-
bindings[nodeId] = v2 ?? SELECT_LATER;
|
|
520
|
-
}"
|
|
521
|
-
>
|
|
522
|
-
<SelectTrigger :id="`tpl-cred-${nodeId}`" class="bg-white">
|
|
523
|
-
<SelectValue :placeholder="t('integration.templates.detail.install.selectLater')" />
|
|
524
|
-
</SelectTrigger>
|
|
525
|
-
<SelectContent>
|
|
526
|
-
<SelectItem :value="SELECT_LATER">{{ t("integration.templates.detail.install.selectLater") }}</SelectItem>
|
|
527
|
-
<SelectItem v-for="c in ofType(req.slug)" :key="c.id" :value="c.id">
|
|
528
|
-
{{ c.name }}
|
|
529
|
-
</SelectItem>
|
|
530
|
-
</SelectContent>
|
|
531
|
-
</Select>
|
|
532
|
-
</div>
|
|
533
|
-
</div>
|
|
534
|
-
</div>
|
|
535
|
-
|
|
536
|
-
<!-- Secrets -->
|
|
537
|
-
<div v-if="secretChecks.length" class="space-y-2">
|
|
538
|
-
<div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
|
|
539
|
-
{{ t("integration.templates.detail.install.secretsHeader") }}
|
|
540
|
-
</div>
|
|
541
|
-
<p class="text-sm text-muted-foreground">
|
|
542
|
-
{{ t("integration.templates.detail.install.secretsDescription") }}
|
|
543
|
-
<NuxtLink to="/integrations/secrets" class="text-primary hover:underline">{{ t("integration.nav.item.secrets") }}</NuxtLink> {{ t("integration.templates.detail.install.secretsDescriptionSuffix") }}
|
|
544
|
-
</p>
|
|
545
|
-
<ul class="space-y-1">
|
|
546
|
-
<li
|
|
547
|
-
v-for="(s, i) in secretChecks"
|
|
548
|
-
:key="s.key ?? `dlg-secret-${i}`"
|
|
549
|
-
class="flex items-center justify-between rounded-md border border-black/[0.06] bg-muted/20 px-2 py-1 text-sm"
|
|
550
|
-
>
|
|
551
|
-
<code class="font-mono">{{ s.label }}</code>
|
|
552
|
-
<Badge variant="outline" :class="s.present ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-700 text-sm' : 'bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm'">
|
|
553
|
-
{{ s.present ? t("integration.templates.detail.install.secretStored") : t("integration.templates.detail.secrets.missing") }}
|
|
554
|
-
</Badge>
|
|
555
|
-
</li>
|
|
556
|
-
</ul>
|
|
557
|
-
</div>
|
|
558
|
-
|
|
559
|
-
<div v-if="installError" class="rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
|
|
560
|
-
{{ installError }}
|
|
561
|
-
</div>
|
|
562
|
-
</div>
|
|
563
|
-
|
|
564
|
-
<DialogFooter>
|
|
565
|
-
<Button variant="outline" :disabled="installing" @click="installOpen = false">
|
|
566
|
-
{{ t("integration.common.action.cancel") }}
|
|
567
|
-
</Button>
|
|
568
|
-
<Button :disabled="installing || !name.trim()" @click="confirmInstall">
|
|
569
|
-
<Loader2 v-if="installing" :size="14" class="mr-1 animate-spin" />
|
|
570
|
-
{{ installing ? t("integration.templates.detail.install.submitting") : t("integration.templates.detail.install.submit") }}
|
|
571
|
-
</Button>
|
|
572
|
-
</DialogFooter>
|
|
456
|
+
<TemplateInstallStep :slug="slug" @installed="installed" @update:loading="stepLoading = $event">
|
|
457
|
+
<template #secondary="{ installing }">
|
|
458
|
+
<Button variant="outline" :disabled="installing" @click="installOpen = false">
|
|
459
|
+
{{ t("integration.common.action.cancel") }}
|
|
460
|
+
</Button>
|
|
461
|
+
</template>
|
|
462
|
+
</TemplateInstallStep>
|
|
573
463
|
</DialogContent>
|
|
574
464
|
</Dialog>
|
|
575
465
|
</IntegrationsLayout>
|
|
@@ -46,6 +46,8 @@ 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";
|
|
49
51
|
import { payloadFieldsFromBlob } from "../../../data/payloadForm";
|
|
50
52
|
import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
|
|
51
53
|
const t = useIntegrationsT();
|
|
@@ -108,6 +110,27 @@ async function revealNode(nodeId) {
|
|
|
108
110
|
await nextTick();
|
|
109
111
|
editorRef.value?.revealNode(nodeId);
|
|
110
112
|
}
|
|
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
|
+
}
|
|
111
134
|
const { load: loadNodeCatalog } = useIntegrationsNodes();
|
|
112
135
|
onMounted(() => {
|
|
113
136
|
void loadNodeCatalog();
|
|
@@ -177,6 +200,7 @@ watch(id, async () => {
|
|
|
177
200
|
if (!w) await leaveTo("/integrations/workflows");
|
|
178
201
|
draft.value = null;
|
|
179
202
|
warningsExpanded.value = false;
|
|
203
|
+
adviceExpanded.value = false;
|
|
180
204
|
if (tab.value === "runs") void loadRuns(1);
|
|
181
205
|
settleBuildStatus();
|
|
182
206
|
}, { immediate: true });
|
|
@@ -369,7 +393,7 @@ async function run(mode) {
|
|
|
369
393
|
const payloadDriven = computed(() => payloadFieldsFromBlob(shownBlob.value ?? { nodes: [] }).length > 0);
|
|
370
394
|
async function runFromEditor() {
|
|
371
395
|
if (!workflow.value) return;
|
|
372
|
-
if (payloadDriven.value) {
|
|
396
|
+
if (payloadDriven.value || workflow.value.execution_mode === "caller_decides") {
|
|
373
397
|
runPayloadOpen.value = true;
|
|
374
398
|
return;
|
|
375
399
|
}
|
|
@@ -626,16 +650,20 @@ function closeRun(target) {
|
|
|
626
650
|
tab.value = target;
|
|
627
651
|
void router.replace({ query: { ...route.query, run: void 0, view: void 0 } });
|
|
628
652
|
}
|
|
629
|
-
const canRetryOpenRun = computed(() =>
|
|
653
|
+
const canRetryOpenRun = computed(() => openRun.value?.status === "failed");
|
|
630
654
|
const retryBusy = ref(false);
|
|
631
655
|
async function retryOpenRun() {
|
|
632
|
-
|
|
656
|
+
const source = openRun.value;
|
|
657
|
+
if (!source || retryBusy.value) return;
|
|
633
658
|
retryBusy.value = true;
|
|
634
659
|
try {
|
|
635
|
-
const res = await retryRun(
|
|
660
|
+
const res = await retryRun(source.workflowId, source.id);
|
|
636
661
|
const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
|
|
637
|
-
toast.success(newId && newId !==
|
|
662
|
+
toast.success(newId && newId !== source.id ? t("integration.editor.retry.toast.started", { id: newId }) : t("integration.editor.retry.toast.requested"));
|
|
638
663
|
void loadRuns(1, { silent: true });
|
|
664
|
+
if (newId && newId !== source.id) {
|
|
665
|
+
await router.push({ query: { ...route.query, run: newId, view: void 0 } });
|
|
666
|
+
}
|
|
639
667
|
} catch (e) {
|
|
640
668
|
toast.error(errorReason(e, t("integration.editor.retry.error.generic")));
|
|
641
669
|
} finally {
|
|
@@ -770,32 +798,32 @@ function formatDateTime(iso) {
|
|
|
770
798
|
<template v-if="workflow">
|
|
771
799
|
<PageHeader :title="shownName">
|
|
772
800
|
<template #title>
|
|
773
|
-
<span class="
|
|
774
|
-
<NuxtLink to="/integrations/workflows" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary">{{ t("integration.editor.breadcrumb.workflows") }}</NuxtLink>
|
|
775
|
-
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
801
|
+
<span class="flex min-w-0 items-center gap-1.5">
|
|
802
|
+
<NuxtLink to="/integrations/workflows" class="shrink-0 font-normal text-muted-foreground/80 transition-colors hover:text-primary">{{ t("integration.editor.breadcrumb.workflows") }}</NuxtLink>
|
|
803
|
+
<Chevron :size="15" weight="Outline" class="shrink-0 text-muted-foreground/50" />
|
|
776
804
|
<!-- Run open: the breadcrumb extends — name and Runs become crumbs
|
|
777
805
|
back into the workflow surface. -->
|
|
778
806
|
<template v-if="openRunId">
|
|
779
|
-
<button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('editor')">{{ shownName }}</button>
|
|
780
|
-
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
781
|
-
<button type="button" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('runs')">{{ t("integration.editor.breadcrumb.runs") }}</button>
|
|
782
|
-
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
807
|
+
<button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" :title="shownName" @click="closeRun('editor')">{{ shownName }}</button>
|
|
808
|
+
<Chevron :size="15" weight="Outline" class="shrink-0 text-muted-foreground/50" />
|
|
809
|
+
<button type="button" class="shrink-0 font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('runs')">{{ t("integration.editor.breadcrumb.runs") }}</button>
|
|
810
|
+
<Chevron :size="15" weight="Outline" class="shrink-0 text-muted-foreground/50" />
|
|
783
811
|
<!-- The Temporal run id IS the run's identity; the internal
|
|
784
812
|
number only fills in until the run is handed off. -->
|
|
785
|
-
<span class="
|
|
813
|
+
<span class="shrink-0 font-mono">{{ openRun?.temporalRunId ?? t("integration.editor.breadcrumb.runNumber", { id: openRunId }) }}</span>
|
|
786
814
|
</template>
|
|
787
815
|
<template v-else>
|
|
788
816
|
<span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
789
817
|
<WorkflowIconView :icon="shownIcon" :size="16" />
|
|
790
818
|
</span>
|
|
791
|
-
<span class="truncate">{{ shownName }}</span>
|
|
819
|
+
<span class="truncate" :title="shownName">{{ shownName }}</span>
|
|
792
820
|
<!-- The SAVED mode, beside a drafted name, and the difference is not an
|
|
793
821
|
oversight. A name changes nothing about what happens when Run is
|
|
794
822
|
pressed, so the header may show what the operator just entered. The
|
|
795
823
|
dispatch mode decides what a run does — and a run goes to the
|
|
796
824
|
workflow the platform holds, not to the draft. A badge following the
|
|
797
825
|
draft would read SYNC over a button that fires an async run. -->
|
|
798
|
-
<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) })">
|
|
826
|
+
<Badge variant="outline" class="ml-1.5 shrink-0 border text-xs font-medium uppercase text-muted-foreground" :title="t('integration.editor.header.executionMode.title', { mode: modeLabel(workflow.execution_mode) })">
|
|
799
827
|
{{ modeLabel(workflow.execution_mode) }}
|
|
800
828
|
</Badge>
|
|
801
829
|
</template>
|
|
@@ -812,25 +840,33 @@ function formatDateTime(iso) {
|
|
|
812
840
|
outside it. Leftmost there is as near the rail as the shared header
|
|
813
841
|
allows without a slot of its own.
|
|
814
842
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
843
|
+
The way back out carries its words; the way in does not. An
|
|
844
|
+
icon-only control is one nobody finds, and a folded rail is the one
|
|
845
|
+
state where that is unaffordable — nothing else on the page offers
|
|
846
|
+
the navigation back. Folding it away needs no such help: the rail
|
|
847
|
+
is standing right beside the pictogram, and the words cost 159 px
|
|
848
|
+
of a header row that a long workflow name already fills, where they
|
|
849
|
+
were the widest thing in it for the least important thing in it.
|
|
850
|
+
Icon-only, so it carries the same name as before to a screen reader
|
|
851
|
+
and to a test. Rendered only where the canvas fills the page, the
|
|
852
|
+
one surface it acts on. -->
|
|
819
853
|
<Button
|
|
820
854
|
v-if="canvasFillsPage"
|
|
821
855
|
variant="ghost"
|
|
822
856
|
size="sm"
|
|
823
|
-
class="h-7 shrink-0 gap-1.5
|
|
857
|
+
class="h-7 shrink-0 gap-1.5 text-sm font-normal text-muted-foreground hover:text-primary"
|
|
858
|
+
:class="railFolded ? 'px-2' : 'w-7 px-0'"
|
|
859
|
+
:aria-label="railFolded ? void 0 : t('integration.nav.action.hide')"
|
|
824
860
|
:title="railFolded ? t('integration.nav.action.show.title') : t('integration.nav.action.hide.title')"
|
|
825
861
|
@click="toggleRail"
|
|
826
862
|
>
|
|
827
863
|
<RailIcon :size="15" />
|
|
828
|
-
{{
|
|
864
|
+
<template v-if="railFolded">{{ t("integration.nav.action.show") }}</template>
|
|
829
865
|
</Button>
|
|
830
866
|
|
|
831
867
|
<!-- Run open: run-scoped actions replace the workflow Save. -->
|
|
832
868
|
<template v-if="openRunId">
|
|
833
|
-
<Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" @click="retryOpenRun">
|
|
869
|
+
<Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" :title="t('integration.editor.action.retry.title')" @click="retryOpenRun">
|
|
834
870
|
<Loader2 v-if="retryBusy" :size="12" class="animate-spin" />
|
|
835
871
|
<RetryIcon v-else :size="12" />
|
|
836
872
|
{{ t("integration.editor.action.retry") }}
|
|
@@ -843,13 +879,13 @@ function formatDateTime(iso) {
|
|
|
843
879
|
<template v-else>
|
|
844
880
|
<span
|
|
845
881
|
v-if="buildWorking"
|
|
846
|
-
class="inline-flex h-7 items-center gap-1.5 rounded-md border border-sky-500/25 bg-sky-500/10 px-2.5 text-sm text-sky-700"
|
|
882
|
+
class="inline-flex h-7 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border border-sky-500/25 bg-sky-500/10 px-2.5 text-sm text-sky-700"
|
|
847
883
|
:title="buildNotice?.text"
|
|
848
884
|
>
|
|
849
885
|
<Loader2 :size="12" class="animate-spin" />
|
|
850
886
|
{{ workflow.buildStatus === "building" ? t("integration.editor.build.chip.building") : t("integration.editor.build.chip.queued") }}
|
|
851
887
|
</span>
|
|
852
|
-
<span v-if="isDirty" class="text-sm text-amber-600">{{ t("integration.editor.status.unsavedChanges") }}</span>
|
|
888
|
+
<span v-if="isDirty" class="shrink-0 whitespace-nowrap text-sm text-amber-600">{{ t("integration.editor.status.unsavedChanges") }}</span>
|
|
853
889
|
<!-- What is done to the WORKFLOW, as opposed to the draft in front
|
|
854
890
|
of it: the buttons beside this menu discard and save an
|
|
855
891
|
afternoon's work, and these two make and unmake the thing that
|
|
@@ -981,6 +1017,62 @@ function formatDateTime(iso) {
|
|
|
981
1017
|
</ul>
|
|
982
1018
|
</div>
|
|
983
1019
|
|
|
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
|
+
|
|
984
1076
|
<!-- Only the FAILED build state banners (its message matters); the
|
|
985
1077
|
working states live as the header chip — and inside a run the
|
|
986
1078
|
workflow's build state is noise, so nothing shows at all. -->
|
|
@@ -1005,7 +1097,7 @@ function formatDateTime(iso) {
|
|
|
1005
1097
|
</div>
|
|
1006
1098
|
</div>
|
|
1007
1099
|
<div v-else class="min-h-0 flex-1">
|
|
1008
|
-
<WorkflowRunReplayPanel :workflow="workflow" :initial-run-id="openRunId"
|
|
1100
|
+
<WorkflowRunReplayPanel :workflow="workflow" :initial-run-id="openRunId" />
|
|
1009
1101
|
</div>
|
|
1010
1102
|
</template>
|
|
1011
1103
|
|
|
@@ -1033,7 +1125,7 @@ function formatDateTime(iso) {
|
|
|
1033
1125
|
</div>
|
|
1034
1126
|
|
|
1035
1127
|
<!-- Runs — this workflow's full execution history. Manual runs start
|
|
1036
|
-
from the editor's Run
|
|
1128
|
+
from the editor's Run button, through the run dialog. -->
|
|
1037
1129
|
<div v-else-if="tab === 'runs'" class="p-4 space-y-4 overflow-y-auto">
|
|
1038
1130
|
<div v-if="runsError" class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700">
|
|
1039
1131
|
{{ runsError }}
|
|
@@ -1078,7 +1170,7 @@ function formatDateTime(iso) {
|
|
|
1078
1170
|
</div>
|
|
1079
1171
|
</div>
|
|
1080
1172
|
|
|
1081
|
-
<!--
|
|
1173
|
+
<!-- The run dialog — opened by the editor's Run button. -->
|
|
1082
1174
|
<Dialog v-model:open="runPayloadOpen">
|
|
1083
1175
|
<DialogContent class="sm:max-w-lg">
|
|
1084
1176
|
<DialogHeader>
|
|
@@ -1116,27 +1208,30 @@ function formatDateTime(iso) {
|
|
|
1116
1208
|
</p>
|
|
1117
1209
|
</div>
|
|
1118
1210
|
<div v-if="workflow" class="flex items-center justify-end gap-2">
|
|
1211
|
+
<!-- Both modes, and the waiting one is the offer: the author pressed Run
|
|
1212
|
+
to see what comes back, so sync is the primary button — which puts it
|
|
1213
|
+
on the right, where every primary button in the studio sits. -->
|
|
1119
1214
|
<template v-if="workflow.execution_mode === 'caller_decides'">
|
|
1120
1215
|
<Button
|
|
1121
1216
|
variant="outline"
|
|
1122
1217
|
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
1123
1218
|
:title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
|
|
1124
1219
|
class="h-8 text-sm"
|
|
1125
|
-
@click="runFromPayloadDialog('
|
|
1220
|
+
@click="runFromPayloadDialog('async')"
|
|
1126
1221
|
>
|
|
1127
|
-
<Loader2 v-if="
|
|
1222
|
+
<Loader2 v-if="running || waitingMode === 'async'" :size="14" class="animate-spin" />
|
|
1128
1223
|
<Play v-else :size="14" />
|
|
1129
|
-
{{ waitingMode === "
|
|
1224
|
+
{{ waitingMode === "async" ? t("integration.editor.run.waitingForBuild") : t("integration.editor.runPayload.action.run", { mode: t("integration.editor.mode.word.async") }) }}
|
|
1130
1225
|
</Button>
|
|
1131
1226
|
<Button
|
|
1132
1227
|
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
1133
1228
|
:title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
|
|
1134
1229
|
class="h-8 text-sm"
|
|
1135
|
-
@click="runFromPayloadDialog('
|
|
1230
|
+
@click="runFromPayloadDialog('sync')"
|
|
1136
1231
|
>
|
|
1137
|
-
<Loader2 v-if="
|
|
1232
|
+
<Loader2 v-if="syncLoading || waitingMode === 'sync'" :size="14" class="animate-spin" />
|
|
1138
1233
|
<Play v-else :size="14" />
|
|
1139
|
-
{{ waitingMode === "
|
|
1234
|
+
{{ waitingMode === "sync" ? t("integration.editor.run.waitingForBuild") : t("integration.editor.runPayload.action.run", { mode: t("integration.editor.mode.word.sync") }) }}
|
|
1140
1235
|
</Button>
|
|
1141
1236
|
</template>
|
|
1142
1237
|
<Button
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One record, from `{ data: record }` or from a bare record.
|
|
3
|
+
*
|
|
4
|
+
* Throws when the response carries an envelope with nothing in it, or no body at
|
|
5
|
+
* all — the case the old expression turned into an object with none of the
|
|
6
|
+
* caller's fields, which every page downstream drew as empty.
|
|
7
|
+
*
|
|
8
|
+
* **The thrown message is deliberately empty.** `errorReason(err, fallback)` in
|
|
9
|
+
* `apiError.ts` falls back to the caller's own translated sentence when the
|
|
10
|
+
* message is blank, so a shape violation surfaces as the page's ordinary "could
|
|
11
|
+
* not load this" copy rather than as an untranslated technical string in the UI.
|
|
12
|
+
* The response that caused it rides along in `body`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function unwrapRecord<T>(res: unknown, ownFields?: readonly string[]): T;
|
|
15
|
+
/**
|
|
16
|
+
* A list, from `{ data: [ … ] }` or from a bare array.
|
|
17
|
+
*
|
|
18
|
+
* An empty collection is `[]` or `{ "data": [] }` and answers empty. A body that is
|
|
19
|
+
* neither throws, for the same reason the record above does — and it was drafted the
|
|
20
|
+
* other way round, returning empty for anything unreadable, until a review pointed
|
|
21
|
+
* out what that costs: the workflows listing used to raise (`ingestList` mapped over
|
|
22
|
+
* the body and threw inside its own `try`), so an unreadable answer reached the
|
|
23
|
+
* reader as an error. Empty would have turned that into "no workflows yet" for a
|
|
24
|
+
* tenant whose workflows are all still there — a silent wrong answer in place of a
|
|
25
|
+
* loud one, in the change that exists to remove exactly that.
|
|
26
|
+
*/
|
|
27
|
+
export declare function unwrapList<T>(res: unknown): T[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IntegrationsApiError } from "./apiError.js";
|
|
2
|
+
export function unwrapRecord(res, ownFields = []) {
|
|
3
|
+
if (res === null || res === void 0) {
|
|
4
|
+
throw new IntegrationsApiError(200, res, "");
|
|
5
|
+
}
|
|
6
|
+
if (typeof res === "object" && "data" in res && !ownFields.some((f) => f in res)) {
|
|
7
|
+
const data = res.data;
|
|
8
|
+
if (data === null || data === void 0) {
|
|
9
|
+
throw new IntegrationsApiError(200, res, "");
|
|
10
|
+
}
|
|
11
|
+
return data;
|
|
12
|
+
}
|
|
13
|
+
return res;
|
|
14
|
+
}
|
|
15
|
+
export function unwrapList(res) {
|
|
16
|
+
if (Array.isArray(res)) return res;
|
|
17
|
+
const data = res?.data;
|
|
18
|
+
if (Array.isArray(data)) return data;
|
|
19
|
+
throw new IntegrationsApiError(200, res, "");
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConfigField } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* The config a set of settings starts life with: what each one declares as its
|
|
4
|
+
* default, and nothing else.
|
|
5
|
+
*
|
|
6
|
+
* A setting with no `default` is left OUT rather than written as an empty
|
|
7
|
+
* string, because the two are different answers. `''` is a value — the engine
|
|
8
|
+
* reads it, `??` does not replace it, and a node whose own default would have
|
|
9
|
+
* applied never gets to apply it. An absent key is the absence the node's
|
|
10
|
+
* default answers to.
|
|
11
|
+
*
|
|
12
|
+
* Used in the two places a fresh set of values is made: a node dropped on the
|
|
13
|
+
* canvas (WorkflowEditor), and a row added to a list setting (NodeConfigField).
|
|
14
|
+
* Flat on purpose — it reads `default` at the level it is given and does not
|
|
15
|
+
* descend into `properties` or `items`, so what a newly inserted node carries
|
|
16
|
+
* is exactly what it carried before this became shared.
|
|
17
|
+
*/
|
|
18
|
+
export declare function defaultsFor(fields?: ReadonlyArray<Pick<ConfigField, 'key' | 'default'>>): Record<string, unknown>;
|
|
@@ -0,0 +1,60 @@
|
|
|
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[];
|