@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
|
@@ -21,7 +21,6 @@ const {
|
|
|
21
21
|
getStackTrace,
|
|
22
22
|
cancelRun,
|
|
23
23
|
terminateRun,
|
|
24
|
-
retryRun,
|
|
25
24
|
resumeRun
|
|
26
25
|
} = useIntegrationsRuns();
|
|
27
26
|
const DETAILS_POLL_MS = 3e3;
|
|
@@ -41,8 +40,10 @@ const detailsError = ref(null);
|
|
|
41
40
|
const steps = ref([]);
|
|
42
41
|
const stepsLoading = ref(false);
|
|
43
42
|
const stepsError = ref(null);
|
|
44
|
-
const stepsWithPayloads = ref(false);
|
|
45
43
|
const expandedStep = ref(null);
|
|
44
|
+
const payloadsLoading = ref(false);
|
|
45
|
+
const payloadsError = ref(null);
|
|
46
|
+
const payloadCache = ref(/* @__PURE__ */ new Map());
|
|
46
47
|
const historyLoading = ref(false);
|
|
47
48
|
const historyError = ref(null);
|
|
48
49
|
const stackEnabled = ref(false);
|
|
@@ -82,6 +83,7 @@ const temporalWorkflowId = computed(() => pickStr(runData.value, ["temporal_work
|
|
|
82
83
|
const temporalRunId = computed(() => pickStr(runData.value, ["temporal_run_id", "temporalRunId"]) ?? props.run.temporalRunId ?? null);
|
|
83
84
|
const hasTemporal = computed(() => !!temporalWorkflowId.value && !!temporalRunId.value);
|
|
84
85
|
const runResult = computed(() => pick(runData.value, ["result"]));
|
|
86
|
+
const MAX_RESULT_LINKS = 50;
|
|
85
87
|
const resultLinks = computed(() => {
|
|
86
88
|
const out = [];
|
|
87
89
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -100,7 +102,7 @@ const resultLinks = computed(() => {
|
|
|
100
102
|
}
|
|
101
103
|
};
|
|
102
104
|
walk(runResult.value, []);
|
|
103
|
-
return out.
|
|
105
|
+
return out.length > MAX_RESULT_LINKS ? [] : out;
|
|
104
106
|
});
|
|
105
107
|
const hasResult = computed(() => runResult.value != null && (typeof runResult.value !== "object" || Object.keys(runResult.value).length > 0));
|
|
106
108
|
const isTerminal = computed(() => {
|
|
@@ -303,6 +305,24 @@ async function loadDetails() {
|
|
|
303
305
|
detailsLoading.value = false;
|
|
304
306
|
}
|
|
305
307
|
}
|
|
308
|
+
function stepKey(st, i) {
|
|
309
|
+
return `${st.node_id}-${st.scheduled_event_id ?? i}`;
|
|
310
|
+
}
|
|
311
|
+
function cachedPayload(st) {
|
|
312
|
+
return st.scheduled_event_id === null ? void 0 : payloadCache.value.get(st.scheduled_event_id);
|
|
313
|
+
}
|
|
314
|
+
function hasPayload(st) {
|
|
315
|
+
return cachedPayload(st) !== void 0;
|
|
316
|
+
}
|
|
317
|
+
function withCachedPayload(st) {
|
|
318
|
+
const cached = cachedPayload(st);
|
|
319
|
+
if (!cached) return st;
|
|
320
|
+
return { ...st, input: cached.input, output: cached.output, config: cached.config, branch: cached.branch };
|
|
321
|
+
}
|
|
322
|
+
function openStep() {
|
|
323
|
+
if (expandedStep.value === null) return null;
|
|
324
|
+
return steps.value.find((st, i) => stepKey(st, i) === expandedStep.value) ?? null;
|
|
325
|
+
}
|
|
306
326
|
async function loadSteps() {
|
|
307
327
|
if (!hasTemporal.value) {
|
|
308
328
|
steps.value = [];
|
|
@@ -312,22 +332,55 @@ async function loadSteps() {
|
|
|
312
332
|
stepsLoading.value = true;
|
|
313
333
|
stepsError.value = null;
|
|
314
334
|
try {
|
|
315
|
-
const res = await getSteps(props.run.workflowId, props.run.id,
|
|
316
|
-
steps.value = res?.steps ?? [];
|
|
335
|
+
const res = await getSteps(props.run.workflowId, props.run.id, false);
|
|
336
|
+
steps.value = (res?.steps ?? []).map(withCachedPayload);
|
|
317
337
|
} catch (e) {
|
|
318
338
|
stepsError.value = stepsErrMsg(e);
|
|
319
339
|
steps.value = [];
|
|
320
340
|
} finally {
|
|
321
341
|
stepsLoading.value = false;
|
|
322
342
|
}
|
|
343
|
+
void loadPayloads(false);
|
|
344
|
+
}
|
|
345
|
+
async function loadPayloads(opened) {
|
|
346
|
+
if (!hasTemporal.value || payloadsLoading.value) return;
|
|
347
|
+
const st = openStep();
|
|
348
|
+
if (!st) return;
|
|
349
|
+
const cached = cachedPayload(st);
|
|
350
|
+
if (cached && cached.status === st.status) return;
|
|
351
|
+
if (!cached && !opened) return;
|
|
352
|
+
const runId = props.run.id;
|
|
353
|
+
const readFor = expandedStep.value;
|
|
354
|
+
payloadsLoading.value = true;
|
|
355
|
+
payloadsError.value = null;
|
|
356
|
+
try {
|
|
357
|
+
const res = await getSteps(props.run.workflowId, runId, true);
|
|
358
|
+
if (props.run.id !== runId) return;
|
|
359
|
+
const next = new Map(payloadCache.value);
|
|
360
|
+
for (const row of res?.steps ?? []) {
|
|
361
|
+
if (row.scheduled_event_id === null) continue;
|
|
362
|
+
next.set(row.scheduled_event_id, {
|
|
363
|
+
status: row.status,
|
|
364
|
+
input: row.input,
|
|
365
|
+
output: row.output,
|
|
366
|
+
config: row.config,
|
|
367
|
+
branch: row.branch
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
payloadCache.value = next;
|
|
371
|
+
steps.value = steps.value.map(withCachedPayload);
|
|
372
|
+
} catch (e) {
|
|
373
|
+
if (props.run.id !== runId) return;
|
|
374
|
+
payloadsError.value = stepsErrMsg(e);
|
|
375
|
+
} finally {
|
|
376
|
+
if (props.run.id === runId) payloadsLoading.value = false;
|
|
377
|
+
}
|
|
378
|
+
if (expandedStep.value !== readFor) void loadPayloads(true);
|
|
323
379
|
}
|
|
324
|
-
function
|
|
325
|
-
|
|
326
|
-
expandedStep.value = null;
|
|
327
|
-
void
|
|
328
|
-
}
|
|
329
|
-
function toggleStep(i) {
|
|
330
|
-
expandedStep.value = expandedStep.value === i ? null : i;
|
|
380
|
+
function toggleStep(st, i) {
|
|
381
|
+
const key = stepKey(st, i);
|
|
382
|
+
expandedStep.value = expandedStep.value === key ? null : key;
|
|
383
|
+
if (expandedStep.value !== null) void loadPayloads(true);
|
|
331
384
|
}
|
|
332
385
|
async function loadStack() {
|
|
333
386
|
stackEnabled.value = true;
|
|
@@ -422,8 +475,10 @@ function resetState() {
|
|
|
422
475
|
detailsError.value = null;
|
|
423
476
|
steps.value = [];
|
|
424
477
|
stepsError.value = null;
|
|
425
|
-
stepsWithPayloads.value = false;
|
|
426
478
|
expandedStep.value = null;
|
|
479
|
+
payloadsLoading.value = false;
|
|
480
|
+
payloadsError.value = null;
|
|
481
|
+
payloadCache.value = /* @__PURE__ */ new Map();
|
|
427
482
|
historyError.value = null;
|
|
428
483
|
timelineEvents.value = [];
|
|
429
484
|
timelineDetail.value = null;
|
|
@@ -441,8 +496,6 @@ function actionLabel(a) {
|
|
|
441
496
|
return t("integration.runs.action.cancel");
|
|
442
497
|
case "terminate":
|
|
443
498
|
return t("integration.runs.action.terminate");
|
|
444
|
-
case "retry":
|
|
445
|
-
return t("integration.runs.action.retry");
|
|
446
499
|
case "resume":
|
|
447
500
|
return t("integration.runs.action.resume");
|
|
448
501
|
}
|
|
@@ -453,8 +506,6 @@ function confirmQuestion(a) {
|
|
|
453
506
|
return t("integration.runs.confirm.cancel");
|
|
454
507
|
case "terminate":
|
|
455
508
|
return t("integration.runs.confirm.terminate");
|
|
456
|
-
case "retry":
|
|
457
|
-
return t("integration.runs.confirm.retry");
|
|
458
509
|
case "resume":
|
|
459
510
|
return t("integration.runs.confirm.resume");
|
|
460
511
|
}
|
|
@@ -465,8 +516,6 @@ function actionErrorFallback(a) {
|
|
|
465
516
|
return t("integration.runs.error.cancel");
|
|
466
517
|
case "terminate":
|
|
467
518
|
return t("integration.runs.error.terminate");
|
|
468
|
-
case "retry":
|
|
469
|
-
return t("integration.runs.error.retry");
|
|
470
519
|
case "resume":
|
|
471
520
|
return t("integration.runs.error.resume");
|
|
472
521
|
}
|
|
@@ -488,6 +537,10 @@ watch(() => props.run.id, () => {
|
|
|
488
537
|
resetState();
|
|
489
538
|
void loadAll().then(startPollingIfNeeded);
|
|
490
539
|
}, { immediate: true });
|
|
540
|
+
watch(apiStatusRaw, (now, before) => {
|
|
541
|
+
if (!now || !before || now === before) return;
|
|
542
|
+
emit("changed");
|
|
543
|
+
});
|
|
491
544
|
onUnmounted(stopPolling);
|
|
492
545
|
function arm(a) {
|
|
493
546
|
armed.value = a;
|
|
@@ -511,15 +564,12 @@ async function confirmAction() {
|
|
|
511
564
|
case "terminate":
|
|
512
565
|
res = await terminateRun(props.run.workflowId, props.run.id);
|
|
513
566
|
break;
|
|
514
|
-
case "retry":
|
|
515
|
-
res = await retryRun(props.run.workflowId, props.run.id);
|
|
516
|
-
break;
|
|
517
567
|
case "resume":
|
|
518
568
|
res = await resumeRun(props.run.workflowId, props.run.id);
|
|
519
569
|
break;
|
|
520
570
|
}
|
|
521
571
|
const newId = pickStr(res, ["id"]);
|
|
522
|
-
actionNotice.value =
|
|
572
|
+
actionNotice.value = a === "resume" && newId && newId !== String(props.run.id) ? t("integration.runs.notice.started", { id: newId }) : t("integration.runs.notice.requested", { action: actionLabel(a) });
|
|
523
573
|
armed.value = null;
|
|
524
574
|
await loadAll();
|
|
525
575
|
startPollingIfNeeded();
|
|
@@ -537,7 +587,7 @@ async function confirmAction() {
|
|
|
537
587
|
<!-- ── Details view: overview strip + result. ─────────────────────────── -->
|
|
538
588
|
<template v-if="view === 'details'">
|
|
539
589
|
<!-- Toolbar — only the status-gated stop/resume actions; hidden entirely
|
|
540
|
-
for a terminal run (
|
|
590
|
+
for a terminal run (Run again sits in the page header, data auto-polls). -->
|
|
541
591
|
<div v-if="hasActions" class="flex items-center gap-2">
|
|
542
592
|
<!-- confirm step: one click arms, second confirms -->
|
|
543
593
|
<template v-if="armed">
|
|
@@ -668,10 +718,6 @@ async function confirmAction() {
|
|
|
668
718
|
<section v-if="hasTemporal" class="rounded-md border border-black/[0.06] bg-white p-4 space-y-3">
|
|
669
719
|
<div class="flex items-center gap-2 border-b border-black/[0.06] pb-2">
|
|
670
720
|
<h4 class="font-mono text-xs uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.steps.title") }}</h4>
|
|
671
|
-
<div class="flex-1" />
|
|
672
|
-
<Button variant="outline" size="sm" :disabled="stepsLoading" @click="toggleStepPayloads">
|
|
673
|
-
{{ stepsWithPayloads ? t("integration.runs.detail.steps.action.hidePayloads") : t("integration.runs.detail.steps.action.showPayloads") }}
|
|
674
|
-
</Button>
|
|
675
721
|
</div>
|
|
676
722
|
|
|
677
723
|
<p v-if="stepsError" class="text-rose-700">{{ stepsError }}</p>
|
|
@@ -688,7 +734,7 @@ async function confirmAction() {
|
|
|
688
734
|
<button
|
|
689
735
|
type="button"
|
|
690
736
|
class="w-full flex items-center gap-3 px-3 py-2 text-left"
|
|
691
|
-
@click="toggleStep(i)"
|
|
737
|
+
@click="toggleStep(st, i)"
|
|
692
738
|
>
|
|
693
739
|
<Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0', runStatusClass(st.status)]">{{ runStatusLabel(st.status) }}</Badge>
|
|
694
740
|
<div class="min-w-0 flex-1">
|
|
@@ -698,13 +744,13 @@ async function confirmAction() {
|
|
|
698
744
|
</div>
|
|
699
745
|
</div>
|
|
700
746
|
<span class="text-xs text-muted-foreground tabular-nums shrink-0">{{ fmtMs(st.duration_ms) }}</span>
|
|
701
|
-
<span class="text-[10px] text-muted-foreground shrink-0">{{ expandedStep === i ? "\u25BC" : "\u25B6" }}</span>
|
|
747
|
+
<span class="text-[10px] text-muted-foreground shrink-0">{{ expandedStep === stepKey(st, i) ? "\u25BC" : "\u25B6" }}</span>
|
|
702
748
|
</button>
|
|
703
749
|
<div v-if="st.error" class="px-3 pb-2 -mt-1">
|
|
704
750
|
<div class="rounded-md border border-rose-500/20 bg-rose-500/5 px-2 py-1 text-xs text-rose-700 break-words">{{ st.error }}</div>
|
|
705
751
|
</div>
|
|
706
|
-
<div v-if="expandedStep === i" class="px-3 pb-3 space-y-2">
|
|
707
|
-
<template v-if="
|
|
752
|
+
<div v-if="expandedStep === stepKey(st, i)" class="px-3 pb-3 space-y-2">
|
|
753
|
+
<template v-if="hasPayload(st)">
|
|
708
754
|
<div class="space-y-1">
|
|
709
755
|
<div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.steps.field.input") }}</div>
|
|
710
756
|
<pre class="rounded-md bg-muted/40 p-3 text-xs overflow-x-auto max-h-40">{{ st.input === void 0 ? "\u2014" : json(st.input) }}</pre>
|
|
@@ -714,8 +760,9 @@ async function confirmAction() {
|
|
|
714
760
|
<pre class="rounded-md bg-muted/40 p-3 text-xs overflow-x-auto max-h-40">{{ st.output === void 0 ? "\u2014" : json(st.output) }}</pre>
|
|
715
761
|
</div>
|
|
716
762
|
</template>
|
|
763
|
+
<p v-else-if="payloadsError" class="text-xs text-rose-700">{{ payloadsError }}</p>
|
|
717
764
|
<p v-else class="text-xs text-muted-foreground">
|
|
718
|
-
{{ t("integration.runs.detail.steps.
|
|
765
|
+
{{ t("integration.runs.detail.steps.payloadsLoading") }}
|
|
719
766
|
</p>
|
|
720
767
|
</div>
|
|
721
768
|
</li>
|
|
@@ -5,10 +5,6 @@ type __VLS_Props = {
|
|
|
5
5
|
density: 'full' | 'compact';
|
|
6
6
|
/** Given → the card is a link there. Omitted → a button that emits `select`. */
|
|
7
7
|
to?: string;
|
|
8
|
-
/** This card is the one being acted on: it says so and cannot be pressed again. */
|
|
9
|
-
busy?: boolean;
|
|
10
|
-
/** Another card on the surface is being acted on, so this one is out of reach. */
|
|
11
|
-
disabled?: boolean;
|
|
12
8
|
};
|
|
13
9
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
14
10
|
select: () => any;
|
|
@@ -7,9 +7,7 @@ import TemplateFlowPreview from "./TemplateFlowPreview.vue";
|
|
|
7
7
|
const props = defineProps({
|
|
8
8
|
template: { type: Object, required: true },
|
|
9
9
|
density: { type: String, required: true },
|
|
10
|
-
to: { type: String, required: false }
|
|
11
|
-
busy: { type: Boolean, required: false },
|
|
12
|
-
disabled: { type: Boolean, required: false }
|
|
10
|
+
to: { type: String, required: false }
|
|
13
11
|
});
|
|
14
12
|
const emit = defineEmits(["select"]);
|
|
15
13
|
const t = useIntegrationsT();
|
|
@@ -40,8 +38,8 @@ const chipSize = computed(() => full.value ? "px-2 py-0.5 text-xs" : "px-1.5 py-
|
|
|
40
38
|
<template>
|
|
41
39
|
<component
|
|
42
40
|
:is="to ? NuxtLink : 'button'"
|
|
43
|
-
v-bind="to ? { to } : { type: 'button'
|
|
44
|
-
class="flex flex-col overflow-hidden rounded-lg border border-black/[0.06] bg-white text-left transition hover:border-black/20 hover:shadow-sm
|
|
41
|
+
v-bind="to ? { to } : { type: 'button' }"
|
|
42
|
+
class="flex flex-col overflow-hidden rounded-lg border border-black/[0.06] bg-white text-left transition hover:border-black/20 hover:shadow-sm"
|
|
45
43
|
@click="to ? void 0 : emit('select')"
|
|
46
44
|
>
|
|
47
45
|
<!-- The picture: the flow this template builds, drawn from its own
|
|
@@ -87,7 +85,6 @@ const chipSize = computed(() => full.value ? "px-2 py-0.5 text-xs" : "px-1.5 py-
|
|
|
87
85
|
<span v-if="packageLabel" :class="[CHIP, chipSize, 'bg-violet-100 text-violet-700']">{{ packageLabel }}</span>
|
|
88
86
|
</div>
|
|
89
87
|
|
|
90
|
-
<Loader2 v-if="busy" :size="13" class="absolute right-1.5 top-1.5 animate-spin text-muted-foreground" />
|
|
91
88
|
</div>
|
|
92
89
|
|
|
93
90
|
<div class="flex min-h-0 flex-1 flex-col" :class="full ? 'p-4' : 'p-3'">
|
|
@@ -5,10 +5,6 @@ type __VLS_Props = {
|
|
|
5
5
|
density: 'full' | 'compact';
|
|
6
6
|
/** Given → the card is a link there. Omitted → a button that emits `select`. */
|
|
7
7
|
to?: string;
|
|
8
|
-
/** This card is the one being acted on: it says so and cannot be pressed again. */
|
|
9
|
-
busy?: boolean;
|
|
10
|
-
/** Another card on the surface is being acted on, so this one is out of reach. */
|
|
11
|
-
disabled?: boolean;
|
|
12
8
|
};
|
|
13
9
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
14
10
|
select: () => any;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
slug: string;
|
|
3
|
+
};
|
|
4
|
+
declare var __VLS_69: {
|
|
5
|
+
installing: any;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_Slots = {} & {
|
|
8
|
+
secondary?: (props: typeof __VLS_69) => any;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
installed: (id: number) => any;
|
|
12
|
+
"update:loading": (loading: boolean) => any;
|
|
13
|
+
"update:installing": (installing: boolean) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
15
|
+
onInstalled?: ((id: number) => any) | undefined;
|
|
16
|
+
"onUpdate:loading"?: ((loading: boolean) => any) | undefined;
|
|
17
|
+
"onUpdate:installing"?: ((installing: boolean) => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
20
|
+
export default _default;
|
|
21
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
22
|
+
new (): {
|
|
23
|
+
$slots: S;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onBeforeUnmount, reactive, ref, watch } from "vue";
|
|
3
|
+
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
4
|
+
import {
|
|
5
|
+
Badge,
|
|
6
|
+
Button,
|
|
7
|
+
DialogFooter,
|
|
8
|
+
Input,
|
|
9
|
+
Label,
|
|
10
|
+
Select,
|
|
11
|
+
SelectContent,
|
|
12
|
+
SelectItem,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
SelectValue
|
|
15
|
+
} from "@revenexx/studio";
|
|
16
|
+
import { errorReason } from "../../utils/apiError";
|
|
17
|
+
const SELECT_LATER = "__later__";
|
|
18
|
+
const props = defineProps({
|
|
19
|
+
slug: { type: String, required: true }
|
|
20
|
+
});
|
|
21
|
+
const emit = defineEmits(["installed", "update:loading", "update:installing"]);
|
|
22
|
+
const t = useIntegrationsT();
|
|
23
|
+
const { get, load, fetchDetail, requirements, instantiate } = useIntegrationsTemplates();
|
|
24
|
+
const { keys: secretKeys, load: loadSecrets } = useIntegrationsSecrets();
|
|
25
|
+
const { ofType, load: loadCredentials } = useIntegrationsCredentials();
|
|
26
|
+
const reqs = ref(null);
|
|
27
|
+
const loading = ref(true);
|
|
28
|
+
const installing = ref(false);
|
|
29
|
+
const error = ref(null);
|
|
30
|
+
const name = ref("");
|
|
31
|
+
const bindings = reactive({});
|
|
32
|
+
let run = 0;
|
|
33
|
+
watch(() => props.slug, async (slug) => {
|
|
34
|
+
const mine = ++run;
|
|
35
|
+
loading.value = true;
|
|
36
|
+
reqs.value = null;
|
|
37
|
+
error.value = null;
|
|
38
|
+
name.value = "";
|
|
39
|
+
for (const k of Object.keys(bindings)) delete bindings[k];
|
|
40
|
+
await load();
|
|
41
|
+
if (mine !== run) return;
|
|
42
|
+
if (name.value === "") name.value = get(slug)?.name ?? "";
|
|
43
|
+
await Promise.all([fetchDetail(slug), loadCredentials(), loadSecrets()]);
|
|
44
|
+
if (mine !== run) return;
|
|
45
|
+
const answer = await requirements(slug);
|
|
46
|
+
if (mine !== run) return;
|
|
47
|
+
reqs.value = answer;
|
|
48
|
+
loading.value = false;
|
|
49
|
+
}, { immediate: true });
|
|
50
|
+
watch(loading, (v) => emit("update:loading", v), { immediate: true });
|
|
51
|
+
watch(installing, (v) => emit("update:installing", v));
|
|
52
|
+
onBeforeUnmount(() => emit("update:loading", false));
|
|
53
|
+
const tpl = computed(() => get(props.slug));
|
|
54
|
+
const credentialReqs = computed(() => reqs.value?.credentialTypes ?? []);
|
|
55
|
+
const secretReqs = computed(() => reqs.value?.secrets ?? []);
|
|
56
|
+
const secretChecks = computed(() => {
|
|
57
|
+
const fromApi = secretReqs.value;
|
|
58
|
+
if (fromApi.length) {
|
|
59
|
+
return fromApi.map((s) => ({
|
|
60
|
+
key: s.key,
|
|
61
|
+
label: s.key ?? t("integration.templates.detail.secrets.chosenAtInstall"),
|
|
62
|
+
present: s.exists || (s.key ? secretKeys.value.includes(s.key) : false)
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
return (tpl.value?.requiredSecrets ?? []).map((key) => ({
|
|
66
|
+
key,
|
|
67
|
+
label: key,
|
|
68
|
+
present: secretKeys.value.includes(key)
|
|
69
|
+
}));
|
|
70
|
+
});
|
|
71
|
+
function nodesFor(usedBy) {
|
|
72
|
+
return [...new Set(usedBy.map((u) => u.nodeId))];
|
|
73
|
+
}
|
|
74
|
+
async function confirmInstall() {
|
|
75
|
+
installing.value = true;
|
|
76
|
+
error.value = null;
|
|
77
|
+
try {
|
|
78
|
+
const credentialBindings = {};
|
|
79
|
+
for (const [nodeId, credId] of Object.entries(bindings)) {
|
|
80
|
+
if (credId && credId !== SELECT_LATER) credentialBindings[nodeId] = credId;
|
|
81
|
+
}
|
|
82
|
+
const id = await instantiate(props.slug, {
|
|
83
|
+
name: name.value.trim(),
|
|
84
|
+
credentialBindings
|
|
85
|
+
});
|
|
86
|
+
if (!Number.isFinite(id)) throw new Error(t("integration.templates.detail.install.error.fallback"));
|
|
87
|
+
emit("installed", id);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
error.value = errorReason(e, t("integration.templates.detail.install.error.fallback"));
|
|
90
|
+
} finally {
|
|
91
|
+
installing.value = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<template>
|
|
97
|
+
<div>
|
|
98
|
+
<div class="space-y-4 py-1 max-h-[28rem] overflow-y-auto pr-1">
|
|
99
|
+
<div class="space-y-1.5">
|
|
100
|
+
<Label for="tpl-install-name">{{ t("integration.templates.detail.install.nameLabel") }}</Label>
|
|
101
|
+
<Input id="tpl-install-name" v-model="name" class="bg-white" :placeholder="t('integration.templates.detail.install.namePlaceholder')" />
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div v-if="loading" class="flex items-center gap-2 py-4 text-sm text-muted-foreground">
|
|
105
|
+
<Loader2 :size="14" class="animate-spin" />
|
|
106
|
+
{{ t("integration.templates.detail.install.state.loading") }}
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<template v-else>
|
|
110
|
+
<!-- Credentials -->
|
|
111
|
+
<div class="space-y-2">
|
|
112
|
+
<div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
|
|
113
|
+
{{ t("integration.templates.detail.credentials.title") }}
|
|
114
|
+
</div>
|
|
115
|
+
<p v-if="!credentialReqs.length" class="text-sm text-muted-foreground italic">
|
|
116
|
+
{{ t("integration.templates.detail.install.noCredentials") }}
|
|
117
|
+
</p>
|
|
118
|
+
<div v-for="req in credentialReqs" :key="req.slug" class="space-y-2">
|
|
119
|
+
<div class="flex items-center gap-2">
|
|
120
|
+
<span class="text-sm font-medium">{{ req.name ?? req.slug }}</span>
|
|
121
|
+
<Badge v-if="!req.registered" variant="outline" class="bg-rose-500/10 border-rose-500/20 text-rose-700 text-sm">
|
|
122
|
+
{{ t("integration.templates.detail.install.notRegistered") }}
|
|
123
|
+
</Badge>
|
|
124
|
+
<Badge v-else-if="!ofType(req.slug).length" variant="outline" class="bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm">
|
|
125
|
+
{{ t("integration.templates.detail.install.noneYet") }}
|
|
126
|
+
</Badge>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<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">
|
|
130
|
+
{{ t("integration.templates.detail.install.noCredentialsOfType") }}
|
|
131
|
+
<NuxtLink to="/integrations/credentials" class="text-primary hover:underline">{{ t("integration.templates.detail.install.createOneLink") }}</NuxtLink>
|
|
132
|
+
{{ t("integration.templates.detail.install.orInstallLater") }}
|
|
133
|
+
</p>
|
|
134
|
+
<div v-for="nodeId in nodesFor(req.usedBy)" v-else :key="nodeId" class="space-y-1">
|
|
135
|
+
<Label :for="`tpl-cred-${nodeId}`" class="text-xs text-muted-foreground font-mono">{{ t("integration.templates.detail.install.nodeLabel", { id: nodeId }) }}</Label>
|
|
136
|
+
<Select
|
|
137
|
+
:model-value="bindings[nodeId] ?? SELECT_LATER"
|
|
138
|
+
@update:model-value="(v) => {
|
|
139
|
+
bindings[nodeId] = v ?? SELECT_LATER;
|
|
140
|
+
}"
|
|
141
|
+
>
|
|
142
|
+
<SelectTrigger :id="`tpl-cred-${nodeId}`" class="bg-white">
|
|
143
|
+
<SelectValue :placeholder="t('integration.templates.detail.install.selectLater')" />
|
|
144
|
+
</SelectTrigger>
|
|
145
|
+
<SelectContent>
|
|
146
|
+
<SelectItem :value="SELECT_LATER">{{ t("integration.templates.detail.install.selectLater") }}</SelectItem>
|
|
147
|
+
<SelectItem v-for="c in ofType(req.slug)" :key="c.id" :value="c.id">
|
|
148
|
+
{{ c.name }}
|
|
149
|
+
</SelectItem>
|
|
150
|
+
</SelectContent>
|
|
151
|
+
</Select>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<!-- Secrets -->
|
|
157
|
+
<div v-if="secretChecks.length" class="space-y-2">
|
|
158
|
+
<div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
|
|
159
|
+
{{ t("integration.templates.detail.install.secretsHeader") }}
|
|
160
|
+
</div>
|
|
161
|
+
<p class="text-sm text-muted-foreground">
|
|
162
|
+
{{ t("integration.templates.detail.install.secretsDescription") }}
|
|
163
|
+
<NuxtLink to="/integrations/secrets" class="text-primary hover:underline">{{ t("integration.nav.item.secrets") }}</NuxtLink> {{ t("integration.templates.detail.install.secretsDescriptionSuffix") }}
|
|
164
|
+
</p>
|
|
165
|
+
<ul class="space-y-1">
|
|
166
|
+
<li
|
|
167
|
+
v-for="(s, i) in secretChecks"
|
|
168
|
+
:key="s.key ?? `dlg-secret-${i}`"
|
|
169
|
+
class="flex items-center justify-between rounded-md border border-black/[0.06] bg-muted/20 px-2 py-1 text-sm"
|
|
170
|
+
>
|
|
171
|
+
<code class="font-mono">{{ s.label }}</code>
|
|
172
|
+
<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'">
|
|
173
|
+
{{ s.present ? t("integration.templates.detail.install.secretStored") : t("integration.templates.detail.secrets.missing") }}
|
|
174
|
+
</Badge>
|
|
175
|
+
</li>
|
|
176
|
+
</ul>
|
|
177
|
+
</div>
|
|
178
|
+
</template>
|
|
179
|
+
|
|
180
|
+
<div v-if="error" class="rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
|
|
181
|
+
{{ error }}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<DialogFooter>
|
|
186
|
+
<!-- Cancel here, back to the picker there: what the secondary control does
|
|
187
|
+
is the caller's, and only the caller knows what it goes back to. -->
|
|
188
|
+
<slot name="secondary" :installing="installing" />
|
|
189
|
+
<!-- Also closed while the step is still checking: a reader who confirmed
|
|
190
|
+
in that window would get the unwired copy the short route used to
|
|
191
|
+
produce, which is the outcome this step exists to replace. -->
|
|
192
|
+
<Button :disabled="installing || loading || !name.trim()" @click="confirmInstall">
|
|
193
|
+
<Loader2 v-if="installing" :size="14" class="mr-1 animate-spin" />
|
|
194
|
+
{{ installing ? t("integration.templates.detail.install.submitting") : t("integration.templates.detail.install.submit") }}
|
|
195
|
+
</Button>
|
|
196
|
+
</DialogFooter>
|
|
197
|
+
</div>
|
|
198
|
+
</template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
slug: string;
|
|
3
|
+
};
|
|
4
|
+
declare var __VLS_69: {
|
|
5
|
+
installing: any;
|
|
6
|
+
};
|
|
7
|
+
type __VLS_Slots = {} & {
|
|
8
|
+
secondary?: (props: typeof __VLS_69) => any;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
installed: (id: number) => any;
|
|
12
|
+
"update:loading": (loading: boolean) => any;
|
|
13
|
+
"update:installing": (installing: boolean) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
15
|
+
onInstalled?: ((id: number) => any) | undefined;
|
|
16
|
+
"onUpdate:loading"?: ((loading: boolean) => any) | undefined;
|
|
17
|
+
"onUpdate:installing"?: ((installing: boolean) => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
20
|
+
export default _default;
|
|
21
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
22
|
+
new (): {
|
|
23
|
+
$slots: S;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -17,11 +17,11 @@ type __VLS_Slots = {} & {
|
|
|
17
17
|
default?: (props: typeof __VLS_14) => any;
|
|
18
18
|
};
|
|
19
19
|
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
20
|
-
action: (id: MenuActionId) => any;
|
|
21
20
|
"update:open": (open: boolean) => any;
|
|
21
|
+
action: (id: MenuActionId) => any;
|
|
22
22
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
23
|
-
onAction?: ((id: MenuActionId) => any) | undefined;
|
|
24
23
|
"onUpdate:open"?: ((open: boolean) => any) | undefined;
|
|
24
|
+
onAction?: ((id: MenuActionId) => any) | undefined;
|
|
25
25
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
26
26
|
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
27
27
|
export default _default;
|