@revenexx/studio-integrations 2.8.1 → 2.10.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 +28 -0
- package/dist/module.json +2 -2
- package/dist/runtime/components/Integrations/ExpressionInput.vue +31 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +5 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +75 -71
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +11 -5
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue +2 -2
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -2
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +147 -27
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +36 -12
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +170 -13
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +34 -12
- package/dist/runtime/components/Integrations/WorkflowTriggerView.d.vue.ts +8 -1
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +3 -1
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue.d.ts +8 -1
- package/dist/runtime/components/Integrations/canvasSides.d.ts +27 -0
- package/dist/runtime/components/Integrations/canvasSides.js +22 -0
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +4 -1
- package/dist/runtime/composables/useIntegrationsCredentials.js +8 -3
- package/dist/runtime/data/canvasShapes.d.ts +139 -0
- package/dist/runtime/data/canvasShapes.js +130 -0
- package/dist/runtime/data/connectionRules.d.ts +110 -0
- package/dist/runtime/data/connectionRules.js +110 -0
- package/dist/runtime/data/graphShapes.d.ts +175 -0
- package/dist/runtime/data/graphShapes.js +359 -0
- package/dist/runtime/data/handleId.d.ts +12 -0
- package/dist/runtime/data/handleId.js +3 -0
- package/dist/runtime/data/integrationTemplates.js +3 -4
- package/dist/runtime/data/refusalMessage.d.ts +32 -0
- package/dist/runtime/data/refusalMessage.js +42 -0
- package/dist/runtime/data/saveRefusal.d.ts +31 -0
- package/dist/runtime/data/saveRefusal.js +25 -0
- package/dist/runtime/i18n/messages/de.json +36 -1
- package/dist/runtime/i18n/messages/en.json +36 -1
- package/dist/runtime/pages/integrations/schedules.vue +1 -1
- package/dist/runtime/pages/integrations/workflows/[id].vue +38 -3
- package/dist/runtime/types/integrations.d.ts +13 -4
- package/dist/runtime/utils/apiError.js +7 -4
- package/dist/runtime/utils/packageLabel.d.ts +17 -0
- package/dist/runtime/utils/packageLabel.js +6 -0
- package/dist/runtime/utils/skippedStep.d.ts +39 -0
- package/dist/runtime/utils/skippedStep.js +22 -0
- package/package.json +3 -3
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +0 -21
- package/dist/runtime/components/Integrations/NodeLogo.vue +0 -10
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +0 -21
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, inject, nextTick, ref, watch } from "vue";
|
|
2
|
+
import { computed, inject, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
|
3
3
|
import { Handle, Position, useVueFlow } from "@vue-flow/core";
|
|
4
4
|
import { NodeToolbar } from "@vue-flow/node-toolbar";
|
|
5
5
|
import * as Lucide from "lucide-vue-next";
|
|
6
6
|
import { Copy, DangerTriangle as AlertTriangle, RefreshCircle as Loader2, Repeat, TrashBinMinimalistic as TrashBin } from "#solar-icons";
|
|
7
7
|
import { NODE_CATEGORIES, findManifest, isVersionExecutable } from "../../data/integrationNodeCatalog";
|
|
8
8
|
import { inputHandleId, outputHandleId } from "../../data/handleId";
|
|
9
|
+
import { branchLineText } from "../../data/canvasShapes";
|
|
10
|
+
import { joinRefusalText } from "../../data/refusalMessage";
|
|
9
11
|
import { isTerminalManifest } from "../../data/nodeInputData";
|
|
10
12
|
import { resolveLocalized } from "../../utils/localize";
|
|
11
|
-
import { SIDE_POSITION, distributedStyle } from "./canvasSides";
|
|
12
|
-
import { fieldDisplayLabel } from "../../utils/validationFindings";
|
|
13
|
+
import { SIDE_POSITION, distributedStyle, spacedByLabel } from "./canvasSides";
|
|
14
|
+
import { fieldDisplayLabel, nodeDisplayLabel } from "../../utils/validationFindings";
|
|
13
15
|
import { nodeDisplayName } from "../../utils/nodeDisplayName";
|
|
16
|
+
import { packageLabel } from "../../utils/packageLabel";
|
|
14
17
|
const props = defineProps({
|
|
15
18
|
id: { type: String, required: true },
|
|
16
19
|
data: { type: Object, required: true },
|
|
@@ -22,6 +25,7 @@ const t = useIntegrationsT();
|
|
|
22
25
|
const manifest = computed(() => findManifest(props.data.nodeSlug, props.data.nodeVersion));
|
|
23
26
|
const category = computed(() => NODE_CATEGORIES.find((c) => c.key === manifest.value?.category));
|
|
24
27
|
const title = computed(() => nodeDisplayName(props.data));
|
|
28
|
+
const fromPackage = computed(() => manifest.value?.__package ? packageLabel(manifest.value.__package) : null);
|
|
25
29
|
const invalidFields = computed(() => props.data.__invalidFields ?? []);
|
|
26
30
|
const invalidLabel = computed(() => t("integration.canvas.node.badge.invalidConfig", invalidFields.value.length, {
|
|
27
31
|
fields: invalidFields.value.map((key) => fieldDisplayLabel(props.data, key)).join(", ")
|
|
@@ -116,6 +120,94 @@ function portTone(p) {
|
|
|
116
120
|
const showPortLabels = computed(() => resolvePending.value || outputs.value.length > 1 || outputs.value.some((p) => p.kind !== "default"));
|
|
117
121
|
const IN_SIDES = ["top"];
|
|
118
122
|
const OUT_SIDES = ["bottom"];
|
|
123
|
+
const splitPorts = computed(() => new Set(props.data.__shape?.splitPorts ?? []));
|
|
124
|
+
const isMerger = computed(() => props.data.__shape?.merger === true);
|
|
125
|
+
const isRefused = computed(() => props.data.__shape?.refused !== void 0);
|
|
126
|
+
const inputTitle = computed(() => {
|
|
127
|
+
const shape = props.data.__shape;
|
|
128
|
+
if (shape?.refused) {
|
|
129
|
+
const text = joinRefusalText(shape.refused, nodeDisplayLabel(props.data));
|
|
130
|
+
return [t(text.title), t(text.description, text.params)].join("\n");
|
|
131
|
+
}
|
|
132
|
+
const lines = (shape?.inputBranches ?? []).map(branchLineText);
|
|
133
|
+
if (isMerger.value) return [t("integration.canvas.node.handle.merger"), ...lines].join("\n");
|
|
134
|
+
if (shape?.converging) {
|
|
135
|
+
const keyed = shape.inputBranches?.some((b) => b.address !== "in");
|
|
136
|
+
return [
|
|
137
|
+
t("integration.canvas.node.handle.converging"),
|
|
138
|
+
...keyed ? [t("integration.canvas.node.handle.convergingKeyed")] : [],
|
|
139
|
+
...lines
|
|
140
|
+
].join("\n");
|
|
141
|
+
}
|
|
142
|
+
return t("integration.canvas.node.handle.input");
|
|
143
|
+
});
|
|
144
|
+
const DOT_CLASS = "!size-3 !rounded-full";
|
|
145
|
+
const BOX_CLASS = "!h-3 !w-6 !rounded-[3px] hover:!w-7";
|
|
146
|
+
function outputTitle(p) {
|
|
147
|
+
return splitPorts.value.has(p.name) ? `${handleTitle(p)} \u2014 ${t("integration.canvas.node.handle.splitter")}` : handleTitle(p);
|
|
148
|
+
}
|
|
149
|
+
const VM_STEP = 44;
|
|
150
|
+
const VM_SPREAD = 56;
|
|
151
|
+
const virtualMergers = computed(() => {
|
|
152
|
+
const list = props.data.__shape?.virtualMergers ?? [];
|
|
153
|
+
return list.map((vm) => ({
|
|
154
|
+
...vm,
|
|
155
|
+
x: vm.offset * VM_SPREAD,
|
|
156
|
+
y: -VM_STEP * vm.level,
|
|
157
|
+
toX: vm.parent === null ? 0 : list[vm.parent].offset * VM_SPREAD,
|
|
158
|
+
toY: -VM_STEP * (vm.level - 1),
|
|
159
|
+
title: [
|
|
160
|
+
t(vm.kind === "merger" ? "integration.canvas.node.handle.virtualMerger" : "integration.canvas.node.handle.virtualConverging"),
|
|
161
|
+
...vm.branches.map(branchLineText)
|
|
162
|
+
].join("\n")
|
|
163
|
+
}));
|
|
164
|
+
});
|
|
165
|
+
const CARD_WIDTH = 220;
|
|
166
|
+
const LABEL_GAP = 8;
|
|
167
|
+
const DOT_ROOM = 14;
|
|
168
|
+
const BOX_ROOM = 28;
|
|
169
|
+
const labelEls = /* @__PURE__ */ new Map();
|
|
170
|
+
const labelWidths = ref({});
|
|
171
|
+
const labelObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measureLabels);
|
|
172
|
+
onBeforeUnmount(() => labelObserver?.disconnect());
|
|
173
|
+
function measureLabels() {
|
|
174
|
+
const next = { ...labelWidths.value };
|
|
175
|
+
let changed = false;
|
|
176
|
+
for (const [name, el] of labelEls) {
|
|
177
|
+
if (next[name] !== el.offsetWidth) {
|
|
178
|
+
next[name] = el.offsetWidth;
|
|
179
|
+
changed = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (changed) labelWidths.value = next;
|
|
183
|
+
}
|
|
184
|
+
function setLabelEl(el, name) {
|
|
185
|
+
const node = el instanceof HTMLElement ? el : null;
|
|
186
|
+
const known = labelEls.get(name);
|
|
187
|
+
if (known === node) return;
|
|
188
|
+
if (known) labelObserver?.unobserve(known);
|
|
189
|
+
if (node) {
|
|
190
|
+
labelEls.set(name, node);
|
|
191
|
+
labelObserver?.observe(node);
|
|
192
|
+
} else {
|
|
193
|
+
labelEls.delete(name);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const portLayout = computed(() => {
|
|
197
|
+
if (!showPortLabels.value || resolvePending.value) return null;
|
|
198
|
+
const widths = outputs.value.map((p) => {
|
|
199
|
+
const w = labelWidths.value[p.name];
|
|
200
|
+
return w === void 0 || !splitPorts.value.has(p.name) ? w : Math.max(w, BOX_ROOM);
|
|
201
|
+
});
|
|
202
|
+
if (widths.includes(void 0)) return null;
|
|
203
|
+
return spacedByLabel(widths, CARD_WIDTH, LABEL_GAP, DOT_ROOM);
|
|
204
|
+
});
|
|
205
|
+
function outputStyle(side, i) {
|
|
206
|
+
const centre = portLayout.value?.centres[i];
|
|
207
|
+
if (centre === void 0 || side !== "bottom" && side !== "top") return distributedStyle(side, i, outputs.value.length);
|
|
208
|
+
return { left: `${centre}px` };
|
|
209
|
+
}
|
|
210
|
+
watch(() => JSON.stringify(portLayout.value), () => nextTick(() => flow.updateNodeInternals([props.id])));
|
|
119
211
|
const flow = useVueFlow();
|
|
120
212
|
const hovered = ref(false);
|
|
121
213
|
let hoverTimer = null;
|
|
@@ -240,12 +332,37 @@ function remove() {
|
|
|
240
332
|
:key="`in-${side}`"
|
|
241
333
|
type="target"
|
|
242
334
|
:position="SIDE_POSITION[side]"
|
|
243
|
-
:title="
|
|
244
|
-
class="!
|
|
335
|
+
:title="inputTitle"
|
|
336
|
+
class="!border-2 !border-white !shadow-sm transition-all"
|
|
337
|
+
:class="[
|
|
338
|
+
isMerger ? BOX_CLASS : `${DOT_CLASS} hover:!size-3.5`,
|
|
339
|
+
isRefused ? '!bg-rose-600 hover:!bg-rose-700' : '!bg-slate-500 hover:!bg-slate-700'
|
|
340
|
+
]"
|
|
341
|
+
:data-refused="isRefused ? 'true' : void 0"
|
|
342
|
+
/>
|
|
343
|
+
|
|
344
|
+
<!-- Virtual mergers (PO-750): drawn, never stored. Each is a dot of this card's
|
|
345
|
+
own — a connection of its level arrives here instead of at the input — so it
|
|
346
|
+
moves with the card, cannot be selected, dragged or deleted on its own, and
|
|
347
|
+
nothing can be started from it. Dropping a connection on one connects to
|
|
348
|
+
this node, exactly as the input dot would. -->
|
|
349
|
+
<Handle
|
|
350
|
+
v-for="vm in virtualMergers"
|
|
351
|
+
:id="vm.handleId"
|
|
352
|
+
:key="vm.handleId"
|
|
353
|
+
type="target"
|
|
354
|
+
:position="Position.Top"
|
|
355
|
+
:connectable-start="false"
|
|
356
|
+
:title="vm.title"
|
|
357
|
+
:style="{ top: `${vm.y}px`, left: `calc(50% + ${vm.x}px)` }"
|
|
358
|
+
class="!h-3 !w-6 !rounded-[3px] !border-2 !border-dashed !border-slate-500 !bg-background dark:!border-slate-400"
|
|
245
359
|
/>
|
|
246
360
|
|
|
247
361
|
<!-- Output handles: right + bottom, distributed per side. Color encodes the
|
|
248
|
-
port semantics (true/false/error/branch/default), matching the edges.
|
|
362
|
+
port semantics (true/false/error/branch/default), matching the edges.
|
|
363
|
+
Only the hover size is animated, never the place: a dot gliding to where
|
|
364
|
+
its name moved it (PO-790) is measured by the canvas on its way there, and
|
|
365
|
+
its connection is left behind at the old spot. -->
|
|
249
366
|
<template v-for="side in OUT_SIDES" :key="`side-${side}`">
|
|
250
367
|
<Handle
|
|
251
368
|
v-for="(p, i) in outputs"
|
|
@@ -253,17 +370,18 @@ function remove() {
|
|
|
253
370
|
:key="`${p.name}-${side}`"
|
|
254
371
|
type="source"
|
|
255
372
|
:position="SIDE_POSITION[side]"
|
|
256
|
-
:title="
|
|
257
|
-
:style="
|
|
258
|
-
class="!
|
|
259
|
-
:class="portTone(p).handle"
|
|
373
|
+
:title="outputTitle(p)"
|
|
374
|
+
:style="outputStyle(side, i)"
|
|
375
|
+
class="!border-2 !border-white !shadow-sm transition-[width,height]"
|
|
376
|
+
:class="[portTone(p).handle, splitPorts.has(p.name) ? BOX_CLASS : `${DOT_CLASS} hover:!size-3.5`]"
|
|
260
377
|
/>
|
|
261
378
|
</template>
|
|
262
379
|
|
|
263
380
|
<!-- The card itself — deliberately minimal: icon, name, what it's set to.
|
|
264
381
|
Everything else lives in the config lightbox. -->
|
|
265
382
|
<div
|
|
266
|
-
class="relative
|
|
383
|
+
class="relative rounded-lg border bg-white transition-[box-shadow,border-color] duration-150"
|
|
384
|
+
:style="{ width: `${portLayout?.width ?? CARD_WIDTH}px` }"
|
|
267
385
|
:class="[
|
|
268
386
|
selected ? 'border-emerald-500/50 shadow-[0_0_0_2px_rgba(16,185,129,0.18)]' : invalidFields.length ? 'border-amber-500/70 shadow-[0_0_0_2px_rgba(245,158,11,0.22)] hover:shadow-md' : inSelectedLoop ? 'border-slate-400/80 shadow-[0_0_0_2px_rgba(100,116,139,0.20)]' : 'border-black/[0.08] shadow-sm hover:shadow-md hover:border-black/[0.16]',
|
|
269
387
|
showPortLabels ? 'pb-3.5' : ''
|
|
@@ -271,6 +389,27 @@ function remove() {
|
|
|
271
389
|
@mouseenter="onHoverEnter"
|
|
272
390
|
@mouseleave="onHoverLeave"
|
|
273
391
|
>
|
|
392
|
+
<!-- The lines a virtual merger stands on, down to what it feeds. Inside the
|
|
393
|
+
card rather than beside it, so the card stays the wrapper's one child that
|
|
394
|
+
is not a port; it claims no layout and takes no pointer. -->
|
|
395
|
+
<svg
|
|
396
|
+
v-if="virtualMergers.length"
|
|
397
|
+
class="pointer-events-none absolute left-1/2 top-0 overflow-visible text-slate-400 dark:text-slate-500"
|
|
398
|
+
width="1"
|
|
399
|
+
height="1"
|
|
400
|
+
aria-hidden="true"
|
|
401
|
+
>
|
|
402
|
+
<line
|
|
403
|
+
v-for="vm in virtualMergers"
|
|
404
|
+
:key="`vm-line-${vm.handleId}`"
|
|
405
|
+
:x1="vm.x"
|
|
406
|
+
:y1="vm.y"
|
|
407
|
+
:x2="vm.toX"
|
|
408
|
+
:y2="vm.toY"
|
|
409
|
+
stroke="currentColor"
|
|
410
|
+
stroke-width="2"
|
|
411
|
+
/>
|
|
412
|
+
</svg>
|
|
274
413
|
<!-- The card's marks, in one row at the corner the input handles leave free.
|
|
275
414
|
A row rather than two absolutely-placed badges: the amber one's width
|
|
276
415
|
follows its count, so any fixed offset for a second badge overlaps it as
|
|
@@ -278,7 +417,7 @@ function remove() {
|
|
|
278
417
|
a bare count at a corner is a riddle, so each carries its own title;
|
|
279
418
|
`nodrag` keeps that hover from starting a card drag. -->
|
|
280
419
|
<span
|
|
281
|
-
v-if="invalidFields.length || unbuildableVersion || insideLoops.length || bodySize !== void 0"
|
|
420
|
+
v-if="invalidFields.length || unbuildableVersion || insideLoops.length || bodySize !== void 0 || data.__skipped"
|
|
282
421
|
class="nodrag absolute -left-2 -top-2 z-10 flex items-center gap-1"
|
|
283
422
|
>
|
|
284
423
|
<span
|
|
@@ -324,6 +463,17 @@ function remove() {
|
|
|
324
463
|
<Repeat :size="9" weight="Bold" />
|
|
325
464
|
<template v-if="insideLoops.length > 1">{{ insideLoops.length }}</template>
|
|
326
465
|
</span>
|
|
466
|
+
<!-- A step the run skipped (PO-750), with the reason as its name and title.
|
|
467
|
+
Slate and outlined: it is a statement about the run, not a defect. -->
|
|
468
|
+
<span
|
|
469
|
+
v-if="data.__skipped"
|
|
470
|
+
role="img"
|
|
471
|
+
:aria-label="data.__skipped"
|
|
472
|
+
:title="data.__skipped"
|
|
473
|
+
class="flex cursor-help items-center rounded-full border border-slate-400 bg-white px-1.5 py-0.5 text-[9px] font-bold leading-none text-slate-600 shadow-sm"
|
|
474
|
+
>
|
|
475
|
+
{{ data.__skipped }}
|
|
476
|
+
</span>
|
|
327
477
|
<!-- The loop itself, and how much of the canvas it repeats. Outlined
|
|
328
478
|
rather than filled: it names the same fact from the other end, and two
|
|
329
479
|
solid slate pills side by side on a nested iterator would read as one. -->
|
|
@@ -400,9 +550,10 @@ function remove() {
|
|
|
400
550
|
<span
|
|
401
551
|
v-for="(p, i) in outputs"
|
|
402
552
|
:key="`plabel-${p.name}`"
|
|
553
|
+
:ref="(el) => setLabelEl(el, p.name)"
|
|
403
554
|
class="pointer-events-none absolute bottom-1 select-none whitespace-nowrap text-[9px] font-semibold uppercase tracking-wide"
|
|
404
555
|
:class="portTone(p).label"
|
|
405
|
-
:style="{
|
|
556
|
+
:style="{ ...outputStyle('bottom', i), transform: 'translateX(-50%)' }"
|
|
406
557
|
>
|
|
407
558
|
{{ p.label }}
|
|
408
559
|
</span>
|
|
@@ -424,6 +575,12 @@ function remove() {
|
|
|
424
575
|
inspector. Left-aligned and on the second line, clear of the
|
|
425
576
|
top-right edit affordance (PO-192). -->
|
|
426
577
|
<div v-if="data.nodeVersion" class="mt-0.5 flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
578
|
+
<span
|
|
579
|
+
v-if="fromPackage"
|
|
580
|
+
:title="fromPackage"
|
|
581
|
+
class="min-w-0 truncate text-[10px] font-medium leading-none text-muted-foreground/70"
|
|
582
|
+
>{{ fromPackage }}</span>
|
|
583
|
+
<span v-if="fromPackage" aria-hidden="true" class="text-[10px] leading-none text-muted-foreground/40">·</span>
|
|
427
584
|
<span
|
|
428
585
|
class="shrink-0 text-[10px] font-medium leading-none tabular-nums text-muted-foreground/55"
|
|
429
586
|
:title="t('integration.canvas.node.meta.versionTitle', { version: data.nodeVersion })"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NodeShapeData } from '../../data/canvasShapes.js';
|
|
1
2
|
import type { NodeManifestOutput, WorkflowNode } from '../../types/integrations.js';
|
|
2
3
|
interface NodeData extends WorkflowNode {
|
|
3
4
|
__selected?: boolean;
|
|
@@ -22,6 +23,19 @@ interface NodeData extends WorkflowNode {
|
|
|
22
23
|
/** Set on an iterator itself: how many nodes hang off its `item` port. */
|
|
23
24
|
bodySize?: number;
|
|
24
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Run replay only (PO-750): the node's step was skipped, and this says why —
|
|
28
|
+
* "Skipped: dead end in branch 2", or a plain "Skipped" for a reason the studio
|
|
29
|
+
* does not name. Injected like the markers above; no other canvas passes it.
|
|
30
|
+
*/
|
|
31
|
+
__skipped?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Where the workflow splits and merges at this node (PO-750), computed per sync
|
|
34
|
+
* from the blob's edges (`data/canvasShapes.ts`) and injected into all three
|
|
35
|
+
* canvases like `__loop`: a split port and a merging input are drawn as boxes,
|
|
36
|
+
* and the levels of a nested merge as small boxes in front of the input.
|
|
37
|
+
*/
|
|
38
|
+
__shape?: NodeShapeData;
|
|
25
39
|
}
|
|
26
40
|
type __VLS_Props = {
|
|
27
41
|
id: string;
|
|
@@ -26,9 +26,11 @@ import { findManifest } from "../../data/integrationNodeCatalog";
|
|
|
26
26
|
import { OPPOSITE_SIDE } from "./canvasSides";
|
|
27
27
|
import { inputHandleId, outputHandleId } from "../../data/handleId";
|
|
28
28
|
import { nodeLoopData } from "../../data/loopBody";
|
|
29
|
+
import { canvasShapes } from "../../data/canvasShapes";
|
|
29
30
|
import { formatDuration } from "../../utils/formatDuration";
|
|
30
31
|
import { SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
31
32
|
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
33
|
+
import { executedNodeIds, isSkippedStep, skipReasonMessage, stepStatusLabel } from "../../utils/skippedStep";
|
|
32
34
|
import { errorReason } from "../../utils/apiError";
|
|
33
35
|
const props = defineProps({
|
|
34
36
|
workflow: { type: Object, required: true },
|
|
@@ -139,6 +141,10 @@ preview.onPaneReady(() => {
|
|
|
139
141
|
void preview.fitView({ padding: 0.2, maxZoom: 1 });
|
|
140
142
|
});
|
|
141
143
|
provide("workflow-node-actions", null);
|
|
144
|
+
function memberSize(id) {
|
|
145
|
+
const d = preview.findNode(id)?.dimensions;
|
|
146
|
+
return d && d.width > 0 ? d : { width: 220, height: 64 };
|
|
147
|
+
}
|
|
142
148
|
const dynamicOutputs = ref({});
|
|
143
149
|
const dynamicOutputNodes = computed(() => props.workflow.blob.nodes.filter((n) => {
|
|
144
150
|
const m = findManifest(n.nodeSlug, n.nodeVersion);
|
|
@@ -161,14 +167,14 @@ const finalStatus = computed(() => {
|
|
|
161
167
|
for (const s of steps.value) m.set(s.node_id, s.status);
|
|
162
168
|
return m;
|
|
163
169
|
});
|
|
164
|
-
const runNodeIds = computed(() =>
|
|
170
|
+
const runNodeIds = computed(() => executedNodeIds(steps.value));
|
|
165
171
|
const triggerIds = computed(() => new Set(props.workflow.blob.triggers.map((t2) => t2.id)));
|
|
166
172
|
function edgeColorByKind(kind) {
|
|
167
173
|
return kind === "error" ? "#cbd5e1" : "#94a3b8";
|
|
168
174
|
}
|
|
169
175
|
const GROUP_PAD = 24;
|
|
170
176
|
const GROUP_LABEL_ROOM = 26;
|
|
171
|
-
function previewGroupNodes(blob, nodePos) {
|
|
177
|
+
function previewGroupNodes(blob, nodePos, nodeSize) {
|
|
172
178
|
return (blob.groups ?? []).map((g, i) => {
|
|
173
179
|
let minX = Number.POSITIVE_INFINITY;
|
|
174
180
|
let minY = Number.POSITIVE_INFINITY;
|
|
@@ -176,10 +182,11 @@ function previewGroupNodes(blob, nodePos) {
|
|
|
176
182
|
let maxY = Number.NEGATIVE_INFINITY;
|
|
177
183
|
for (const id of g.nodeIds) {
|
|
178
184
|
const pos = nodePos(id);
|
|
185
|
+
const size = nodeSize(id);
|
|
179
186
|
minX = Math.min(minX, pos.x);
|
|
180
187
|
minY = Math.min(minY, pos.y);
|
|
181
|
-
maxX = Math.max(maxX, pos.x +
|
|
182
|
-
maxY = Math.max(maxY, pos.y +
|
|
188
|
+
maxX = Math.max(maxX, pos.x + size.width);
|
|
189
|
+
maxY = Math.max(maxY, pos.y + size.height);
|
|
183
190
|
}
|
|
184
191
|
if (!Number.isFinite(minX)) return null;
|
|
185
192
|
return {
|
|
@@ -203,17 +210,28 @@ function previewGroupNodes(blob, nodePos) {
|
|
|
203
210
|
function stickyArea(s) {
|
|
204
211
|
return (s.size?.width ?? 240) * (s.size?.height ?? 160);
|
|
205
212
|
}
|
|
213
|
+
function skippedLabel(nodeId) {
|
|
214
|
+
const step = steps.value.findLast((s) => s.node_id === nodeId);
|
|
215
|
+
if (!step || !isSkippedStep(step)) return void 0;
|
|
216
|
+
return stepSkipText(step);
|
|
217
|
+
}
|
|
218
|
+
function stepSkipText(step) {
|
|
219
|
+
const m = skipReasonMessage(step);
|
|
220
|
+
return m ? t(m.key, m.params) : t("integration.runs.skip.plain");
|
|
221
|
+
}
|
|
206
222
|
function replayNodeClass(id, type) {
|
|
207
223
|
if (type === "sticky") return "replay-dim";
|
|
208
224
|
if (type === "trigger") return steps.value.length ? "replay-node-ok" : "";
|
|
209
225
|
const final = finalStatus.value.get(id);
|
|
210
226
|
if (!final) return "replay-dim";
|
|
211
227
|
const reached = reachedStatus.value.has(id);
|
|
228
|
+
if (isSkippedStep({ status: final })) return `replay-node-skipped${activeStep.value?.node_id === id ? " replay-node-active" : ""}`;
|
|
212
229
|
const tone = final === "failed" ? "replay-node-failed" : final === "running" ? "replay-node-running" : "replay-node-ok";
|
|
213
230
|
const active = activeStep.value?.node_id === id ? " replay-node-active" : "";
|
|
214
231
|
return `${tone}${reached ? "" : " replay-unreached"}${active}`;
|
|
215
232
|
}
|
|
216
233
|
const loops = computed(() => nodeLoopData(props.workflow.blob.nodes, props.workflow.blob.edges));
|
|
234
|
+
const shapes = computed(() => canvasShapes(props.workflow.blob.nodes, props.workflow.blob.edges, props.workflow.blob.triggers));
|
|
217
235
|
const vfNodes = computed(() => {
|
|
218
236
|
const blob = props.workflow.blob;
|
|
219
237
|
const triggerPos = (id, i) => blob.triggerPositions?.[id] ?? { x: -40, y: 60 + i * 140 };
|
|
@@ -226,7 +244,7 @@ const vfNodes = computed(() => {
|
|
|
226
244
|
};
|
|
227
245
|
const stickies = [...blob.stickies ?? []].sort((a, b) => stickyArea(b) - stickyArea(a));
|
|
228
246
|
return [
|
|
229
|
-
...previewGroupNodes(blob, posOf),
|
|
247
|
+
...previewGroupNodes(blob, posOf, memberSize),
|
|
230
248
|
...stickies.map((s, i) => ({
|
|
231
249
|
id: s.id,
|
|
232
250
|
type: "sticky",
|
|
@@ -244,7 +262,7 @@ const vfNodes = computed(() => {
|
|
|
244
262
|
position: triggerPos(t2.id, i),
|
|
245
263
|
draggable: false,
|
|
246
264
|
class: replayNodeClass(t2.id, "trigger"),
|
|
247
|
-
data: t2
|
|
265
|
+
data: { ...t2, __split: shapes.value.splitTriggers.has(t2.id) }
|
|
248
266
|
})),
|
|
249
267
|
...blob.nodes.map((n) => ({
|
|
250
268
|
id: n.id,
|
|
@@ -252,7 +270,7 @@ const vfNodes = computed(() => {
|
|
|
252
270
|
position: n.position ?? { x: 240, y: 80 },
|
|
253
271
|
draggable: false,
|
|
254
272
|
class: replayNodeClass(n.id, "workflow"),
|
|
255
|
-
data: { ...n, __resolvedOutputs: dynamicOutputs.value[n.id], __loop: loops.value.get(n.id) }
|
|
273
|
+
data: { ...n, __resolvedOutputs: dynamicOutputs.value[n.id], __loop: loops.value.get(n.id), __skipped: skippedLabel(n.id), __shape: shapes.value.byNode.get(n.id) }
|
|
256
274
|
}))
|
|
257
275
|
];
|
|
258
276
|
});
|
|
@@ -300,7 +318,7 @@ const vfEdges = computed(() => {
|
|
|
300
318
|
source: e.from.nodeId,
|
|
301
319
|
target: e.to.nodeId,
|
|
302
320
|
sourceHandle: outputHandleId(e.from.output, srcSide),
|
|
303
|
-
targetHandle: inputHandleId(OPPOSITE_SIDE[srcSide]),
|
|
321
|
+
targetHandle: shapes.value.arrivalHandle(e) ?? inputHandleId(OPPOSITE_SIDE[srcSide]),
|
|
304
322
|
type: "flow",
|
|
305
323
|
selectable: false,
|
|
306
324
|
animated: traversed && !targetReached,
|
|
@@ -365,7 +383,7 @@ function pretty(v) {
|
|
|
365
383
|
const activeStepTitle = computed(() => {
|
|
366
384
|
const s = activeStep.value;
|
|
367
385
|
if (!s) return "";
|
|
368
|
-
return `${s.node_slug || s.node_id}${s.attempt > 0 ? ` (#${s.attempt + 1})` : ""}`;
|
|
386
|
+
return `${s.node_slug || s.node_id}${(s.attempt ?? 0) > 0 ? ` (#${(s.attempt ?? 0) + 1})` : ""}`;
|
|
369
387
|
});
|
|
370
388
|
onMounted(() => {
|
|
371
389
|
if (props.initialRunId) selectRun(props.initialRunId);
|
|
@@ -478,7 +496,7 @@ watch(() => props.initialRunId, (id) => {
|
|
|
478
496
|
</div>
|
|
479
497
|
</div>
|
|
480
498
|
<Badge v-if="activeStep" variant="outline" class="shrink-0 border text-xs font-medium uppercase" :class="runStatusClass(activeStep.status)">
|
|
481
|
-
{{
|
|
499
|
+
{{ stepStatusLabel(activeStep.status) }}
|
|
482
500
|
</Badge>
|
|
483
501
|
</div>
|
|
484
502
|
|
|
@@ -486,7 +504,7 @@ watch(() => props.initialRunId, (id) => {
|
|
|
486
504
|
<div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
|
|
487
505
|
<div>
|
|
488
506
|
<div class="text-mono text-muted-foreground">{{ t("integration.replay.debug.field.attempt") }}</div>
|
|
489
|
-
<div class="tabular-nums">{{ activeStep.attempt + 1 }}</div>
|
|
507
|
+
<div class="tabular-nums">{{ activeStep.attempt == null ? "\u2014" : activeStep.attempt + 1 }}</div>
|
|
490
508
|
</div>
|
|
491
509
|
<div>
|
|
492
510
|
<div class="text-mono text-muted-foreground">{{ t("integration.replay.debug.field.duration") }}</div>
|
|
@@ -502,6 +520,10 @@ watch(() => props.initialRunId, (id) => {
|
|
|
502
520
|
</div>
|
|
503
521
|
</div>
|
|
504
522
|
|
|
523
|
+
<div v-if="isSkippedStep(activeStep) && skipReasonMessage(activeStep)" class="text-sm" data-testid="replay-skip-reason">
|
|
524
|
+
{{ stepSkipText(activeStep) }}
|
|
525
|
+
</div>
|
|
526
|
+
|
|
505
527
|
<div v-if="activeStep.branch" class="text-sm">
|
|
506
528
|
<div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.branch") }}</div>
|
|
507
529
|
<code class="rounded bg-black/[0.05] px-1.5 py-0.5 font-mono">{{ activeStep.branch }}</code>
|
|
@@ -536,5 +558,5 @@ watch(() => props.initialRunId, (id) => {
|
|
|
536
558
|
</template>
|
|
537
559
|
|
|
538
560
|
<style>
|
|
539
|
-
.vue-flow__node.replay-dim{opacity:.35}.vue-flow__node.replay-unreached{opacity:.45}.vue-flow__node.replay-node-ok{filter:drop-shadow(0 0 5px rgba(16,185,129,.45))}.vue-flow__node.replay-node-failed{filter:drop-shadow(0 0 5px rgba(244,63,94,.5))}.vue-flow__node.replay-node-running{filter:drop-shadow(0 0 5px rgba(14,165,233,.5))}.vue-flow__node.replay-node-active{filter:drop-shadow(0 0 10px rgba(59,130,246,.65))}
|
|
561
|
+
.vue-flow__node.replay-dim{opacity:.35}.vue-flow__node.replay-unreached{opacity:.45}.vue-flow__node.replay-node-skipped{border-radius:12px;filter:grayscale(1);opacity:.55;outline:2px dashed #94a3b8;outline-offset:3px}.vue-flow__node.replay-node-ok{filter:drop-shadow(0 0 5px rgba(16,185,129,.45))}.vue-flow__node.replay-node-failed{filter:drop-shadow(0 0 5px rgba(244,63,94,.5))}.vue-flow__node.replay-node-running{filter:drop-shadow(0 0 5px rgba(14,165,233,.5))}.vue-flow__node.replay-node-active{filter:drop-shadow(0 0 10px rgba(59,130,246,.65))}
|
|
540
562
|
</style>
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { WorkflowTrigger } from '../../types/integrations.js';
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
id: string;
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* `__split`: the trigger's one output has more than one connection leaving it,
|
|
6
|
+
* so it is a splitter and drawn as a box (PO-750). Injected per sync by every
|
|
7
|
+
* canvas, never stored.
|
|
8
|
+
*/
|
|
9
|
+
data: WorkflowTrigger & {
|
|
10
|
+
__split?: boolean;
|
|
11
|
+
};
|
|
5
12
|
selected?: boolean;
|
|
6
13
|
};
|
|
7
14
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, any, string, import("vue").PublicProps, any, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -105,6 +105,8 @@ const OUT_SIDES = ["bottom"];
|
|
|
105
105
|
:key="side"
|
|
106
106
|
type="source"
|
|
107
107
|
:position="SIDE_POSITION[side]"
|
|
108
|
-
|
|
108
|
+
:title="data.__split ? t('integration.canvas.node.handle.splitter') : void 0"
|
|
109
|
+
class="!border-2 !border-white !bg-emerald-500 !shadow-sm transition-all hover:!bg-emerald-600"
|
|
110
|
+
:class="data.__split ? '!h-3 !w-6 !rounded-[3px] hover:!w-7' : '!size-3 !rounded-full hover:!size-3.5'"
|
|
109
111
|
/>
|
|
110
112
|
</template>
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { WorkflowTrigger } from '../../types/integrations.js';
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
id: string;
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* `__split`: the trigger's one output has more than one connection leaving it,
|
|
6
|
+
* so it is a splitter and drawn as a box (PO-750). Injected per sync by every
|
|
7
|
+
* canvas, never stored.
|
|
8
|
+
*/
|
|
9
|
+
data: WorkflowTrigger & {
|
|
10
|
+
__split?: boolean;
|
|
11
|
+
};
|
|
5
12
|
selected?: boolean;
|
|
6
13
|
};
|
|
7
14
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, any, string, import("vue").PublicProps, any, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -46,3 +46,30 @@ export declare const OPPOSITE_SIDE: Record<Side, Side>;
|
|
|
46
46
|
* output lands in the middle.
|
|
47
47
|
*/
|
|
48
48
|
export declare function distributedStyle(side: Side, i: number, total: number): Record<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* Where the output dots go when their names would not fit evenly spaced (PO-790), or
|
|
51
|
+
* `null` when they do — and then {@link distributedStyle} places them as it always has.
|
|
52
|
+
*
|
|
53
|
+
* Each name is centred over its dot, so evenly spaced dots give every name the same
|
|
54
|
+
* room whatever its length: three outputs on a 220px card stand 55px apart, and
|
|
55
|
+
* "NICHT GEFUNDEN" alone is 81px. Spaced instead with equal gaps *between the names*,
|
|
56
|
+
* the same three fit with room to spare, and each dot moves under the centre of its
|
|
57
|
+
* name. Only where even that does not fit does the card grow, to exactly the width
|
|
58
|
+
* the names need.
|
|
59
|
+
*
|
|
60
|
+
* Returning `null` for the case that fits is deliberate: a card that reads well today
|
|
61
|
+
* keeps its dots exactly where they were, and so do the connections leaving them.
|
|
62
|
+
*
|
|
63
|
+
* A name is never narrower than its dot, though: an empty switch case, or a one-letter
|
|
64
|
+
* one, still has a 12px dot under it, and spacing names that take no room would put
|
|
65
|
+
* two dots on top of each other — one of them no longer reachable.
|
|
66
|
+
*
|
|
67
|
+
* @param measured the rendered width of each name, in port order
|
|
68
|
+
* @param width the card's width
|
|
69
|
+
* @param gap the least space between two names, and between a name and the edge
|
|
70
|
+
* @param least the least room a name takes: its dot's, at its hover size
|
|
71
|
+
*/
|
|
72
|
+
export declare function spacedByLabel(measured: readonly number[], width: number, gap: number, least: number): {
|
|
73
|
+
width: number;
|
|
74
|
+
centres: number[];
|
|
75
|
+
} | null;
|
|
@@ -20,3 +20,25 @@ const SIDE_SPREAD_AXIS = {
|
|
|
20
20
|
export function distributedStyle(side, i, total) {
|
|
21
21
|
return { [SIDE_SPREAD_AXIS[side]]: `${(i + 1) / (total + 1) * 100}%` };
|
|
22
22
|
}
|
|
23
|
+
export function spacedByLabel(measured, width, gap, least) {
|
|
24
|
+
const n = measured.length;
|
|
25
|
+
if (n === 0) return null;
|
|
26
|
+
const widths = measured.map((w) => Math.max(w, least));
|
|
27
|
+
const even = widths.map((_, i) => width * (i + 1) / (n + 1));
|
|
28
|
+
const fitsEvenly = widths.every(
|
|
29
|
+
(w, i) => i === 0 ? even[0] - w / 2 >= gap : even[i] - even[i - 1] >= (widths[i - 1] + w) / 2 + gap
|
|
30
|
+
) && width - even[n - 1] - widths[n - 1] / 2 >= gap;
|
|
31
|
+
if (fitsEvenly) return null;
|
|
32
|
+
const total = widths.reduce((a, b) => a + b, 0);
|
|
33
|
+
const needed = total + gap * (n + 1);
|
|
34
|
+
const cardWidth = Math.max(width, needed);
|
|
35
|
+
const space = (cardWidth - total) / (n + 1);
|
|
36
|
+
const centres = [];
|
|
37
|
+
let x = 0;
|
|
38
|
+
for (const w of widths) {
|
|
39
|
+
x += space;
|
|
40
|
+
centres.push(x + w / 2);
|
|
41
|
+
x += w;
|
|
42
|
+
}
|
|
43
|
+
return { width: cardWidth, centres };
|
|
44
|
+
}
|
|
@@ -40,7 +40,10 @@ export declare function useIntegrationsCredentials(): {
|
|
|
40
40
|
loaded: any;
|
|
41
41
|
total: import("vue").ComputedRef<any>;
|
|
42
42
|
needsReauth: import("vue").ComputedRef<any>;
|
|
43
|
-
load: (force?: boolean
|
|
43
|
+
load: (force?: boolean, { quiet }?: {
|
|
44
|
+
quiet?: boolean | undefined;
|
|
45
|
+
}) => Promise<Credential[]>;
|
|
46
|
+
refresh: () => Promise<Credential[]>;
|
|
44
47
|
ofType: (slug: string) => Credential[];
|
|
45
48
|
get: (id: string) => Promise<Credential>;
|
|
46
49
|
create: (input: {
|
|
@@ -12,19 +12,23 @@ export function useIntegrationsCredentials() {
|
|
|
12
12
|
const gate = useLoadGate("integrations.credentials", { loading, loaded });
|
|
13
13
|
const api = useIntegrationsApi();
|
|
14
14
|
const t = useIntegrationsT();
|
|
15
|
-
async function load(force = false) {
|
|
15
|
+
async function load(force = false, { quiet = false } = {}) {
|
|
16
16
|
if (!await gate.claim(force)) return credentials.value;
|
|
17
|
-
error.value = null;
|
|
17
|
+
if (!quiet) error.value = null;
|
|
18
18
|
try {
|
|
19
19
|
credentials.value = unwrapList(await api.get("/credentials"));
|
|
20
20
|
loaded.value = true;
|
|
21
|
+
error.value = null;
|
|
21
22
|
} catch (e) {
|
|
22
|
-
error.value = errorReason(e, t("integration.credentials.error.loadFailed"));
|
|
23
|
+
if (!quiet) error.value = errorReason(e, t("integration.credentials.error.loadFailed"));
|
|
23
24
|
} finally {
|
|
24
25
|
gate.release();
|
|
25
26
|
}
|
|
26
27
|
return credentials.value;
|
|
27
28
|
}
|
|
29
|
+
function refresh() {
|
|
30
|
+
return load(true, { quiet: loaded.value });
|
|
31
|
+
}
|
|
28
32
|
function ofType(slug) {
|
|
29
33
|
return credentials.value.filter((c) => c.credential_type_slug === slug);
|
|
30
34
|
}
|
|
@@ -80,6 +84,7 @@ export function useIntegrationsCredentials() {
|
|
|
80
84
|
total,
|
|
81
85
|
needsReauth,
|
|
82
86
|
load,
|
|
87
|
+
refresh,
|
|
83
88
|
ofType,
|
|
84
89
|
get,
|
|
85
90
|
create,
|