@revenexx/studio-integrations 2.0.0 → 2.2.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 +141 -0
- package/README.md +1 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -6
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +134 -131
- package/dist/runtime/components/Integrations/CredentialLogo.vue +1 -1
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +1 -1
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +6 -6
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +90 -11
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +121 -121
- package/dist/runtime/components/Integrations/NodeConfigField.vue +28 -9
- package/dist/runtime/components/Integrations/NodeInspector.vue +44 -41
- package/dist/runtime/components/Integrations/NodePalette.vue +52 -37
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +6 -5
- package/dist/runtime/components/Integrations/RunDetailPanel.d.vue.ts +7 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +14 -2
- package/dist/runtime/components/Integrations/RunDetailPanel.vue.d.ts +7 -0
- package/dist/runtime/components/Integrations/RunsTable.vue +7 -26
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +10 -6
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +224 -225
- package/dist/runtime/components/Integrations/WorkflowExportDialog.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowExportDialog.vue +71 -0
- package/dist/runtime/components/Integrations/WorkflowExportDialog.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.d.vue.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.vue +95 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.vue.d.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +5 -4
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +0 -7
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +2 -2
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +2 -3
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +14 -8
- package/dist/runtime/composables/useIntegrationsNodes.js +4 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +3 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -22
- package/dist/runtime/composables/useNow.d.ts +17 -0
- package/dist/runtime/composables/useNow.js +16 -0
- package/dist/runtime/composables/useWorkflowFile.d.ts +21 -0
- package/dist/runtime/composables/useWorkflowFile.js +63 -0
- package/dist/runtime/data/expressionEditor.d.ts +11 -1
- package/dist/runtime/data/expressionEditor.js +2 -1
- package/dist/runtime/data/nodeIcons.d.ts +14 -0
- package/dist/runtime/data/nodeIcons.js +57 -14
- package/dist/runtime/data/nodeRunData.js +1 -1
- package/dist/runtime/data/payloadForm.js +1 -1
- package/dist/runtime/i18n/messages/de.json +42 -0
- package/dist/runtime/i18n/messages/en.json +42 -0
- package/dist/runtime/pages/integrations/audit.vue +3 -3
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +2 -2
- package/dist/runtime/pages/integrations/index.vue +5 -10
- package/dist/runtime/pages/integrations/schedules.vue +13 -1
- package/dist/runtime/pages/integrations/templates/index.vue +40 -40
- package/dist/runtime/pages/integrations/workflows/[id].vue +324 -244
- package/dist/runtime/types/integrations.d.ts +13 -0
- package/dist/runtime/utils/credentialState.d.ts +0 -7
- package/dist/runtime/utils/credentialState.js +0 -12
- package/dist/runtime/utils/localize.d.ts +12 -0
- package/dist/runtime/utils/localize.js +1 -1
- package/dist/runtime/utils/paletteOrder.d.ts +29 -0
- package/dist/runtime/utils/paletteOrder.js +46 -0
- package/dist/runtime/utils/relativeTime.d.ts +29 -0
- package/dist/runtime/utils/relativeTime.js +23 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +12 -6
- package/dist/runtime/utils/runFeedQuery.js +3 -18
- package/dist/runtime/utils/settingValue.d.ts +33 -0
- package/dist/runtime/utils/settingValue.js +8 -0
- package/dist/runtime/utils/statusStyle.d.ts +8 -8
- package/dist/runtime/utils/statusStyle.js +4 -1
- package/dist/runtime/utils/workflowFile.d.ts +119 -0
- package/dist/runtime/utils/workflowFile.js +192 -0
- package/package.json +8 -3
|
@@ -22,13 +22,7 @@ import {
|
|
|
22
22
|
DropdownMenuLabel,
|
|
23
23
|
DropdownMenuSeparator,
|
|
24
24
|
DropdownMenuTrigger,
|
|
25
|
-
|
|
26
|
-
SelectContent,
|
|
27
|
-
SelectItem,
|
|
28
|
-
SelectTrigger,
|
|
29
|
-
SelectValue,
|
|
30
|
-
Switch,
|
|
31
|
-
Textarea
|
|
25
|
+
Switch
|
|
32
26
|
} from "@revenexx/studio";
|
|
33
27
|
import { deepClone } from "../../../utils/deepClone";
|
|
34
28
|
import WorkflowEditor from "../../../components/Integrations/WorkflowEditor.vue";
|
|
@@ -37,6 +31,9 @@ import WorkflowRunReplayPanel from "../../../components/Integrations/WorkflowRun
|
|
|
37
31
|
import WorkflowSettingsDialog from "../../../components/Integrations/WorkflowSettingsDialog.vue";
|
|
38
32
|
import WorkflowDeleteDialog from "../../../components/Integrations/WorkflowDeleteDialog.vue";
|
|
39
33
|
import WorkflowDuplicateDialog from "../../../components/Integrations/WorkflowDuplicateDialog.vue";
|
|
34
|
+
import WorkflowExportDialog from "../../../components/Integrations/WorkflowExportDialog.vue";
|
|
35
|
+
import { useWorkflowFile } from "../../../composables/useWorkflowFile";
|
|
36
|
+
import { parseWorkflowFile } from "../../../utils/workflowFile";
|
|
40
37
|
import RunsTable from "../../../components/Integrations/RunsTable.vue";
|
|
41
38
|
import RunDetailPanel from "../../../components/Integrations/RunDetailPanel.vue";
|
|
42
39
|
import { toast } from "vue-sonner";
|
|
@@ -232,6 +229,41 @@ async function onDuplicated(copy) {
|
|
|
232
229
|
toast.success(t("integration.editor.toast.duplicated", { name: copy.name }));
|
|
233
230
|
await leaveTo(`/integrations/workflows/${copy.id}`);
|
|
234
231
|
}
|
|
232
|
+
const exporter = ref(null);
|
|
233
|
+
const { planOverwrite } = useWorkflowFile();
|
|
234
|
+
const importInput = ref(null);
|
|
235
|
+
const importPlan = ref(null);
|
|
236
|
+
const importOpen = ref(false);
|
|
237
|
+
async function onImportPicked(event) {
|
|
238
|
+
const input = event.target;
|
|
239
|
+
const picked = input.files?.[0];
|
|
240
|
+
input.value = "";
|
|
241
|
+
if (!picked || !workflow.value) return;
|
|
242
|
+
const parsed = parseWorkflowFile(await picked.text());
|
|
243
|
+
if (!parsed.ok) {
|
|
244
|
+
toast.error(t(`integration.workflowFile.refused.${parsed.reason}`));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
let plan;
|
|
248
|
+
try {
|
|
249
|
+
plan = await planOverwrite(workflow.value, parsed.file);
|
|
250
|
+
} catch {
|
|
251
|
+
toast.error(t("integration.workflowFile.import.planError"));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
importPlan.value = { ...plan, executionMode: parsed.file.workflow.executionMode, active: workflow.value.active, dirty: isDirty.value };
|
|
255
|
+
importOpen.value = true;
|
|
256
|
+
}
|
|
257
|
+
function confirmImport() {
|
|
258
|
+
const plan = importPlan.value;
|
|
259
|
+
importOpen.value = false;
|
|
260
|
+
draft.value = null;
|
|
261
|
+
const base = draftFrom();
|
|
262
|
+
if (!plan || !base) return;
|
|
263
|
+
draft.value = { ...base, executionMode: plan.executionMode, blob: plan.blob };
|
|
264
|
+
editorResetKey.value++;
|
|
265
|
+
toast.success(t("integration.workflowFile.overwrite.toast"));
|
|
266
|
+
}
|
|
235
267
|
const deleteOpen = ref(false);
|
|
236
268
|
const deleteTarget = ref(null);
|
|
237
269
|
function askDelete() {
|
|
@@ -756,14 +788,6 @@ async function confirmDeactivate() {
|
|
|
756
788
|
saving.value = false;
|
|
757
789
|
}
|
|
758
790
|
}
|
|
759
|
-
function formatDateTime(iso) {
|
|
760
|
-
if (!iso) return "\u2014";
|
|
761
|
-
try {
|
|
762
|
-
return new Date(iso).toLocaleString();
|
|
763
|
-
} catch {
|
|
764
|
-
return iso;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
791
|
</script>
|
|
768
792
|
|
|
769
793
|
<template>
|
|
@@ -773,40 +797,40 @@ function formatDateTime(iso) {
|
|
|
773
797
|
<div class="flex h-full flex-col">
|
|
774
798
|
<template v-if="workflow">
|
|
775
799
|
<PageHeader :title="shownName">
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
<Chevron :size="15" weight="Outline" class="shrink-0 text-muted-foreground/50" />
|
|
780
|
-
<!-- Run open: the breadcrumb extends — name and Runs become crumbs
|
|
781
|
-
back into the workflow surface. -->
|
|
782
|
-
<template v-if="openRunId">
|
|
783
|
-
<button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" :title="shownName" @click="closeRun('editor')">{{ shownName }}</button>
|
|
800
|
+
<template #title>
|
|
801
|
+
<span class="flex min-w-0 items-center gap-1.5">
|
|
802
|
+
<NuxtLink to="/integrations/workflows" class="shrink-0 font-normal text-muted-foreground/80 transition-colors hover:text-primary">{{ t("integration.editor.breadcrumb.workflows") }}</NuxtLink>
|
|
784
803
|
<Chevron :size="15" weight="Outline" class="shrink-0 text-muted-foreground/50" />
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
804
|
+
<!-- Run open: the breadcrumb extends — name and Runs become crumbs
|
|
805
|
+
back into the workflow surface. -->
|
|
806
|
+
<template v-if="openRunId">
|
|
807
|
+
<button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" :title="shownName" @click="closeRun('editor')">{{ shownName }}</button>
|
|
808
|
+
<Chevron :size="15" weight="Outline" class="shrink-0 text-muted-foreground/50" />
|
|
809
|
+
<button type="button" class="shrink-0 font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('runs')">{{ t("integration.editor.breadcrumb.runs") }}</button>
|
|
810
|
+
<Chevron :size="15" weight="Outline" class="shrink-0 text-muted-foreground/50" />
|
|
811
|
+
<!-- The Temporal run id IS the run's identity; the internal
|
|
788
812
|
number only fills in until the run is handed off. -->
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
813
|
+
<span class="shrink-0 font-mono">{{ openRun?.temporalRunId ?? t("integration.editor.breadcrumb.runNumber", { id: openRunId }) }}</span>
|
|
814
|
+
</template>
|
|
815
|
+
<template v-else>
|
|
816
|
+
<span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
817
|
+
<WorkflowIconView :icon="shownIcon" :size="16" />
|
|
818
|
+
</span>
|
|
819
|
+
<span class="truncate" :title="shownName">{{ shownName }}</span>
|
|
820
|
+
<!-- The SAVED mode, beside a drafted name, and the difference is not an
|
|
797
821
|
oversight. A name changes nothing about what happens when Run is
|
|
798
822
|
pressed, so the header may show what the operator just entered. The
|
|
799
823
|
dispatch mode decides what a run does — and a run goes to the
|
|
800
824
|
workflow the platform holds, not to the draft. A badge following the
|
|
801
825
|
draft would read SYNC over a button that fires an async run. -->
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
826
|
+
<Badge variant="outline" class="ml-1.5 shrink-0 border text-xs font-medium uppercase text-muted-foreground" :title="t('integration.editor.header.executionMode.title', { mode: modeLabel(workflow.execution_mode) })">
|
|
827
|
+
{{ modeLabel(workflow.execution_mode) }}
|
|
828
|
+
</Badge>
|
|
829
|
+
</template>
|
|
830
|
+
</span>
|
|
831
|
+
</template>
|
|
832
|
+
<template #actions>
|
|
833
|
+
<!-- Folding the rail away, and getting it back (PO-273). It reads as
|
|
810
834
|
chrome rather than as an action on this workflow, so the natural
|
|
811
835
|
place was ahead of the breadcrumb — but the shell's `#title` slot
|
|
812
836
|
*is* the `<h1>`'s content, and a control inside the heading makes
|
|
@@ -826,43 +850,43 @@ function formatDateTime(iso) {
|
|
|
826
850
|
Icon-only, so it carries the same name as before to a screen reader
|
|
827
851
|
and to a test. Rendered only where the canvas fills the page, the
|
|
828
852
|
one surface it acts on. -->
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
>
|
|
839
|
-
<RailIcon :size="15" />
|
|
840
|
-
<template v-if="railFolded">{{ t("integration.nav.action.show") }}</template>
|
|
841
|
-
</Button>
|
|
842
|
-
|
|
843
|
-
<!-- Run open: run-scoped actions replace the workflow Save. -->
|
|
844
|
-
<template v-if="openRunId">
|
|
845
|
-
<Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" :title="t('integration.editor.action.retry.title')" @click="retryOpenRun">
|
|
846
|
-
<Loader2 v-if="retryBusy" :size="12" class="animate-spin" />
|
|
847
|
-
<RetryIcon v-else :size="12" />
|
|
848
|
-
{{ t("integration.editor.action.retry") }}
|
|
849
|
-
</Button>
|
|
850
|
-
<Button class="h-7 text-sm" @click="closeRun('editor')">
|
|
851
|
-
<ArrowLeft :size="12" />
|
|
852
|
-
{{ t("integration.editor.action.backToWorkflow") }}
|
|
853
|
-
</Button>
|
|
854
|
-
</template>
|
|
855
|
-
<template v-else>
|
|
856
|
-
<span
|
|
857
|
-
v-if="buildWorking"
|
|
858
|
-
class="inline-flex h-7 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border border-sky-500/25 bg-sky-500/10 px-2.5 text-sm text-sky-700"
|
|
859
|
-
:title="buildNotice?.text"
|
|
853
|
+
<Button
|
|
854
|
+
v-if="canvasFillsPage"
|
|
855
|
+
variant="ghost"
|
|
856
|
+
size="sm"
|
|
857
|
+
class="h-7 shrink-0 gap-1.5 text-sm font-normal text-muted-foreground hover:text-primary"
|
|
858
|
+
:class="railFolded ? 'px-2' : 'w-7 px-0'"
|
|
859
|
+
:aria-label="railFolded ? void 0 : t('integration.nav.action.hide')"
|
|
860
|
+
:title="railFolded ? t('integration.nav.action.show.title') : t('integration.nav.action.hide.title')"
|
|
861
|
+
@click="toggleRail"
|
|
860
862
|
>
|
|
861
|
-
<
|
|
862
|
-
|
|
863
|
-
</
|
|
864
|
-
|
|
865
|
-
<!--
|
|
863
|
+
<RailIcon :size="15" />
|
|
864
|
+
<template v-if="railFolded">{{ t("integration.nav.action.show") }}</template>
|
|
865
|
+
</Button>
|
|
866
|
+
|
|
867
|
+
<!-- Run open: run-scoped actions replace the workflow Save. -->
|
|
868
|
+
<template v-if="openRunId">
|
|
869
|
+
<Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" :title="t('integration.editor.action.retry.title')" @click="retryOpenRun">
|
|
870
|
+
<Loader2 v-if="retryBusy" :size="12" class="animate-spin" />
|
|
871
|
+
<RetryIcon v-else :size="12" />
|
|
872
|
+
{{ t("integration.editor.action.retry") }}
|
|
873
|
+
</Button>
|
|
874
|
+
<Button class="h-7 text-sm" @click="closeRun('editor')">
|
|
875
|
+
<ArrowLeft :size="12" />
|
|
876
|
+
{{ t("integration.editor.action.backToWorkflow") }}
|
|
877
|
+
</Button>
|
|
878
|
+
</template>
|
|
879
|
+
<template v-else>
|
|
880
|
+
<span
|
|
881
|
+
v-if="buildWorking"
|
|
882
|
+
class="inline-flex h-7 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border border-sky-500/25 bg-sky-500/10 px-2.5 text-sm text-sky-700"
|
|
883
|
+
:title="buildNotice?.text"
|
|
884
|
+
>
|
|
885
|
+
<Loader2 :size="12" class="animate-spin" />
|
|
886
|
+
{{ workflow.buildStatus === "building" ? t("integration.editor.build.chip.building") : t("integration.editor.build.chip.queued") }}
|
|
887
|
+
</span>
|
|
888
|
+
<span v-if="isDirty" class="shrink-0 whitespace-nowrap text-sm text-amber-600">{{ t("integration.editor.status.unsavedChanges") }}</span>
|
|
889
|
+
<!-- What is done to the WORKFLOW, as opposed to the draft in front
|
|
866
890
|
of it: the buttons beside this menu discard and save an
|
|
867
891
|
afternoon's work, and these two make and unmake the thing that
|
|
868
892
|
work belongs to. Two reasons they are a menu rather than two more
|
|
@@ -871,33 +895,39 @@ function formatDateTime(iso) {
|
|
|
871
895
|
Duplicate that stayed a button reads as an oversight rather than
|
|
872
896
|
as a grouping — the menu only holds together while it holds both.
|
|
873
897
|
Icon-only, so it carries a name of its own. -->
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
898
|
+
<DropdownMenu>
|
|
899
|
+
<DropdownMenuTrigger as-child>
|
|
900
|
+
<Button variant="outline" size="icon" class="size-7" :disabled="saving" :aria-label="t('integration.editor.action.more.aria')">
|
|
901
|
+
<MoreHorizontal :size="12" />
|
|
902
|
+
</Button>
|
|
903
|
+
</DropdownMenuTrigger>
|
|
904
|
+
<DropdownMenuContent align="end">
|
|
905
|
+
<DropdownMenuLabel>{{ t("integration.editor.action.more.label") }}</DropdownMenuLabel>
|
|
906
|
+
<DropdownMenuSeparator />
|
|
907
|
+
<DropdownMenuItem @select="duplicateOpen = true">
|
|
908
|
+
{{ t("integration.editor.action.duplicate.label") }}
|
|
909
|
+
</DropdownMenuItem>
|
|
910
|
+
<DropdownMenuItem @select="exporter?.start(workflow.id)">
|
|
911
|
+
{{ t("integration.workflowFile.action.export") }}
|
|
912
|
+
</DropdownMenuItem>
|
|
913
|
+
<DropdownMenuItem @select="importInput?.click()">
|
|
914
|
+
{{ t("integration.workflowFile.action.import") }}
|
|
915
|
+
</DropdownMenuItem>
|
|
916
|
+
<DropdownMenuSeparator />
|
|
917
|
+
<DropdownMenuItem class="text-destructive focus:text-destructive" @select="askDelete">
|
|
918
|
+
{{ t("integration.common.action.delete") }}
|
|
919
|
+
</DropdownMenuItem>
|
|
920
|
+
</DropdownMenuContent>
|
|
921
|
+
</DropdownMenu>
|
|
922
|
+
<Button variant="outline" class="h-7 text-sm" :disabled="!isDirty || saving" :title="t('integration.editor.action.discard.title')" @click="discardChanges">
|
|
923
|
+
{{ t("integration.editor.action.discard.label") }}
|
|
924
|
+
</Button>
|
|
925
|
+
<Button class="h-7 text-sm" :disabled="!isDirty || saving" @click="save">
|
|
926
|
+
<Loader2 v-if="saving" :size="12" class="animate-spin" />
|
|
927
|
+
<Save v-else :size="12" />
|
|
928
|
+
{{ saving ? t("integration.editor.action.save.saving") : t("integration.editor.action.save.label") }}
|
|
929
|
+
</Button>
|
|
930
|
+
<!-- Activation is a standing state, not a transport control. The play
|
|
901
931
|
symbol belongs to Run, 600 px away in the canvas's control pane,
|
|
902
932
|
where pressing it makes one thing happen now; arming a workflow's
|
|
903
933
|
triggers looks like nothing happening at all. So this states the
|
|
@@ -909,180 +939,180 @@ function formatDateTime(iso) {
|
|
|
909
939
|
is not disabled, and `aria-describedby` puts the same sentence on
|
|
910
940
|
the switch for anyone who never hovers. Sighted users also have
|
|
911
941
|
the amber banner directly below, which says it at length. -->
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
942
|
+
<div
|
|
943
|
+
class="flex h-7 items-center gap-2 rounded-md border border-black/[0.08] px-2"
|
|
944
|
+
:title="workflow.active ? t('integration.editor.action.pause.title') : isInvalid ? t('integration.editor.action.activate.title.invalid', validationWarnings?.length ?? 0) : t('integration.editor.action.activate.title')"
|
|
945
|
+
>
|
|
946
|
+
<Switch
|
|
947
|
+
:aria-label="t('integration.editor.action.state.label')"
|
|
948
|
+
:aria-describedby="!workflow.active && isInvalid ? 'wf-activation-blocked' : void 0"
|
|
949
|
+
class="data-[state=checked]:bg-emerald-600 data-[state=unchecked]:bg-slate-300"
|
|
950
|
+
:model-value="workflow.active"
|
|
951
|
+
:disabled="!workflow.active && isInvalid"
|
|
952
|
+
@update:model-value="onToggleActive"
|
|
953
|
+
/>
|
|
954
|
+
<span class="text-sm" :class="workflow.active ? 'font-medium text-emerald-700' : 'text-muted-foreground'">
|
|
955
|
+
{{ workflow.active ? t("integration.workflows.table.status.lifecycle.active") : t("integration.workflows.table.status.lifecycle.paused") }}
|
|
956
|
+
</span>
|
|
957
|
+
<span v-if="!workflow.active && isInvalid" id="wf-activation-blocked" class="sr-only">
|
|
958
|
+
{{ t("integration.editor.action.activate.title.invalid", validationWarnings?.length ?? 0) }}
|
|
959
|
+
</span>
|
|
960
|
+
</div>
|
|
961
|
+
</template>
|
|
931
962
|
</template>
|
|
932
|
-
</template>
|
|
933
963
|
|
|
934
|
-
|
|
964
|
+
</PageHeader>
|
|
935
965
|
|
|
936
|
-
|
|
937
|
-
|
|
966
|
+
<TabBar v-if="!openRunId" v-model="tab" :tabs="tabs" class="shrink-0 px-4" />
|
|
967
|
+
<TabBar v-else v-model="runView" :tabs="runViews" class="shrink-0 px-4" />
|
|
938
968
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
969
|
+
<div v-if="saveError" class="border-b border-rose-500/20 bg-rose-500/5 px-4 py-2 text-sm text-rose-700">
|
|
970
|
+
{{ saveError }}
|
|
971
|
+
</div>
|
|
942
972
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
973
|
+
<div v-if="reauthCredentials.length" class="border-b border-amber-500/20 bg-amber-500/5 px-4 py-2 text-sm text-amber-800">
|
|
974
|
+
{{ t("integration.editor.reauth.banner", reauthCredentials.length) }}
|
|
975
|
+
({{ reauthCredentials.map((c) => c.name).join(", ") }}) —
|
|
976
|
+
<NuxtLink to="/integrations/credentials" class="font-medium underline">
|
|
977
|
+
{{ t("integration.editor.reauth.action.fix") }}
|
|
978
|
+
</NuxtLink>.
|
|
979
|
+
</div>
|
|
950
980
|
|
|
951
|
-
|
|
981
|
+
<!-- Manifest validation (PO-186): an inactive workflow with warnings can't
|
|
952
982
|
be activated. The banner names what's incomplete; the header's state
|
|
953
983
|
switch stays blocked until the next save recomputes an empty warnings set.
|
|
954
984
|
Amber (not red) — the work is saved fine, it just isn't runnable yet.
|
|
955
985
|
One line per NODE, listing its fields (PO-209): the messages behind
|
|
956
986
|
them ride along as the line's title, and the line leads to the node. -->
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
>
|
|
961
|
-
<p class="font-medium">
|
|
962
|
-
{{ t("integration.editor.validation.banner.title", { summary: warningSummary }) }}
|
|
963
|
-
</p>
|
|
964
|
-
<ul
|
|
965
|
-
class="mt-1 space-y-0.5"
|
|
966
|
-
:class="warningsExpanded ? 'max-h-36 overflow-y-auto pr-1' : ''"
|
|
987
|
+
<div
|
|
988
|
+
v-if="!openRunId && !workflow.active && isInvalid"
|
|
989
|
+
class="border-b border-amber-500/20 bg-amber-500/5 px-4 py-2 text-sm text-amber-800"
|
|
967
990
|
>
|
|
968
|
-
<
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
991
|
+
<p class="font-medium">
|
|
992
|
+
{{ t("integration.editor.validation.banner.title", { summary: warningSummary }) }}
|
|
993
|
+
</p>
|
|
994
|
+
<ul
|
|
995
|
+
class="mt-1 space-y-0.5"
|
|
996
|
+
:class="warningsExpanded ? 'max-h-36 overflow-y-auto pr-1' : ''"
|
|
972
997
|
>
|
|
973
|
-
<
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
v-if="g.nodeId"
|
|
978
|
-
type="button"
|
|
979
|
-
class="text-left underline decoration-amber-700/30 underline-offset-2 hover:decoration-amber-800"
|
|
980
|
-
:title="g.messages.join('\n')"
|
|
981
|
-
:aria-label="t('integration.editor.validation.node.ariaFix', g.fields.length, { label: g.label })"
|
|
982
|
-
@click="revealNode(g.nodeId)"
|
|
998
|
+
<li
|
|
999
|
+
v-for="(g, i) in warningsExpanded ? warningGroups : warningGroups.slice(0, WARN_COLLAPSED)"
|
|
1000
|
+
:key="`${g.nodeId ?? 'unattributed'}-${i}`"
|
|
1001
|
+
class="flex gap-1.5"
|
|
983
1002
|
>
|
|
984
|
-
<span class="
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1003
|
+
<span class="select-none text-amber-500">•</span>
|
|
1004
|
+
<!-- Attributed to a node: the whole line jumps there and opens its
|
|
1005
|
+
settings. Otherwise there is nowhere to go — state the finding. -->
|
|
1006
|
+
<button
|
|
1007
|
+
v-if="g.nodeId"
|
|
1008
|
+
type="button"
|
|
1009
|
+
class="text-left underline decoration-amber-700/30 underline-offset-2 hover:decoration-amber-800"
|
|
1010
|
+
:title="g.messages.join('\n')"
|
|
1011
|
+
:aria-label="t('integration.editor.validation.node.ariaFix', g.fields.length, { label: g.label })"
|
|
1012
|
+
@click="revealNode(g.nodeId)"
|
|
1013
|
+
>
|
|
1014
|
+
<span class="font-medium">{{ g.label }}</span> — {{ g.fields.join(", ") }}
|
|
1015
|
+
</button>
|
|
1016
|
+
<span v-else>{{ g.messages.join(" ") }}</span>
|
|
1017
|
+
</li>
|
|
1018
|
+
<li v-if="!warningsExpanded && warningGroups.length > WARN_COLLAPSED" class="pl-3">
|
|
1019
|
+
<button type="button" class="text-amber-700/90 underline underline-offset-2 hover:text-amber-900" @click="warningsExpanded = true">
|
|
1020
|
+
{{ t("integration.editor.validation.banner.more", { n: warningGroups.length - WARN_COLLAPSED }) }}
|
|
1021
|
+
</button>
|
|
1022
|
+
</li>
|
|
1023
|
+
</ul>
|
|
1024
|
+
</div>
|
|
995
1025
|
|
|
996
|
-
|
|
1026
|
+
<!-- Only the FAILED build state banners (its message matters); the
|
|
997
1027
|
working states live as the header chip — and inside a run the
|
|
998
1028
|
workflow's build state is noise, so nothing shows at all. -->
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1029
|
+
<div
|
|
1030
|
+
v-if="!openRunId && buildNotice && buildNotice.tone === 'error'"
|
|
1031
|
+
class="border-b border-rose-500/20 bg-rose-500/5 px-4 py-2 text-sm text-rose-700"
|
|
1032
|
+
>
|
|
1033
|
+
{{ buildNotice.text }}
|
|
1034
|
+
</div>
|
|
1005
1035
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1036
|
+
<!-- Nested run detail — overview/timeline panel or Replay canvas. -->
|
|
1037
|
+
<template v-if="openRunId">
|
|
1038
|
+
<div v-if="runView !== 'replay'" class="flex-1 overflow-y-auto p-4">
|
|
1039
|
+
<div class="w-full">
|
|
1040
|
+
<RunDetailPanel v-if="openRun" :run="openRun" :view="runView" :nodes="workflow?.blob?.nodes ?? []" @changed="loadOpenRun" />
|
|
1041
|
+
<div v-else-if="openRunError" class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700">
|
|
1042
|
+
{{ openRunError }}
|
|
1043
|
+
</div>
|
|
1044
|
+
<div v-else class="flex items-center gap-2 p-12 text-sm text-muted-foreground">
|
|
1045
|
+
<Loader2 :size="16" class="animate-spin" /> {{ t("integration.editor.run.loading") }}
|
|
1046
|
+
</div>
|
|
1016
1047
|
</div>
|
|
1017
1048
|
</div>
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
</
|
|
1022
|
-
</template>
|
|
1049
|
+
<div v-else class="min-h-0 flex-1">
|
|
1050
|
+
<WorkflowRunReplayPanel :workflow="workflow" :initial-run-id="openRunId" />
|
|
1051
|
+
</div>
|
|
1052
|
+
</template>
|
|
1023
1053
|
|
|
1024
|
-
|
|
1054
|
+
<!-- Editor tab — fills the remaining height inside the panel via
|
|
1025
1055
|
the flex wrapper in the studio layout.
|
|
1026
1056
|
|
|
1027
1057
|
overlay-open: every lightbox this page owns renders as a sibling of
|
|
1028
1058
|
the tabs, so the editor stays mounted underneath one and would keep
|
|
1029
1059
|
its keyboard shortcuts live (Delete deleting the selected node behind
|
|
1030
1060
|
the dialog — PO-208). Add new page-level dialogs to this list. -->
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1061
|
+
<div v-else-if="tab === 'editor'" class="flex-1 min-h-0">
|
|
1062
|
+
<WorkflowEditor
|
|
1063
|
+
:key="editorResetKey"
|
|
1064
|
+
ref="editorRef"
|
|
1065
|
+
:workflow="workflow"
|
|
1066
|
+
:draft="draft?.blob ?? null"
|
|
1067
|
+
:run-busy="running || syncLoading || waitingForBuild"
|
|
1068
|
+
:run-waiting="waitingForBuild"
|
|
1069
|
+
:overlay-open="settingsOpen || duplicateOpen || importOpen || !!exporter?.open || deleteOpen || syncOpen || runPayloadOpen || deactivationOpen || leaveOpen"
|
|
1070
|
+
@change="onChange"
|
|
1071
|
+
@run="runFromEditor"
|
|
1072
|
+
@toggle-active="onToggleActive"
|
|
1073
|
+
@open-settings="settingsOpen = true"
|
|
1074
|
+
/>
|
|
1075
|
+
</div>
|
|
1046
1076
|
|
|
1047
|
-
|
|
1077
|
+
<!-- Runs — this workflow's full execution history. Manual runs start
|
|
1048
1078
|
from the editor's Run button, through the run dialog. -->
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1079
|
+
<div v-else-if="tab === 'runs'" class="p-4 space-y-4 overflow-y-auto">
|
|
1080
|
+
<div v-if="runsError" class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700">
|
|
1081
|
+
{{ runsError }}
|
|
1082
|
+
</div>
|
|
1053
1083
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1084
|
+
<RunsTable
|
|
1085
|
+
:runs="wfRuns"
|
|
1086
|
+
:loading="runsLoading"
|
|
1087
|
+
:pagination="false"
|
|
1088
|
+
:show-workflow="false"
|
|
1089
|
+
:empty-text="t('integration.editor.runs.empty')"
|
|
1090
|
+
@changed="() => loadRuns()"
|
|
1091
|
+
/>
|
|
1062
1092
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1093
|
+
<!-- Server pagination — the endpoint pages, so the whole history is reachable. -->
|
|
1094
|
+
<div v-if="runsLastPage > 1 || runsTotal > wfRuns.length" class="flex items-center justify-between text-sm text-muted-foreground">
|
|
1095
|
+
<span class="tabular-nums">{{ t("integration.editor.runs.pagination.page", { page: runsPage, lastPage: runsLastPage }) }} · {{ t("integration.editor.runs.pagination.count", runsTotal) }}</span>
|
|
1096
|
+
<div class="flex gap-2">
|
|
1097
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsWanted <= 1 || runsLoading" @click="goRunsPage(runsWanted - 1)">
|
|
1098
|
+
{{ t("integration.editor.runs.pagination.newer") }}
|
|
1099
|
+
</Button>
|
|
1100
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsWanted >= runsLastPage || runsLoading" @click="goRunsPage(runsWanted + 1)">
|
|
1101
|
+
{{ t("integration.editor.runs.pagination.older") }}
|
|
1102
|
+
</Button>
|
|
1103
|
+
</div>
|
|
1073
1104
|
</div>
|
|
1074
1105
|
</div>
|
|
1075
|
-
</div>
|
|
1076
1106
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1107
|
+
<!-- Dead letters — triggers that never produced a run -->
|
|
1108
|
+
<div v-else-if="tab === 'dead-letters'" class="p-6 overflow-y-auto">
|
|
1109
|
+
<IntegrationsDeadLettersPanel :workflow-id="workflow.id" />
|
|
1110
|
+
</div>
|
|
1081
1111
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1112
|
+
<!-- History — n8n-style: read-only canvas preview + revision list. -->
|
|
1113
|
+
<div v-else-if="tab === 'history'" class="flex-1 min-h-0">
|
|
1114
|
+
<WorkflowHistoryPanel :workflow="workflow" :draft-pending="isDirty" @restored="onRevisionRestored" />
|
|
1115
|
+
</div>
|
|
1086
1116
|
</template>
|
|
1087
1117
|
|
|
1088
1118
|
<div v-else class="p-12 text-center text-sm text-muted-foreground">
|
|
@@ -1197,6 +1227,56 @@ function formatDateTime(iso) {
|
|
|
1197
1227
|
|
|
1198
1228
|
<WorkflowDuplicateDialog v-model:open="duplicateOpen" :workflow="duplicateTarget" @duplicated="onDuplicated" />
|
|
1199
1229
|
|
|
1230
|
+
<WorkflowExportDialog ref="exporter" />
|
|
1231
|
+
|
|
1232
|
+
<!-- The menu item opens the browser's own file picker; this input is only its
|
|
1233
|
+
carrier, and it carries a name so it is not an unnamed control. -->
|
|
1234
|
+
<input
|
|
1235
|
+
ref="importInput"
|
|
1236
|
+
type="file"
|
|
1237
|
+
accept=".json,application/json"
|
|
1238
|
+
class="hidden"
|
|
1239
|
+
:aria-label="t('integration.workflowFile.field.file')"
|
|
1240
|
+
@change="onImportPicked"
|
|
1241
|
+
>
|
|
1242
|
+
|
|
1243
|
+
<!-- Importing over this workflow. Says how much goes, that the credentials have
|
|
1244
|
+
to be chosen again, and — where it applies — that the running version keeps
|
|
1245
|
+
running and that unsaved work is thrown away. -->
|
|
1246
|
+
<AlertDialog :open="importOpen" @update:open="(v) => {
|
|
1247
|
+
if (!v) importOpen = false;
|
|
1248
|
+
}">
|
|
1249
|
+
<AlertDialogContent>
|
|
1250
|
+
<AlertDialogHeader>
|
|
1251
|
+
<AlertDialogTitle>{{ t("integration.workflowFile.overwrite.title") }}</AlertDialogTitle>
|
|
1252
|
+
<AlertDialogDescription v-if="importPlan && workflow">
|
|
1253
|
+
{{ t("integration.workflowFile.overwrite.description", {
|
|
1254
|
+
name: workflow.name,
|
|
1255
|
+
current: t("integration.workflowFile.count.both", {
|
|
1256
|
+
nodes: t("integration.workflowFile.count.nodes", importPlan.replaced.nodes),
|
|
1257
|
+
triggers: t("integration.workflowFile.count.triggers", importPlan.replaced.triggers)
|
|
1258
|
+
}),
|
|
1259
|
+
incoming: t("integration.workflowFile.count.both", {
|
|
1260
|
+
nodes: t("integration.workflowFile.count.nodes", importPlan.incoming.nodes),
|
|
1261
|
+
triggers: t("integration.workflowFile.count.triggers", importPlan.incoming.triggers)
|
|
1262
|
+
})
|
|
1263
|
+
}) }}
|
|
1264
|
+
</AlertDialogDescription>
|
|
1265
|
+
</AlertDialogHeader>
|
|
1266
|
+
<ul class="space-y-1 text-sm text-muted-foreground">
|
|
1267
|
+
<li>{{ t("integration.workflowFile.overwrite.save") }}</li>
|
|
1268
|
+
<li v-if="importPlan?.active" class="text-amber-700">{{ t("integration.workflowFile.overwrite.active") }}</li>
|
|
1269
|
+
<li v-if="importPlan?.dirty" class="text-amber-700">{{ t("integration.workflowFile.overwrite.draft") }}</li>
|
|
1270
|
+
</ul>
|
|
1271
|
+
<AlertDialogFooter>
|
|
1272
|
+
<AlertDialogCancel as-child>
|
|
1273
|
+
<Button variant="outline">{{ t("integration.common.action.cancel") }}</Button>
|
|
1274
|
+
</AlertDialogCancel>
|
|
1275
|
+
<Button @click="confirmImport">{{ t("integration.workflowFile.overwrite.confirm") }}</Button>
|
|
1276
|
+
</AlertDialogFooter>
|
|
1277
|
+
</AlertDialogContent>
|
|
1278
|
+
</AlertDialog>
|
|
1279
|
+
|
|
1200
1280
|
<!-- Leaving the workflow with unsaved work.
|
|
1201
1281
|
Staying is the default in the only sense that decides anything: it holds the
|
|
1202
1282
|
focus when the dialog opens, so Enter or Space keeps the work. That is what
|