@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
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
RefreshCircle as Loader2
|
|
15
15
|
} from "#solar-icons";
|
|
16
16
|
import { Badge, Button } from "@revenexx/studio";
|
|
17
|
-
import { toast } from "vue-sonner";
|
|
18
17
|
import "@vue-flow/core/dist/style.css";
|
|
19
18
|
import "@vue-flow/core/dist/theme-default.css";
|
|
20
19
|
import WorkflowNodeView from "./WorkflowNodeView.vue";
|
|
@@ -27,7 +26,6 @@ import { findManifest } from "../../data/integrationNodeCatalog";
|
|
|
27
26
|
import { OPPOSITE_SIDE } from "./canvasSides";
|
|
28
27
|
import { inputHandleId, outputHandleId } from "../../data/handleId";
|
|
29
28
|
import { nodeLoopData } from "../../data/loopBody";
|
|
30
|
-
import { triggerGlyph } from "../../data/triggerGlyphs";
|
|
31
29
|
import { formatDuration } from "../../utils/formatDuration";
|
|
32
30
|
import { SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
33
31
|
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
@@ -37,7 +35,7 @@ const props = defineProps({
|
|
|
37
35
|
initialRunId: { type: [String, null], required: false },
|
|
38
36
|
single: { type: Boolean, required: false }
|
|
39
37
|
});
|
|
40
|
-
const {
|
|
38
|
+
const { getSteps } = useIntegrationsRuns();
|
|
41
39
|
const t = useIntegrationsT();
|
|
42
40
|
const TERMINAL = /* @__PURE__ */ new Set(["completed", "succeeded", "failed", "cancelled", "terminated"]);
|
|
43
41
|
const STEPS_POLL_MS = 4e3;
|
|
@@ -46,33 +44,7 @@ const { delivering: realtimeDelivering, runTick, lastRunEvent } = useIntegration
|
|
|
46
44
|
function stepsPollDelay() {
|
|
47
45
|
return realtimeDelivering.value ? SAFETY_POLL_MS : STEPS_POLL_MS;
|
|
48
46
|
}
|
|
49
|
-
const runs = ref([]);
|
|
50
|
-
const runsPage = ref(1);
|
|
51
|
-
const runsLastPage = ref(1);
|
|
52
|
-
const runsLoading = ref(false);
|
|
53
47
|
const selectedRunId = ref(null);
|
|
54
|
-
async function loadRuns(reset = false) {
|
|
55
|
-
if (runsLoading.value) return;
|
|
56
|
-
runsLoading.value = true;
|
|
57
|
-
try {
|
|
58
|
-
const res = await loadForWorkflow(props.workflow.id, props.workflow.name, {
|
|
59
|
-
page: reset ? 1 : runsPage.value,
|
|
60
|
-
reconcile: true
|
|
61
|
-
});
|
|
62
|
-
runs.value = reset ? res.runs : [...runs.value, ...res.runs];
|
|
63
|
-
runsPage.value = res.page;
|
|
64
|
-
runsLastPage.value = res.lastPage;
|
|
65
|
-
} catch (e) {
|
|
66
|
-
toast.error(errorReason(e, t("integration.replay.toast.loadRunsFailed")));
|
|
67
|
-
} finally {
|
|
68
|
-
runsLoading.value = false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
function loadMoreRuns() {
|
|
72
|
-
if (runsPage.value >= runsLastPage.value) return;
|
|
73
|
-
runsPage.value += 1;
|
|
74
|
-
void loadRuns();
|
|
75
|
-
}
|
|
76
48
|
const steps = ref([]);
|
|
77
49
|
const stepsSource = ref(null);
|
|
78
50
|
const runStatus = ref(null);
|
|
@@ -382,14 +354,6 @@ const edgeTypes = {
|
|
|
382
354
|
preview.onNodeClick((evt) => {
|
|
383
355
|
if (evt.node.type === "workflow") onNodeClick(evt.node.id);
|
|
384
356
|
});
|
|
385
|
-
function formatDateTime(iso) {
|
|
386
|
-
if (!iso) return "\u2014";
|
|
387
|
-
try {
|
|
388
|
-
return new Date(iso).toLocaleString();
|
|
389
|
-
} catch {
|
|
390
|
-
return String(iso);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
357
|
function formatTime(iso) {
|
|
394
358
|
if (!iso) return "\u2014";
|
|
395
359
|
try {
|
|
@@ -405,30 +369,16 @@ function pretty(v) {
|
|
|
405
369
|
return String(v);
|
|
406
370
|
}
|
|
407
371
|
}
|
|
408
|
-
function triggerIcon(type) {
|
|
409
|
-
return triggerGlyph(type);
|
|
410
|
-
}
|
|
411
372
|
const activeStepTitle = computed(() => {
|
|
412
373
|
const s = activeStep.value;
|
|
413
374
|
if (!s) return "";
|
|
414
375
|
return `${s.node_slug || s.node_id}${s.attempt > 0 ? ` (#${s.attempt + 1})` : ""}`;
|
|
415
376
|
});
|
|
416
|
-
onMounted(
|
|
417
|
-
if (props.
|
|
418
|
-
selectRun(props.initialRunId);
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
await loadRuns(true);
|
|
422
|
-
const initial = props.initialRunId && runs.value.some((r) => r.id === props.initialRunId) ? props.initialRunId : runs.value[0]?.id;
|
|
423
|
-
if (initial) selectRun(initial);
|
|
377
|
+
onMounted(() => {
|
|
378
|
+
if (props.initialRunId) selectRun(props.initialRunId);
|
|
424
379
|
});
|
|
425
380
|
watch(() => props.initialRunId, (id) => {
|
|
426
|
-
if (
|
|
427
|
-
if (props.single) {
|
|
428
|
-
selectRun(id);
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
if (runs.value.some((r) => r.id === id)) selectRun(id);
|
|
381
|
+
if (id) selectRun(id);
|
|
432
382
|
});
|
|
433
383
|
</script>
|
|
434
384
|
|
|
@@ -589,58 +539,6 @@ watch(() => props.initialRunId, (id) => {
|
|
|
589
539
|
</p>
|
|
590
540
|
</aside>
|
|
591
541
|
|
|
592
|
-
<!-- Run list — hidden in single mode (the run detail replays ONE run). -->
|
|
593
|
-
<aside v-if="!single" class="flex w-80 shrink-0 flex-col border-l border-black/[0.06] bg-white">
|
|
594
|
-
<div class="border-b border-black/[0.06] px-3 py-2">
|
|
595
|
-
<div class="text-sm font-semibold">{{ t("integration.replay.runsList.heading") }}</div>
|
|
596
|
-
<p class="text-sm text-muted-foreground">
|
|
597
|
-
{{ t("integration.replay.runsList.description") }}
|
|
598
|
-
</p>
|
|
599
|
-
</div>
|
|
600
|
-
|
|
601
|
-
<div class="flex-1 space-y-1 overflow-y-auto p-2">
|
|
602
|
-
<button
|
|
603
|
-
v-for="r in runs"
|
|
604
|
-
:key="r.id"
|
|
605
|
-
type="button"
|
|
606
|
-
class="w-full rounded-lg border p-2.5 text-left transition"
|
|
607
|
-
:class="selectedRunId === r.id ? 'border-primary/40 bg-primary/[0.04]' : 'border-transparent hover:border-black/[0.08] hover:bg-muted/30'"
|
|
608
|
-
@click="selectRun(r.id)"
|
|
609
|
-
>
|
|
610
|
-
<div class="flex items-center justify-between gap-2">
|
|
611
|
-
<span class="flex min-w-0 items-center gap-1.5 text-sm font-medium">
|
|
612
|
-
<component :is="triggerIcon(r.triggerType)" :size="12" class="shrink-0 text-muted-foreground" />
|
|
613
|
-
<span class="truncate">#{{ r.id }}</span>
|
|
614
|
-
</span>
|
|
615
|
-
<Badge variant="outline" class="shrink-0 border text-xs font-medium uppercase" :class="runStatusClass(r.status)">
|
|
616
|
-
{{ runStatusLabel(r.status) }}
|
|
617
|
-
</Badge>
|
|
618
|
-
</div>
|
|
619
|
-
<div class="mt-0.5 flex items-center justify-between gap-2 text-sm text-muted-foreground">
|
|
620
|
-
<span class="tabular-nums">{{ formatDateTime(r.startedAt) }}</span>
|
|
621
|
-
<span class="tabular-nums">{{ formatDuration(r.durationMs) }}</span>
|
|
622
|
-
</div>
|
|
623
|
-
</button>
|
|
624
|
-
|
|
625
|
-
<p v-if="!runs.length && !runsLoading" class="px-2 py-6 text-center text-sm text-muted-foreground">
|
|
626
|
-
{{ t("integration.replay.runsList.empty") }}
|
|
627
|
-
</p>
|
|
628
|
-
|
|
629
|
-
<div v-if="runsLoading" class="flex justify-center py-3">
|
|
630
|
-
<Loader2 :size="15" class="animate-spin text-muted-foreground" />
|
|
631
|
-
</div>
|
|
632
|
-
<Button
|
|
633
|
-
v-else-if="runsPage < runsLastPage"
|
|
634
|
-
variant="ghost"
|
|
635
|
-
size="sm"
|
|
636
|
-
class="w-full text-sm"
|
|
637
|
-
@click="loadMoreRuns"
|
|
638
|
-
>
|
|
639
|
-
{{ t("integration.replay.runsList.loadMore") }}
|
|
640
|
-
</Button>
|
|
641
|
-
</div>
|
|
642
|
-
</aside>
|
|
643
|
-
|
|
644
542
|
</div>
|
|
645
543
|
</template>
|
|
646
544
|
|
|
@@ -3,9 +3,13 @@ import '@vue-flow/core/dist/theme-default.css';
|
|
|
3
3
|
import type { Workflow } from '../../types/integrations.js';
|
|
4
4
|
type __VLS_Props = {
|
|
5
5
|
workflow: Workflow;
|
|
6
|
-
/**
|
|
6
|
+
/** The run to replay (deep-link from the Runs table). */
|
|
7
7
|
initialRunId?: string | null;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated Accepted and ignored since PO-350 — the panel has no other mode
|
|
10
|
+
* to switch out of. Kept so a host passing it keeps working; drop it at the
|
|
11
|
+
* next major.
|
|
12
|
+
*/
|
|
9
13
|
single?: boolean;
|
|
10
14
|
};
|
|
11
15
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
DropdownMenuItem,
|
|
15
15
|
DropdownMenuLabel,
|
|
16
16
|
DropdownMenuSeparator,
|
|
17
|
-
DropdownMenuTrigger
|
|
17
|
+
DropdownMenuTrigger,
|
|
18
|
+
Label,
|
|
19
|
+
Switch
|
|
18
20
|
} from "@revenexx/studio";
|
|
19
21
|
defineProps({
|
|
20
22
|
workflows: { type: Array, required: true },
|
|
@@ -247,16 +249,25 @@ const columns = computed(() => [
|
|
|
247
249
|
@row-click="(wf2) => router.push(`/integrations/workflows/${wf2.id}`)"
|
|
248
250
|
>
|
|
249
251
|
<template #toolbar-left>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
252
|
+
<!-- A switch rather than a button: the control is a state, not an action.
|
|
253
|
+
A button labelled "Group by tags" ungroups once grouping is on, and
|
|
254
|
+
said so neither to a reader nor to a screen reader — the state lived
|
|
255
|
+
in a colour class alone. The Label is what names the switch. -->
|
|
256
|
+
<div class="flex h-7 items-center gap-2">
|
|
257
|
+
<Switch
|
|
258
|
+
id="wf-group-by-tags"
|
|
259
|
+
:model-value="groupByTags"
|
|
260
|
+
@update:model-value="(on) => groupByTags = on"
|
|
261
|
+
/>
|
|
262
|
+
<Label
|
|
263
|
+
for="wf-group-by-tags"
|
|
264
|
+
class="flex cursor-pointer items-center gap-1.5 text-sm font-normal"
|
|
265
|
+
:class="groupByTags ? 'text-primary' : 'text-muted-foreground'"
|
|
266
|
+
>
|
|
267
|
+
<Layers :size="13" />
|
|
268
|
+
{{ t("integration.workflows.table.action.groupByTags") }}
|
|
269
|
+
</Label>
|
|
270
|
+
</div>
|
|
260
271
|
</template>
|
|
261
272
|
|
|
262
273
|
<!-- Name (icon + title + description) -->
|
|
@@ -32,6 +32,40 @@ export type ShortcutHandler = ((event: KeyboardEvent) => void) | {
|
|
|
32
32
|
run: (event: KeyboardEvent) => void;
|
|
33
33
|
};
|
|
34
34
|
export type ShortcutMap = Record<string, ShortcutHandler>;
|
|
35
|
+
/**
|
|
36
|
+
* A map key in its comparable form.
|
|
37
|
+
*
|
|
38
|
+
* Exported for the unit suite: the whole layout question below is decided here
|
|
39
|
+
* and in {@link shortcutCandidates}, and both are cheaper to prove against
|
|
40
|
+
* hand-built events than against a browser per keyboard layout.
|
|
41
|
+
*/
|
|
42
|
+
export declare function normalizeShortcut(shortcut: string): string;
|
|
43
|
+
/** Everything a match reads from a keydown — so the rule needs no DOM to prove. */
|
|
44
|
+
export interface ShortcutKeyEvent {
|
|
45
|
+
key: string;
|
|
46
|
+
code?: string;
|
|
47
|
+
shiftKey?: boolean;
|
|
48
|
+
altKey?: boolean;
|
|
49
|
+
metaKey?: boolean;
|
|
50
|
+
ctrlKey?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The two forms a keydown is looked up under.
|
|
54
|
+
*
|
|
55
|
+
* `byKey` is what the layout PRODUCED and `byCode` is which key was pressed. Both
|
|
56
|
+
* are needed and neither is enough: a Dvorak or AZERTY layout moves the letters,
|
|
57
|
+
* so a letter binding is only found by key — while a character a layout reaches
|
|
58
|
+
* through a modifier, or a dead key that produces no useful character at all
|
|
59
|
+
* (⌥⇧T is `ˇ`), is only found by code.
|
|
60
|
+
*
|
|
61
|
+
* Shift is dropped from `byKey` alone, where it is part of the character. `byCode`
|
|
62
|
+
* stays strict: there the physical key says nothing about what was produced, so
|
|
63
|
+
* Shift is the only thing separating `shift+s` from `s`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function shortcutCandidates(event: ShortcutKeyEvent, isMac?: boolean): {
|
|
66
|
+
byKey: string;
|
|
67
|
+
byCode: string;
|
|
68
|
+
};
|
|
35
69
|
export declare function useCanvasShortcuts(keymap: MaybeRefOrGetter<ShortcutMap>, options?: {
|
|
36
70
|
disabled?: MaybeRefOrGetter<boolean>;
|
|
37
71
|
}): void;
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import { computed, onBeforeUnmount, onMounted, toValue } from "vue";
|
|
2
|
-
|
|
2
|
+
import { IS_MAC, shortcutTokens } from "../utils/shortcutLabel.js";
|
|
3
3
|
function canonical(parts) {
|
|
4
4
|
return parts.map((p) => p.toLowerCase()).sort((a, b) => a.localeCompare(b)).join("+");
|
|
5
5
|
}
|
|
6
|
-
function normalizeShortcut(shortcut) {
|
|
7
|
-
|
|
8
|
-
return canonical(shortcut.split("+"));
|
|
6
|
+
export function normalizeShortcut(shortcut) {
|
|
7
|
+
return canonical(shortcutTokens(shortcut));
|
|
9
8
|
}
|
|
10
9
|
function codeToKey(code) {
|
|
11
10
|
if (code.startsWith("Key")) return code.slice(3).toLowerCase();
|
|
12
11
|
if (code.startsWith("Digit")) return code.slice(5);
|
|
13
12
|
return code.toLowerCase();
|
|
14
13
|
}
|
|
15
|
-
function
|
|
14
|
+
function shiftMadeTheCharacter(loweredKey) {
|
|
15
|
+
return loweredKey.length === 1 && !/^[\p{L}\p{N}]$/u.test(loweredKey);
|
|
16
|
+
}
|
|
17
|
+
export function shortcutCandidates(event, isMac = IS_MAC) {
|
|
16
18
|
const mods = [];
|
|
17
19
|
if (event.shiftKey) mods.push("shift");
|
|
18
20
|
if (event.altKey) mods.push("alt");
|
|
19
|
-
if (
|
|
21
|
+
if ((isMac ? event.metaKey : event.ctrlKey) ?? false) mods.push("mod");
|
|
20
22
|
const key = event.key === " " ? "space" : event.key.toLowerCase();
|
|
21
23
|
const code = codeToKey(event.code ?? "");
|
|
24
|
+
const keyMods = shiftMadeTheCharacter(key) ? mods.filter((m) => m !== "shift") : mods;
|
|
22
25
|
return {
|
|
23
|
-
byKey: canonical([...
|
|
26
|
+
byKey: canonical([...keyMods, key]),
|
|
24
27
|
byCode: canonical([...mods, code])
|
|
25
28
|
};
|
|
26
29
|
}
|
|
@@ -48,7 +51,7 @@ export function useCanvasShortcuts(keymap, options) {
|
|
|
48
51
|
if (toValue(options?.disabled)) return;
|
|
49
52
|
if (event.defaultPrevented) return;
|
|
50
53
|
if (isEditingContext(event)) return;
|
|
51
|
-
const { byKey, byCode } =
|
|
54
|
+
const { byKey, byCode } = shortcutCandidates(event);
|
|
52
55
|
const handler = normalized.value[byKey] ?? normalized.value[byCode];
|
|
53
56
|
if (!handler) return;
|
|
54
57
|
const run = typeof handler === "function" ? handler : handler.disabled() ? void 0 : handler.run;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
2
2
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
3
3
|
import { errorReason } from "../utils/apiError.js";
|
|
4
|
+
import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
|
|
4
5
|
import { useLoadGate } from "./useLoadGate.js";
|
|
5
6
|
export function useCredentialTypes() {
|
|
6
7
|
const types = useState("integrations.credentialTypes", () => []);
|
|
@@ -10,17 +11,11 @@ export function useCredentialTypes() {
|
|
|
10
11
|
const gate = useLoadGate("integrations.credentialTypes", { loading, loaded });
|
|
11
12
|
const api = useIntegrationsApi();
|
|
12
13
|
const t = useIntegrationsT();
|
|
13
|
-
function normalize(res) {
|
|
14
|
-
const obj = res;
|
|
15
|
-
if (Array.isArray(obj)) return obj;
|
|
16
|
-
if (Array.isArray(obj?.data)) return obj.data;
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
14
|
async function load(force = false) {
|
|
20
15
|
if (!await gate.claim(force)) return types.value;
|
|
21
16
|
error.value = null;
|
|
22
17
|
try {
|
|
23
|
-
types.value =
|
|
18
|
+
types.value = unwrapList(await api.get("/credential-types"));
|
|
24
19
|
loaded.value = true;
|
|
25
20
|
} catch (e) {
|
|
26
21
|
error.value = errorReason(e, t("integration.credentials.error.loadTypes"));
|
|
@@ -36,8 +31,7 @@ export function useCredentialTypes() {
|
|
|
36
31
|
const cached = bySlug(slug);
|
|
37
32
|
if (cached) return cached;
|
|
38
33
|
try {
|
|
39
|
-
|
|
40
|
-
return res?.data ?? res ?? null;
|
|
34
|
+
return unwrapRecord(await api.get(`/credential-types/${encodeURIComponent(slug)}`));
|
|
41
35
|
} catch {
|
|
42
36
|
return null;
|
|
43
37
|
}
|
|
@@ -2,6 +2,7 @@ import { computed } from "vue";
|
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
4
|
import { IntegrationsApiError, errorReason } from "../utils/apiError.js";
|
|
5
|
+
import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
|
|
5
6
|
import { useLoadGate } from "./useLoadGate.js";
|
|
6
7
|
export function useIntegrationsCredentials() {
|
|
7
8
|
const credentials = useState("integrations.credentials", () => []);
|
|
@@ -11,20 +12,11 @@ export function useIntegrationsCredentials() {
|
|
|
11
12
|
const gate = useLoadGate("integrations.credentials", { loading, loaded });
|
|
12
13
|
const api = useIntegrationsApi();
|
|
13
14
|
const t = useIntegrationsT();
|
|
14
|
-
function normalize(res) {
|
|
15
|
-
const obj = res;
|
|
16
|
-
if (Array.isArray(obj)) return obj;
|
|
17
|
-
if (Array.isArray(obj?.data)) return obj.data;
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
function unwrap(res) {
|
|
21
|
-
return res?.data ?? res;
|
|
22
|
-
}
|
|
23
15
|
async function load(force = false) {
|
|
24
16
|
if (!await gate.claim(force)) return credentials.value;
|
|
25
17
|
error.value = null;
|
|
26
18
|
try {
|
|
27
|
-
credentials.value =
|
|
19
|
+
credentials.value = unwrapList(await api.get("/credentials"));
|
|
28
20
|
loaded.value = true;
|
|
29
21
|
} catch (e) {
|
|
30
22
|
error.value = errorReason(e, t("integration.credentials.error.loadFailed"));
|
|
@@ -37,15 +29,15 @@ export function useIntegrationsCredentials() {
|
|
|
37
29
|
return credentials.value.filter((c) => c.credential_type_slug === slug);
|
|
38
30
|
}
|
|
39
31
|
async function get(id) {
|
|
40
|
-
return
|
|
32
|
+
return unwrapRecord(await api.get(`/credentials/${encodeURIComponent(id)}`));
|
|
41
33
|
}
|
|
42
34
|
async function create(input) {
|
|
43
|
-
const created =
|
|
35
|
+
const created = unwrapRecord(await api.post("/credentials", input));
|
|
44
36
|
await load(true);
|
|
45
37
|
return created;
|
|
46
38
|
}
|
|
47
39
|
async function update(id, input) {
|
|
48
|
-
const updated =
|
|
40
|
+
const updated = unwrapRecord(await api.patch(`/credentials/${encodeURIComponent(id)}`, input));
|
|
49
41
|
await load(true);
|
|
50
42
|
return updated;
|
|
51
43
|
}
|
|
@@ -71,7 +63,7 @@ export function useIntegrationsCredentials() {
|
|
|
71
63
|
}
|
|
72
64
|
async function oauthAuthorizeUrl(id) {
|
|
73
65
|
const res = await api.post(`/credentials/${encodeURIComponent(id)}/oauth/authorize-url`);
|
|
74
|
-
const authorizeUrl = res
|
|
66
|
+
const authorizeUrl = unwrapRecord(res).authorize_url;
|
|
75
67
|
if (!authorizeUrl) throw new IntegrationsApiError(0, res, t("integration.credentials.error.noAuthorizeUrl"));
|
|
76
68
|
return authorizeUrl;
|
|
77
69
|
}
|
|
@@ -3,6 +3,7 @@ import { resolveLocalized } from "../utils/localize.js";
|
|
|
3
3
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
4
4
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
5
5
|
import { errorReason } from "../utils/apiError.js";
|
|
6
|
+
import { unwrapList } from "../utils/apiEnvelope.js";
|
|
6
7
|
import { useLoadGate } from "./useLoadGate.js";
|
|
7
8
|
const RETRY_COOLDOWN_MS = 3e4;
|
|
8
9
|
export function useIntegrationsNodes() {
|
|
@@ -18,8 +19,7 @@ export function useIntegrationsNodes() {
|
|
|
18
19
|
if (!await gate.claim(force)) return;
|
|
19
20
|
error.value = null;
|
|
20
21
|
try {
|
|
21
|
-
const
|
|
22
|
-
const list = Array.isArray(res) ? res : res?.data ?? [];
|
|
22
|
+
const list = unwrapList(await api.get("/nodes"));
|
|
23
23
|
const manifests = list.filter((n) => n.manifest).map((n) => ({
|
|
24
24
|
...n.manifest,
|
|
25
25
|
slug: n.manifest.slug ?? n.slug,
|
|
@@ -2,6 +2,8 @@ import { computed } from "vue";
|
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
4
|
import { errorReason } from "../utils/apiError.js";
|
|
5
|
+
import { unwrapRecord } from "../utils/apiEnvelope.js";
|
|
6
|
+
import { RUN_OWN_FIELDS } from "../utils/runEnvelope.js";
|
|
5
7
|
import { useLoadGate } from "./useLoadGate.js";
|
|
6
8
|
import {
|
|
7
9
|
DEFAULT_RUN_DIRECTION,
|
|
@@ -93,9 +95,7 @@ export function useIntegrationsRuns() {
|
|
|
93
95
|
const active = runs.value.filter((r) => r.status === "running" || r.status === "queued").sort((a, b) => Number(b.id) - Number(a.id)).slice(0, SYNC_LIMIT);
|
|
94
96
|
await Promise.allSettled(active.map(async (r) => {
|
|
95
97
|
try {
|
|
96
|
-
const
|
|
97
|
-
const apiRun = raw?.data ?? raw;
|
|
98
|
-
const fresh = mapRun(apiRun, r.workflowName);
|
|
98
|
+
const fresh = await fetchRun(r.workflowId, r.id, r.workflowName);
|
|
99
99
|
runs.value = runs.value.map((x) => x.id === fresh.id ? fresh : x);
|
|
100
100
|
} catch {
|
|
101
101
|
}
|
|
@@ -124,9 +124,7 @@ export function useIntegrationsRuns() {
|
|
|
124
124
|
const active = list2.filter((r) => r.status === "running" || r.status === "queued").sort((a, b) => Number(b.id) - Number(a.id)).slice(0, SYNC_LIMIT);
|
|
125
125
|
await Promise.allSettled(active.map(async (r) => {
|
|
126
126
|
try {
|
|
127
|
-
const
|
|
128
|
-
const apiRun = raw?.data ?? raw;
|
|
129
|
-
const fresh = mapRun(apiRun, workflowName);
|
|
127
|
+
const fresh = await fetchRun(workflowId, r.id, workflowName);
|
|
130
128
|
list2 = list2.map((x) => x.id === fresh.id ? fresh : x);
|
|
131
129
|
} catch {
|
|
132
130
|
}
|
|
@@ -185,13 +183,11 @@ export function useIntegrationsRuns() {
|
|
|
185
183
|
async function trigger(workflowId, payload = {}, mode) {
|
|
186
184
|
const body = { trigger_payload: payload };
|
|
187
185
|
if (mode) body.mode = mode;
|
|
188
|
-
|
|
189
|
-
return res?.data ?? res;
|
|
186
|
+
return unwrapRecord(await api.post(`/workflows/${workflowId}/runs`, body));
|
|
190
187
|
}
|
|
191
188
|
async function runAction(workflowId, runId, action, reason) {
|
|
192
189
|
const body = reason ? { reason } : {};
|
|
193
|
-
|
|
194
|
-
return res?.data ?? res;
|
|
190
|
+
return unwrapRecord(await api.post(`/workflows/${workflowId}/runs/${runId}/${action}`, body));
|
|
195
191
|
}
|
|
196
192
|
const cancelRun = (workflowId, runId, reason) => runAction(workflowId, runId, "cancel", reason);
|
|
197
193
|
const terminateRun = (workflowId, runId, reason) => runAction(workflowId, runId, "terminate", reason);
|
|
@@ -199,13 +195,12 @@ export function useIntegrationsRuns() {
|
|
|
199
195
|
const resumeRun = (workflowId, runId, reason) => runAction(workflowId, runId, "resume", reason);
|
|
200
196
|
const getRun = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}`);
|
|
201
197
|
async function fetchRun(workflowId, runId, workflowName) {
|
|
202
|
-
const
|
|
203
|
-
const apiRun = raw?.data ?? raw;
|
|
198
|
+
const apiRun = unwrapRecord(await getRun(workflowId, runId), RUN_OWN_FIELDS);
|
|
204
199
|
return mapRun(apiRun, workflowName);
|
|
205
200
|
}
|
|
206
201
|
const getDetails = (workflowId, runId) => api.get(`/workflows/${workflowId}/runs/${runId}/details`);
|
|
207
202
|
const getSteps = (workflowId, runId, includePayloads = false) => api.get(
|
|
208
|
-
`/workflows/${workflowId}/runs/${runId}/steps
|
|
203
|
+
`/workflows/${workflowId}/runs/${runId}/steps?include_payloads=${includePayloads ? "1" : "0"}`
|
|
209
204
|
);
|
|
210
205
|
const getHistory = (workflowId, runId, params = {}) => {
|
|
211
206
|
const qs = new URLSearchParams();
|
|
@@ -2,6 +2,7 @@ import { computed } from "vue";
|
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
4
|
import { errorReason } from "../utils/apiError.js";
|
|
5
|
+
import { unwrapList } from "../utils/apiEnvelope.js";
|
|
5
6
|
import { useLoadGate } from "./useLoadGate.js";
|
|
6
7
|
const KEY_PATTERN = /^[A-Z0-9_]+$/;
|
|
7
8
|
export function useIntegrationsSecrets() {
|
|
@@ -17,11 +18,10 @@ export function useIntegrationsSecrets() {
|
|
|
17
18
|
}
|
|
18
19
|
function normalize(res) {
|
|
19
20
|
const obj = res;
|
|
20
|
-
if (Array.isArray(obj?.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return [];
|
|
21
|
+
if (obj?.data === void 0 && Array.isArray(obj?.keys)) {
|
|
22
|
+
return obj.keys.map((key) => ({ key, created_at: "", updated_at: "" }));
|
|
23
|
+
}
|
|
24
|
+
return unwrapList(res).map(toRecord);
|
|
25
25
|
}
|
|
26
26
|
function toRecord(r) {
|
|
27
27
|
return { key: r.key ?? "", created_at: r.created_at ?? "", updated_at: r.updated_at ?? "" };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { errorReason } from "../utils/apiError.js";
|
|
4
|
+
import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
|
|
4
5
|
import { resolveLocalized } from "../utils/localize.js";
|
|
5
6
|
import { useLoadGate } from "./useLoadGate.js";
|
|
6
7
|
const BLOB_VERSION = "v0-draft";
|
|
@@ -102,14 +103,11 @@ export function useIntegrationsTemplates() {
|
|
|
102
103
|
const error = useState("integrations.templates.error", () => null);
|
|
103
104
|
const gate = useLoadGate("integrations.templates", { loading, loaded });
|
|
104
105
|
const api = useIntegrationsApi();
|
|
105
|
-
function unwrap(res) {
|
|
106
|
-
return res?.data ?? res;
|
|
107
|
-
}
|
|
108
106
|
async function load(force = false) {
|
|
109
107
|
if (!await gate.claim(force)) return templates.value;
|
|
110
108
|
error.value = null;
|
|
111
109
|
try {
|
|
112
|
-
const list2 =
|
|
110
|
+
const list2 = unwrapList(await api.get("/templates"));
|
|
113
111
|
rawList.value = list2;
|
|
114
112
|
rawDetails.value = {};
|
|
115
113
|
loaded.value = true;
|
|
@@ -133,7 +131,7 @@ export function useIntegrationsTemplates() {
|
|
|
133
131
|
const base = get(slug);
|
|
134
132
|
if (!base?.fromApi) return base;
|
|
135
133
|
try {
|
|
136
|
-
const detail =
|
|
134
|
+
const detail = unwrapRecord(await api.get(`/templates/${slug}`));
|
|
137
135
|
rawDetails.value = { ...rawDetails.value, [slug]: detail };
|
|
138
136
|
return get(slug);
|
|
139
137
|
} catch {
|
|
@@ -152,8 +150,7 @@ export function useIntegrationsTemplates() {
|
|
|
152
150
|
}
|
|
153
151
|
async function requirements(slug) {
|
|
154
152
|
try {
|
|
155
|
-
const raw =
|
|
156
|
-
if (!raw) return null;
|
|
153
|
+
const raw = unwrapRecord(await api.get(`/templates/${slug}/requirements`));
|
|
157
154
|
return {
|
|
158
155
|
...raw,
|
|
159
156
|
credentialTypes: (raw.credentialTypes ?? []).map((c) => ({
|
|
@@ -172,7 +169,7 @@ export function useIntegrationsTemplates() {
|
|
|
172
169
|
Object.entries(opts.credentialBindings ?? {}).filter(([, v]) => v)
|
|
173
170
|
);
|
|
174
171
|
if (Object.keys(bindings).length) body.credential_bindings = bindings;
|
|
175
|
-
const created =
|
|
172
|
+
const created = unwrapRecord(await api.post(`/templates/${slug}/instantiate`, body));
|
|
176
173
|
return Number(created.id);
|
|
177
174
|
}
|
|
178
175
|
async function install(slug) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
2
2
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
3
3
|
import { errorReason } from "../utils/apiError.js";
|
|
4
|
+
import { unwrapList } from "../utils/apiEnvelope.js";
|
|
4
5
|
import { useLoadGate } from "./useLoadGate.js";
|
|
5
6
|
export function tenantTriggers(workflows, byWorkflow) {
|
|
6
7
|
return workflows.flatMap((wf) => (byWorkflow[wf.id] ?? []).flatMap((t) => {
|
|
@@ -24,10 +25,6 @@ export function useIntegrationsTriggers() {
|
|
|
24
25
|
const t = useIntegrationsT();
|
|
25
26
|
const { workflows, apiTriggers, load: loadWorkflows, loaded: workflowsLoaded, error: workflowsError } = useIntegrationsWorkflows();
|
|
26
27
|
const gate = useLoadGate("integrations.triggers", { loading, loaded });
|
|
27
|
-
function unwrapList(res) {
|
|
28
|
-
if (Array.isArray(res)) return res;
|
|
29
|
-
return res?.data ?? [];
|
|
30
|
-
}
|
|
31
28
|
async function list(workflowId) {
|
|
32
29
|
return unwrapList(await api.get(`/workflows/${workflowId}/triggers`));
|
|
33
30
|
}
|
|
@@ -3,6 +3,7 @@ import { useIntegrationsApi } from "./integrationsApi.js";
|
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
4
|
import { deepClone } from "../utils/deepClone.js";
|
|
5
5
|
import { errorReason } from "../utils/apiError.js";
|
|
6
|
+
import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
|
|
6
7
|
import { useLoadGate } from "./useLoadGate.js";
|
|
7
8
|
const BLOB_VERSION = "v0-draft";
|
|
8
9
|
function toCockpit(api) {
|
|
@@ -93,9 +94,6 @@ export function useIntegrationsWorkflows() {
|
|
|
93
94
|
const gate = useLoadGate("integrations.workflows", { loading, loaded });
|
|
94
95
|
const api = useIntegrationsApi();
|
|
95
96
|
const t = useIntegrationsT();
|
|
96
|
-
function unwrap(res) {
|
|
97
|
-
return res?.data ?? res;
|
|
98
|
-
}
|
|
99
97
|
function remember(api2, into) {
|
|
100
98
|
if (api2.triggers !== void 0) into[Number(api2.id)] = api2.triggers;
|
|
101
99
|
}
|
|
@@ -122,7 +120,7 @@ export function useIntegrationsWorkflows() {
|
|
|
122
120
|
if (!await gate.claim(force)) return workflows.value;
|
|
123
121
|
error.value = null;
|
|
124
122
|
try {
|
|
125
|
-
const list2 =
|
|
123
|
+
const list2 = unwrapList(await api.get("/workflows"));
|
|
126
124
|
workflows.value = ingestList(list2);
|
|
127
125
|
loaded.value = true;
|
|
128
126
|
} catch (e) {
|
|
@@ -139,7 +137,7 @@ export function useIntegrationsWorkflows() {
|
|
|
139
137
|
}
|
|
140
138
|
async function get(id) {
|
|
141
139
|
try {
|
|
142
|
-
const wf = ingest(
|
|
140
|
+
const wf = ingest(unwrapRecord(await api.get(`/workflows/${id}`)));
|
|
143
141
|
upsertCache(wf);
|
|
144
142
|
return wf;
|
|
145
143
|
} catch {
|
|
@@ -147,7 +145,7 @@ export function useIntegrationsWorkflows() {
|
|
|
147
145
|
}
|
|
148
146
|
}
|
|
149
147
|
async function create(input) {
|
|
150
|
-
const wf = ingest(
|
|
148
|
+
const wf = ingest(unwrapRecord(await api.post("/workflows", toPayload({
|
|
151
149
|
name: input.name,
|
|
152
150
|
description: input.description,
|
|
153
151
|
blob: input.blob,
|
|
@@ -198,7 +196,7 @@ export function useIntegrationsWorkflows() {
|
|
|
198
196
|
throw lastError instanceof Error ? lastError : new Error(t("integration.workflows.error.duplicateFailed"));
|
|
199
197
|
}
|
|
200
198
|
async function update(id, patch) {
|
|
201
|
-
const wf = ingest(
|
|
199
|
+
const wf = ingest(unwrapRecord(await api.put(`/workflows/${id}`, toPayload({
|
|
202
200
|
name: patch.name,
|
|
203
201
|
description: patch.description,
|
|
204
202
|
blob: patch.blob,
|