@revenexx/studio-integrations 0.5.1 → 0.7.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 +428 -0
- package/README.md +48 -4
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +10 -2
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
- package/dist/runtime/components/Integrations/Layout.vue +48 -4
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
- package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
- package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
- package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
- package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +8 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
- package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
- package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
- package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
- package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
- package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
- package/dist/runtime/composables/useIntegrationsT.js +9 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
- package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
- package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
- package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/branchReorder.d.ts +37 -0
- package/dist/runtime/data/branchReorder.js +71 -0
- package/dist/runtime/data/handleId.d.ts +23 -0
- package/dist/runtime/data/handleId.js +6 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +14 -0
- package/dist/runtime/data/integrationTemplates.d.ts +48 -0
- package/dist/runtime/data/integrationTemplates.js +21 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
- package/dist/runtime/data/nodeIcons.d.ts +2 -0
- package/dist/runtime/data/nodeIcons.js +73 -0
- package/dist/runtime/data/nodeInputData.d.ts +16 -4
- package/dist/runtime/data/nodeInputData.js +4 -0
- package/dist/runtime/data/payloadForm.d.ts +9 -2
- package/dist/runtime/data/payloadForm.js +29 -4
- package/dist/runtime/data/portTone.d.ts +17 -0
- package/dist/runtime/data/portTone.js +7 -0
- package/dist/runtime/data/webhookAuth.d.ts +18 -0
- package/dist/runtime/data/webhookAuth.js +19 -0
- package/dist/runtime/data/workflowIconCatalog.js +4 -6
- package/dist/runtime/i18n/messages/de.json +1207 -0
- package/dist/runtime/i18n/messages/en.json +1207 -0
- package/dist/runtime/i18n/translate.d.ts +83 -0
- package/dist/runtime/i18n/translate.js +41 -0
- package/dist/runtime/pages/integrations/audit.vue +140 -37
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
- package/dist/runtime/pages/integrations/credentials.vue +180 -64
- package/dist/runtime/pages/integrations/index.vue +91 -92
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +88 -23
- package/dist/runtime/pages/integrations/secrets.vue +57 -35
- package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
- package/dist/runtime/pages/integrations/templates/index.vue +82 -103
- package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
- package/dist/runtime/plugins/locale.d.ts +10 -0
- package/dist/runtime/plugins/locale.js +6 -0
- package/dist/runtime/plugins/realtime.client.d.ts +32 -0
- package/dist/runtime/plugins/realtime.client.js +4 -0
- package/dist/runtime/types/integrations.d.ts +65 -6
- package/dist/runtime/utils/apiError.d.ts +47 -0
- package/dist/runtime/utils/apiError.js +32 -0
- package/dist/runtime/utils/cockpitSources.d.ts +153 -0
- package/dist/runtime/utils/cockpitSources.js +14 -0
- package/dist/runtime/utils/credentialState.d.ts +24 -1
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +46 -4
- package/dist/runtime/utils/localize.js +34 -7
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -0
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
- package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
- package/dist/runtime/utils/schedule.d.ts +116 -0
- package/dist/runtime/utils/schedule.js +219 -0
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +54 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/dist/types.d.mts +2 -6
- package/package.json +10 -3
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
|
3
|
-
import { AltArrowLeft as ArrowLeft, AltArrowRight as Chevron, Copy as CopyIcon, Pause as PauseIcon, RefreshCircle as Loader2, Play, Refresh as RetryIcon, Diskette as Save } from "#solar-icons";
|
|
2
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
|
3
|
+
import { AltArrowLeft as ArrowLeft, AltArrowRight as Chevron, Copy as CopyIcon, Pause as PauseIcon, RefreshCircle as Loader2, Play, Refresh as RetryIcon, Diskette as Save, SidebarMinimalistic as RailIcon } from "#solar-icons";
|
|
4
4
|
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogContent,
|
|
7
|
+
AlertDialogDescription,
|
|
8
|
+
AlertDialogFooter,
|
|
9
|
+
AlertDialogHeader,
|
|
10
|
+
AlertDialogTitle,
|
|
5
11
|
Badge,
|
|
6
12
|
Button,
|
|
7
13
|
Dialog,
|
|
@@ -24,28 +30,47 @@ import WorkflowDuplicateDialog from "../../../components/Integrations/WorkflowDu
|
|
|
24
30
|
import RunsTable from "../../../components/Integrations/RunsTable.vue";
|
|
25
31
|
import RunDetailPanel from "../../../components/Integrations/RunDetailPanel.vue";
|
|
26
32
|
import { toast } from "vue-sonner";
|
|
27
|
-
import { IntegrationsApiError } from "../../../utils/apiError";
|
|
33
|
+
import { IntegrationsApiError, apiValidationIssues } from "../../../utils/apiError";
|
|
34
|
+
import { createPollBudget, SAFETY_POLL_MS } from "../../../utils/pollBudget";
|
|
35
|
+
import { findingsSummary, groupFindingsByNode, nodeDisplayLabel } from "../../../utils/validationFindings";
|
|
28
36
|
import { payloadFieldsFromBlob } from "../../../data/payloadForm";
|
|
29
37
|
import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
|
|
38
|
+
const t = useIntegrationsT();
|
|
30
39
|
const route = useRoute();
|
|
31
40
|
const router = useRouter();
|
|
32
41
|
const { workflows, get, update, toggleActive } = useIntegrationsWorkflows();
|
|
33
|
-
const { autoEnable } = useIntegrationsFocus();
|
|
34
|
-
autoEnable();
|
|
35
42
|
const id = computed(() => Number(route.params.id));
|
|
36
43
|
const workflow = computed(() => workflows.value.find((w) => w.id === id.value) ?? null);
|
|
37
|
-
useHead({ title: () =>
|
|
44
|
+
useHead({ title: () => t("integration.editor.head.title", { name: workflow.value?.name ?? t("integration.editor.head.untitled") }) });
|
|
38
45
|
const tab = ref("editor");
|
|
39
|
-
const tabs = [
|
|
40
|
-
{ key: "editor", label: "
|
|
41
|
-
{ key: "history", label: "
|
|
42
|
-
{ key: "runs", label: "
|
|
43
|
-
{ key: "dead-letters", label: "
|
|
44
|
-
];
|
|
46
|
+
const tabs = computed(() => [
|
|
47
|
+
{ key: "editor", label: t("integration.editor.tab.editor") },
|
|
48
|
+
{ key: "history", label: t("integration.editor.tab.history") },
|
|
49
|
+
{ key: "runs", label: t("integration.editor.tab.runs") },
|
|
50
|
+
{ key: "dead-letters", label: t("integration.editor.tab.deadLetters") }
|
|
51
|
+
]);
|
|
45
52
|
const dirtyBlob = ref(null);
|
|
46
53
|
const isDirty = computed(() => dirtyBlob.value !== null);
|
|
47
54
|
const saving = ref(false);
|
|
48
55
|
const saveError = ref(null);
|
|
56
|
+
const validationWarnings = computed(() => workflow.value?.warnings);
|
|
57
|
+
const isInvalid = computed(() => (validationWarnings.value?.length ?? 0) > 0);
|
|
58
|
+
const WARN_COLLAPSED = 3;
|
|
59
|
+
const warningsExpanded = ref(false);
|
|
60
|
+
const warningGroups = computed(() => groupFindingsByNode(validationWarnings.value ?? [], workflow.value?.blob.nodes ?? []));
|
|
61
|
+
const warningSummary = computed(() => findingsSummary(validationWarnings.value?.length ?? 0, warningGroups.value.filter((g) => g.nodeId).length));
|
|
62
|
+
const editorRef = ref(null);
|
|
63
|
+
async function revealNode(nodeId) {
|
|
64
|
+
tab.value = "editor";
|
|
65
|
+
await nextTick();
|
|
66
|
+
editorRef.value?.revealNode(nodeId);
|
|
67
|
+
}
|
|
68
|
+
const { load: loadNodeCatalog } = useIntegrationsNodes();
|
|
69
|
+
onMounted(() => {
|
|
70
|
+
void loadNodeCatalog();
|
|
71
|
+
});
|
|
72
|
+
const deactivationOpen = ref(false);
|
|
73
|
+
const deactivationIssues = ref([]);
|
|
49
74
|
const { load: loadCredentials, needsReauth } = useIntegrationsCredentials();
|
|
50
75
|
onMounted(() => {
|
|
51
76
|
void loadCredentials();
|
|
@@ -70,12 +95,12 @@ async function waitForBuildReady() {
|
|
|
70
95
|
const status = fresh?.buildStatus;
|
|
71
96
|
if (status === "pending" || status === "building") continue;
|
|
72
97
|
if (status === "failed") {
|
|
73
|
-
toast.error("
|
|
98
|
+
toast.error(t("integration.editor.build.toast.failed"));
|
|
74
99
|
return false;
|
|
75
100
|
}
|
|
76
101
|
return true;
|
|
77
102
|
}
|
|
78
|
-
toast.error("
|
|
103
|
+
toast.error(t("integration.editor.build.toast.timeout"));
|
|
79
104
|
return false;
|
|
80
105
|
} finally {
|
|
81
106
|
waitingForBuild.value = false;
|
|
@@ -84,13 +109,13 @@ async function waitForBuildReady() {
|
|
|
84
109
|
const buildNotice = computed(() => {
|
|
85
110
|
switch (workflow.value?.buildStatus) {
|
|
86
111
|
case "building":
|
|
87
|
-
return { tone: "info", text: "
|
|
112
|
+
return { tone: "info", text: t("integration.editor.build.notice.building") };
|
|
88
113
|
case "pending":
|
|
89
|
-
return { tone: "info", text: "
|
|
114
|
+
return { tone: "info", text: t("integration.editor.build.notice.pending") };
|
|
90
115
|
case "failed":
|
|
91
116
|
return {
|
|
92
117
|
tone: "error",
|
|
93
|
-
text:
|
|
118
|
+
text: workflow.value?.buildError ? t("integration.editor.build.notice.failedWithError", { error: workflow.value.buildError }) : t("integration.editor.build.notice.failed")
|
|
94
119
|
};
|
|
95
120
|
default:
|
|
96
121
|
return null;
|
|
@@ -100,6 +125,7 @@ watch(id, async () => {
|
|
|
100
125
|
const w = await get(id.value);
|
|
101
126
|
if (!w) await router.push("/integrations/workflows");
|
|
102
127
|
dirtyBlob.value = null;
|
|
128
|
+
warningsExpanded.value = false;
|
|
103
129
|
if (tab.value === "runs") void loadRuns(1);
|
|
104
130
|
}, { immediate: true });
|
|
105
131
|
function onChange(blob) {
|
|
@@ -113,7 +139,7 @@ function discardChanges() {
|
|
|
113
139
|
const duplicateOpen = ref(false);
|
|
114
140
|
const duplicateTarget = computed(() => workflow.value ? { id: workflow.value.id, name: workflow.value.name } : null);
|
|
115
141
|
async function onDuplicated(copy) {
|
|
116
|
-
toast.success(
|
|
142
|
+
toast.success(t("integration.editor.toast.duplicated", { name: copy.name }));
|
|
117
143
|
await router.push(`/integrations/workflows/${copy.id}`);
|
|
118
144
|
}
|
|
119
145
|
async function onRevisionRestored() {
|
|
@@ -146,11 +172,34 @@ function viewRunFromModal(id2) {
|
|
|
146
172
|
}
|
|
147
173
|
function syncErrorMessage(e) {
|
|
148
174
|
if (e instanceof IntegrationsApiError) {
|
|
149
|
-
if (e.status === 504) return "
|
|
150
|
-
if (e.status === 422 && e.body?.status === "failed") return "
|
|
151
|
-
return e.message || "
|
|
175
|
+
if (e.status === 504) return t("integration.editor.run.error.syncTimeout");
|
|
176
|
+
if (e.status === 422 && e.body?.status === "failed") return t("integration.editor.run.error.syncFailed");
|
|
177
|
+
return e.message || t("integration.editor.run.error.syncFailed");
|
|
178
|
+
}
|
|
179
|
+
return e?.message || t("integration.editor.run.error.syncFailed");
|
|
180
|
+
}
|
|
181
|
+
function refusedForSettings(e) {
|
|
182
|
+
const issues = apiValidationIssues(e);
|
|
183
|
+
if (!issues.length) return null;
|
|
184
|
+
const groups = groupFindingsByNode(issues, workflow.value?.blob.nodes ?? []);
|
|
185
|
+
const named = groups.filter((g) => g.nodeId);
|
|
186
|
+
const summary = findingsSummary(issues.length, named.length);
|
|
187
|
+
const parts = [
|
|
188
|
+
...named.map((g) => `${g.label} (${g.fields.join(", ")})`),
|
|
189
|
+
...groups.filter((g) => !g.nodeId).flatMap((g) => g.messages)
|
|
190
|
+
];
|
|
191
|
+
const where = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
|
|
192
|
+
const line = t("integration.editor.run.error.refusedForSettings", { summary, where });
|
|
193
|
+
return /[.!?]$/.test(line) ? line : `${line}.`;
|
|
194
|
+
}
|
|
195
|
+
async function reportRunFailure(e, fallback) {
|
|
196
|
+
const settings = refusedForSettings(e);
|
|
197
|
+
if (!settings) {
|
|
198
|
+
syncError.value = fallback(e);
|
|
199
|
+
return;
|
|
152
200
|
}
|
|
153
|
-
|
|
201
|
+
syncError.value = settings;
|
|
202
|
+
if (workflow.value) await get(workflow.value.id);
|
|
154
203
|
}
|
|
155
204
|
async function run(mode) {
|
|
156
205
|
if (!workflow.value) return;
|
|
@@ -158,7 +207,7 @@ async function run(mode) {
|
|
|
158
207
|
try {
|
|
159
208
|
payload = JSON.parse(payloadText.value || "{}");
|
|
160
209
|
} catch {
|
|
161
|
-
payloadError.value = "
|
|
210
|
+
payloadError.value = t("integration.editor.payload.error.invalidJson");
|
|
162
211
|
return;
|
|
163
212
|
}
|
|
164
213
|
payloadError.value = null;
|
|
@@ -179,7 +228,7 @@ async function run(mode) {
|
|
|
179
228
|
syncResponse.value = await triggerRun(workflow.value.id, payload, "sync");
|
|
180
229
|
runModalRunId.value = extractRunId(syncResponse.value);
|
|
181
230
|
} catch (e) {
|
|
182
|
-
|
|
231
|
+
await reportRunFailure(e, syncErrorMessage);
|
|
183
232
|
if (e instanceof IntegrationsApiError) {
|
|
184
233
|
runModalRunId.value = extractRunId(e.body?.run);
|
|
185
234
|
}
|
|
@@ -194,7 +243,7 @@ async function run(mode) {
|
|
|
194
243
|
const res = await triggerRun(workflow.value.id, payload, "async");
|
|
195
244
|
runModalRunId.value = extractRunId(res);
|
|
196
245
|
} catch (e) {
|
|
197
|
-
|
|
246
|
+
await reportRunFailure(e, (err) => err?.message || t("integration.editor.run.error.couldNotStart"));
|
|
198
247
|
} finally {
|
|
199
248
|
running.value = false;
|
|
200
249
|
syncLoading.value = false;
|
|
@@ -217,7 +266,7 @@ const eventLog = typeof useIntegrationsEvents === "function" ? useIntegrationsEv
|
|
|
217
266
|
} };
|
|
218
267
|
const prefillBusy = ref(null);
|
|
219
268
|
const eventTriggerTopic = computed(() => {
|
|
220
|
-
const trigger = (workflow.value?.blob.triggers ?? []).find((
|
|
269
|
+
const trigger = (workflow.value?.blob.triggers ?? []).find((t2) => t2.type === "event");
|
|
221
270
|
const topic = trigger?.config?.event_type;
|
|
222
271
|
return typeof topic === "string" && topic.trim() !== "" ? topic.trim() : null;
|
|
223
272
|
});
|
|
@@ -252,7 +301,7 @@ async function insertLatestEvent() {
|
|
|
252
301
|
await eventLog.loadEvents();
|
|
253
302
|
const match = eventLog.events.value.filter((e) => e.topic === topic).sort((a, b) => (b.time ?? "").localeCompare(a.time ?? ""))[0];
|
|
254
303
|
if (!match) {
|
|
255
|
-
toast.info(
|
|
304
|
+
toast.info(t("integration.editor.event.toast.noneReceived", { topic }));
|
|
256
305
|
return;
|
|
257
306
|
}
|
|
258
307
|
payloadText.value = envelope(topic, match.data, {
|
|
@@ -278,6 +327,9 @@ const runsError = ref(null);
|
|
|
278
327
|
const runsPage = ref(1);
|
|
279
328
|
const runsTotal = ref(0);
|
|
280
329
|
const runsLastPage = ref(1);
|
|
330
|
+
const RUNS_POLL_MS = 3e3;
|
|
331
|
+
const { delivering: realtimeDelivering, runTick, lastRunEvent, buildTick, lastBuildEvent } = useIntegrationsRealtime();
|
|
332
|
+
const runsBudget = createPollBudget({ intervalMs: RUNS_POLL_MS, liveIntervalMs: SAFETY_POLL_MS });
|
|
281
333
|
let runsPoll = null;
|
|
282
334
|
function stopRunsPoll() {
|
|
283
335
|
if (runsPoll) {
|
|
@@ -288,12 +340,25 @@ function stopRunsPoll() {
|
|
|
288
340
|
function startRunsPoll() {
|
|
289
341
|
stopRunsPoll();
|
|
290
342
|
runsPoll = setInterval(() => {
|
|
291
|
-
if (runsPage.value === 1) void loadRuns(1, { silent: true });
|
|
292
|
-
},
|
|
343
|
+
if (runsPage.value === 1 && runsBudget.due(realtimeDelivering.value)) void loadRuns(1, { silent: true });
|
|
344
|
+
}, RUNS_POLL_MS);
|
|
293
345
|
}
|
|
294
346
|
onUnmounted(stopRunsPoll);
|
|
347
|
+
watch(runTick, () => {
|
|
348
|
+
if (tab.value !== "runs" || runsPage.value !== 1) return;
|
|
349
|
+
const eventWorkflow = workflowIdOf(lastRunEvent.value);
|
|
350
|
+
if (eventWorkflow && eventWorkflow !== String(workflow.value?.id ?? "")) return;
|
|
351
|
+
void loadRuns(1, { silent: true });
|
|
352
|
+
});
|
|
353
|
+
watch(buildTick, () => {
|
|
354
|
+
if (!workflow.value) return;
|
|
355
|
+
const eventWorkflow = lastBuildEvent.value?.topicId ?? workflowIdOf(lastBuildEvent.value);
|
|
356
|
+
if (eventWorkflow && eventWorkflow !== String(workflow.value.id)) return;
|
|
357
|
+
void get(workflow.value.id);
|
|
358
|
+
});
|
|
295
359
|
async function loadRuns(page = runsPage.value, opts = {}) {
|
|
296
360
|
if (!workflow.value) return;
|
|
361
|
+
runsBudget.spend();
|
|
297
362
|
if (!opts.silent) runsLoading.value = true;
|
|
298
363
|
runsError.value = null;
|
|
299
364
|
try {
|
|
@@ -307,7 +372,7 @@ async function loadRuns(page = runsPage.value, opts = {}) {
|
|
|
307
372
|
runsLastPage.value = res.lastPage;
|
|
308
373
|
runsPage.value = res.page;
|
|
309
374
|
} catch (e) {
|
|
310
|
-
runsError.value = e.message || "
|
|
375
|
+
runsError.value = e.message || t("integration.editor.runs.error.load");
|
|
311
376
|
} finally {
|
|
312
377
|
if (!opts.silent) runsLoading.value = false;
|
|
313
378
|
}
|
|
@@ -316,20 +381,43 @@ function goRunsPage(p) {
|
|
|
316
381
|
const next = Math.min(Math.max(1, p), runsLastPage.value);
|
|
317
382
|
if (next !== runsPage.value) void loadRuns(next);
|
|
318
383
|
}
|
|
319
|
-
watch(tab, (
|
|
320
|
-
if (
|
|
384
|
+
watch(tab, (t2) => {
|
|
385
|
+
if (t2 === "runs") {
|
|
321
386
|
void loadRuns(1);
|
|
322
387
|
startRunsPoll();
|
|
323
388
|
} else stopRunsPoll();
|
|
324
389
|
});
|
|
325
390
|
const settingsOpen = ref(false);
|
|
326
391
|
const openRunId = computed(() => route.query.run != null ? String(route.query.run) : null);
|
|
392
|
+
const { focused: railFolded, enable, disable } = useIntegrationsFocus();
|
|
393
|
+
const WIDE_CANVAS_KEY = "integrations.editor.wideCanvas";
|
|
394
|
+
const wideCanvasWanted = ref(false);
|
|
395
|
+
try {
|
|
396
|
+
wideCanvasWanted.value = localStorage.getItem(WIDE_CANVAS_KEY) === "1";
|
|
397
|
+
} catch {
|
|
398
|
+
}
|
|
399
|
+
const canvasFillsPage = computed(() => tab.value === "editor" && !openRunId.value);
|
|
400
|
+
const railShouldFold = computed(() => wideCanvasWanted.value && canvasFillsPage.value);
|
|
401
|
+
function applyRail() {
|
|
402
|
+
if (railShouldFold.value) enable();
|
|
403
|
+
else disable();
|
|
404
|
+
}
|
|
405
|
+
onMounted(() => applyRail());
|
|
406
|
+
watch(railShouldFold, () => applyRail());
|
|
407
|
+
onUnmounted(() => disable());
|
|
408
|
+
function toggleRail() {
|
|
409
|
+
wideCanvasWanted.value = !wideCanvasWanted.value;
|
|
410
|
+
try {
|
|
411
|
+
localStorage.setItem(WIDE_CANVAS_KEY, wideCanvasWanted.value ? "1" : "0");
|
|
412
|
+
} catch {
|
|
413
|
+
}
|
|
414
|
+
}
|
|
327
415
|
const runView = ref("details");
|
|
328
|
-
const runViews = [
|
|
329
|
-
{ key: "details", label: "
|
|
330
|
-
{ key: "timeline", label: "
|
|
331
|
-
{ key: "replay", label: "
|
|
332
|
-
];
|
|
416
|
+
const runViews = computed(() => [
|
|
417
|
+
{ key: "details", label: t("integration.editor.runView.details") },
|
|
418
|
+
{ key: "timeline", label: t("integration.editor.runView.timeline") },
|
|
419
|
+
{ key: "replay", label: t("integration.editor.runView.replay") }
|
|
420
|
+
]);
|
|
333
421
|
const openRun = ref(null);
|
|
334
422
|
const openRunError = ref(null);
|
|
335
423
|
async function loadOpenRun() {
|
|
@@ -338,7 +426,7 @@ async function loadOpenRun() {
|
|
|
338
426
|
try {
|
|
339
427
|
openRun.value = await fetchRun(workflow.value.id, openRunId.value, workflow.value.name);
|
|
340
428
|
} catch (e) {
|
|
341
|
-
openRunError.value = e?.message ?? "
|
|
429
|
+
openRunError.value = e?.message ?? t("integration.editor.run.error.loadOpenRun");
|
|
342
430
|
}
|
|
343
431
|
}
|
|
344
432
|
watch(openRunId, () => {
|
|
@@ -361,26 +449,26 @@ async function retryOpenRun() {
|
|
|
361
449
|
try {
|
|
362
450
|
const res = await retryRun(openRun.value.workflowId, openRun.value.id);
|
|
363
451
|
const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
|
|
364
|
-
toast.success(newId && newId !== openRun.value.id ?
|
|
452
|
+
toast.success(newId && newId !== openRun.value.id ? t("integration.editor.retry.toast.started", { id: newId }) : t("integration.editor.retry.toast.requested"));
|
|
365
453
|
void loadRuns(1, { silent: true });
|
|
366
454
|
} catch (e) {
|
|
367
|
-
toast.error(e?.message ?? "
|
|
455
|
+
toast.error(e?.message ?? t("integration.editor.retry.error.generic"));
|
|
368
456
|
} finally {
|
|
369
457
|
retryBusy.value = false;
|
|
370
458
|
}
|
|
371
459
|
}
|
|
372
|
-
const EXECUTION_MODES = [
|
|
373
|
-
{ value: "async_only", label: "
|
|
374
|
-
{ value: "sync_only", label: "
|
|
375
|
-
{ value: "caller_decides", label: "
|
|
376
|
-
];
|
|
460
|
+
const EXECUTION_MODES = computed(() => [
|
|
461
|
+
{ value: "async_only", label: t("integration.editor.executionMode.async.label"), hint: t("integration.editor.executionMode.async.hint") },
|
|
462
|
+
{ value: "sync_only", label: t("integration.editor.executionMode.sync.label"), hint: t("integration.editor.executionMode.sync.hint") },
|
|
463
|
+
{ value: "caller_decides", label: t("integration.editor.executionMode.callerDecides.label"), hint: t("integration.editor.executionMode.callerDecides.hint") }
|
|
464
|
+
]);
|
|
377
465
|
const executionModeDraft = ref("async_only");
|
|
378
466
|
watch(workflow, (w) => {
|
|
379
467
|
if (w) executionModeDraft.value = w.execution_mode ?? "async_only";
|
|
380
468
|
}, { immediate: true });
|
|
381
469
|
const savingMode = ref(false);
|
|
382
470
|
function modeLabel(m) {
|
|
383
|
-
return m === "sync_only" ? "
|
|
471
|
+
return m === "sync_only" ? t("integration.editor.executionMode.sync.short") : m === "caller_decides" ? t("integration.editor.executionMode.callerDecides.label") : t("integration.editor.executionMode.async.short");
|
|
384
472
|
}
|
|
385
473
|
async function saveExecutionMode(mode) {
|
|
386
474
|
if (!workflow.value || mode === workflow.value.execution_mode) return;
|
|
@@ -399,7 +487,7 @@ async function saveExecutionMode(mode) {
|
|
|
399
487
|
await get(workflow.value.id);
|
|
400
488
|
} catch (e) {
|
|
401
489
|
executionModeDraft.value = prev;
|
|
402
|
-
saveError.value = e.message || "
|
|
490
|
+
saveError.value = e.message || t("integration.editor.executionMode.error.generic");
|
|
403
491
|
} finally {
|
|
404
492
|
savingMode.value = false;
|
|
405
493
|
}
|
|
@@ -407,7 +495,7 @@ async function saveExecutionMode(mode) {
|
|
|
407
495
|
const savingMeta = ref(false);
|
|
408
496
|
const tagSuggestions = computed(() => {
|
|
409
497
|
const all = /* @__PURE__ */ new Set();
|
|
410
|
-
for (const wf of workflows.value) for (const
|
|
498
|
+
for (const wf of workflows.value) for (const t2 of wf.blob.tags ?? []) all.add(t2);
|
|
411
499
|
return [...all].sort();
|
|
412
500
|
});
|
|
413
501
|
async function saveWorkflowMeta(meta) {
|
|
@@ -432,15 +520,34 @@ async function saveWorkflowMeta(meta) {
|
|
|
432
520
|
});
|
|
433
521
|
await get(workflow.value.id);
|
|
434
522
|
dirtyBlob.value = null;
|
|
435
|
-
toast.success("
|
|
523
|
+
toast.success(t("integration.editor.appearance.toast.saved"));
|
|
436
524
|
} catch (e) {
|
|
437
|
-
const msg = e.message || "
|
|
525
|
+
const msg = e.message || t("integration.editor.appearance.error.generic");
|
|
438
526
|
saveError.value = msg;
|
|
439
527
|
toast.error(msg);
|
|
440
528
|
} finally {
|
|
441
529
|
savingMeta.value = false;
|
|
442
530
|
}
|
|
443
531
|
}
|
|
532
|
+
async function onToggleActive() {
|
|
533
|
+
const wf = workflow.value;
|
|
534
|
+
if (!wf) return;
|
|
535
|
+
if (!wf.active && isInvalid.value) {
|
|
536
|
+
toast.error(t("integration.editor.activate.error.needsFixing", validationWarnings.value?.length ?? 0));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
await toggleActive(wf.id);
|
|
541
|
+
} catch (e) {
|
|
542
|
+
const issues = apiValidationIssues(e);
|
|
543
|
+
if (issues.length) {
|
|
544
|
+
await get(wf.id);
|
|
545
|
+
toast.error(t("integration.editor.activate.error.validationIssues", issues.length));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
toast.error(e.message || t("integration.editor.activate.error.generic"));
|
|
549
|
+
}
|
|
550
|
+
}
|
|
444
551
|
async function save() {
|
|
445
552
|
if (!workflow.value || !dirtyBlob.value) return;
|
|
446
553
|
saving.value = true;
|
|
@@ -455,7 +562,43 @@ async function save() {
|
|
|
455
562
|
});
|
|
456
563
|
dirtyBlob.value = null;
|
|
457
564
|
} catch (e) {
|
|
458
|
-
|
|
565
|
+
const issues = apiValidationIssues(e);
|
|
566
|
+
if (issues.length && workflow.value.active) {
|
|
567
|
+
const nodes = workflow.value.blob.nodes;
|
|
568
|
+
deactivationIssues.value = issues.map((i) => {
|
|
569
|
+
const m = /^blob\.nodes\.(\d+)\b/.exec(i.path);
|
|
570
|
+
const node = m ? nodes[Number(m[1])] : void 0;
|
|
571
|
+
return { ...i, where: node ? nodeDisplayLabel(node) : null };
|
|
572
|
+
});
|
|
573
|
+
deactivationOpen.value = true;
|
|
574
|
+
} else {
|
|
575
|
+
saveError.value = e.message || t("integration.editor.save.error.generic");
|
|
576
|
+
}
|
|
577
|
+
} finally {
|
|
578
|
+
saving.value = false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async function confirmDeactivate() {
|
|
582
|
+
if (!workflow.value || !dirtyBlob.value) {
|
|
583
|
+
deactivationOpen.value = false;
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
saving.value = true;
|
|
587
|
+
try {
|
|
588
|
+
await update(workflow.value.id, {
|
|
589
|
+
name: workflow.value.name,
|
|
590
|
+
description: workflow.value.description,
|
|
591
|
+
blob: dirtyBlob.value,
|
|
592
|
+
active: false,
|
|
593
|
+
executionMode: workflow.value.execution_mode
|
|
594
|
+
});
|
|
595
|
+
await get(workflow.value.id);
|
|
596
|
+
dirtyBlob.value = null;
|
|
597
|
+
deactivationOpen.value = false;
|
|
598
|
+
toast.success(t("integration.editor.deactivate.toast.savedPaused"));
|
|
599
|
+
} catch (e) {
|
|
600
|
+
deactivationOpen.value = false;
|
|
601
|
+
saveError.value = e.message || t("integration.editor.save.error.generic");
|
|
459
602
|
} finally {
|
|
460
603
|
saving.value = false;
|
|
461
604
|
}
|
|
@@ -479,41 +622,67 @@ function formatDateTime(iso) {
|
|
|
479
622
|
<PageHeader :title="workflow.name">
|
|
480
623
|
<template #title>
|
|
481
624
|
<span class="inline-flex min-w-0 items-center gap-1.5">
|
|
482
|
-
<NuxtLink to="/integrations/workflows" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary">
|
|
625
|
+
<NuxtLink to="/integrations/workflows" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary">{{ t("integration.editor.breadcrumb.workflows") }}</NuxtLink>
|
|
483
626
|
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
484
627
|
<!-- Run open: the breadcrumb extends — name and Runs become crumbs
|
|
485
628
|
back into the workflow surface. -->
|
|
486
629
|
<template v-if="openRunId">
|
|
487
630
|
<button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('editor')">{{ workflow.name }}</button>
|
|
488
631
|
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
489
|
-
<button type="button" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('runs')">
|
|
632
|
+
<button type="button" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('runs')">{{ t("integration.editor.breadcrumb.runs") }}</button>
|
|
490
633
|
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
491
634
|
<!-- The Temporal run id IS the run's identity; the internal
|
|
492
635
|
number only fills in until the run is handed off. -->
|
|
493
|
-
<span class="truncate font-mono">{{ openRun?.temporalRunId ??
|
|
636
|
+
<span class="truncate font-mono">{{ openRun?.temporalRunId ?? t("integration.editor.breadcrumb.runNumber", { id: openRunId }) }}</span>
|
|
494
637
|
</template>
|
|
495
638
|
<template v-else>
|
|
496
639
|
<span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
497
640
|
<WorkflowIconView :icon="workflow.blob.icon" :size="16" />
|
|
498
641
|
</span>
|
|
499
642
|
<span class="truncate">{{ workflow.name }}</span>
|
|
500
|
-
<Badge variant="outline" class="ml-1.5 border text-xs font-medium uppercase text-muted-foreground" :title="
|
|
643
|
+
<Badge variant="outline" class="ml-1.5 border text-xs font-medium uppercase text-muted-foreground" :title="t('integration.editor.header.executionMode.title', { mode: modeLabel(workflow.execution_mode) })">
|
|
501
644
|
{{ modeLabel(workflow.execution_mode) }}
|
|
502
645
|
</Badge>
|
|
503
646
|
</template>
|
|
504
647
|
</span>
|
|
505
648
|
</template>
|
|
506
649
|
<template #actions>
|
|
650
|
+
<!-- Folding the rail away, and getting it back (PO-273). It reads as
|
|
651
|
+
chrome rather than as an action on this workflow, so the natural
|
|
652
|
+
place was ahead of the breadcrumb — but the shell's `#title` slot
|
|
653
|
+
*is* the `<h1>`'s content, and a control inside the heading makes
|
|
654
|
+
the page announce itself as "Hide navigation Workflows › Order
|
|
655
|
+
sync". `accessibility.md` AC-4 promises one heading that names what
|
|
656
|
+
the operator is looking at, so this sits in `#actions`, which is
|
|
657
|
+
outside it. Leftmost there is as near the rail as the shared header
|
|
658
|
+
allows without a slot of its own.
|
|
659
|
+
|
|
660
|
+
It carries its words and not just a pictogram: an icon-only control
|
|
661
|
+
here is one nobody finds, and the way back out of a folded rail is
|
|
662
|
+
the one control that cannot afford to be learnt by chance. Rendered
|
|
663
|
+
only where the canvas fills the page, the one surface it acts on. -->
|
|
664
|
+
<Button
|
|
665
|
+
v-if="canvasFillsPage"
|
|
666
|
+
variant="ghost"
|
|
667
|
+
size="sm"
|
|
668
|
+
class="h-7 shrink-0 gap-1.5 px-2 text-sm font-normal text-muted-foreground hover:text-primary"
|
|
669
|
+
:title="railFolded ? t('integration.nav.action.show.title') : t('integration.nav.action.hide.title')"
|
|
670
|
+
@click="toggleRail"
|
|
671
|
+
>
|
|
672
|
+
<RailIcon :size="15" />
|
|
673
|
+
{{ railFolded ? t("integration.nav.action.show") : t("integration.nav.action.hide") }}
|
|
674
|
+
</Button>
|
|
675
|
+
|
|
507
676
|
<!-- Run open: run-scoped actions replace the workflow Save. -->
|
|
508
677
|
<template v-if="openRunId">
|
|
509
678
|
<Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" @click="retryOpenRun">
|
|
510
679
|
<Loader2 v-if="retryBusy" :size="12" class="animate-spin" />
|
|
511
680
|
<RetryIcon v-else :size="12" />
|
|
512
|
-
|
|
681
|
+
{{ t("integration.editor.action.retry") }}
|
|
513
682
|
</Button>
|
|
514
683
|
<Button class="h-7 text-sm" @click="closeRun('editor')">
|
|
515
684
|
<ArrowLeft :size="12" />
|
|
516
|
-
|
|
685
|
+
{{ t("integration.editor.action.backToWorkflow") }}
|
|
517
686
|
</Button>
|
|
518
687
|
</template>
|
|
519
688
|
<template v-else>
|
|
@@ -523,30 +692,31 @@ function formatDateTime(iso) {
|
|
|
523
692
|
:title="buildNotice?.text"
|
|
524
693
|
>
|
|
525
694
|
<Loader2 :size="12" class="animate-spin" />
|
|
526
|
-
{{ workflow.buildStatus === "building" ? "
|
|
695
|
+
{{ workflow.buildStatus === "building" ? t("integration.editor.build.chip.building") : t("integration.editor.build.chip.queued") }}
|
|
527
696
|
</span>
|
|
528
|
-
<span v-if="isDirty" class="text-sm text-amber-600">
|
|
529
|
-
<Button variant="outline" class="h-7 text-sm" :disabled="saving" title="
|
|
697
|
+
<span v-if="isDirty" class="text-sm text-amber-600">{{ t("integration.editor.status.unsavedChanges") }}</span>
|
|
698
|
+
<Button variant="outline" class="h-7 text-sm" :disabled="saving" :title="t('integration.editor.action.duplicate.title')" @click="duplicateOpen = true">
|
|
530
699
|
<CopyIcon :size="12" />
|
|
531
|
-
|
|
700
|
+
{{ t("integration.editor.action.duplicate.label") }}
|
|
532
701
|
</Button>
|
|
533
|
-
<Button variant="outline" class="h-7 text-sm" :disabled="!isDirty || saving" title="
|
|
534
|
-
|
|
702
|
+
<Button variant="outline" class="h-7 text-sm" :disabled="!isDirty || saving" :title="t('integration.editor.action.discard.title')" @click="discardChanges">
|
|
703
|
+
{{ t("integration.editor.action.discard.label") }}
|
|
535
704
|
</Button>
|
|
536
705
|
<Button class="h-7 text-sm" :disabled="!isDirty || saving" @click="save">
|
|
537
706
|
<Loader2 v-if="saving" :size="12" class="animate-spin" />
|
|
538
707
|
<Save v-else :size="12" />
|
|
539
|
-
{{ saving ? "
|
|
708
|
+
{{ saving ? t("integration.editor.action.save.saving") : t("integration.editor.action.save.label") }}
|
|
540
709
|
</Button>
|
|
541
710
|
<Button
|
|
542
711
|
:variant="workflow.active ? 'outline' : 'default'"
|
|
543
712
|
class="h-7 text-sm"
|
|
544
713
|
:class="workflow.active ? 'text-amber-700 hover:text-amber-800' : 'bg-emerald-600 text-white hover:bg-emerald-700'"
|
|
545
|
-
:
|
|
546
|
-
|
|
714
|
+
:disabled="!workflow.active && isInvalid"
|
|
715
|
+
: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')"
|
|
716
|
+
@click="onToggleActive"
|
|
547
717
|
>
|
|
548
718
|
<component :is="workflow.active ? PauseIcon : Play" :size="12" />
|
|
549
|
-
{{ workflow.active ? "
|
|
719
|
+
{{ workflow.active ? t("integration.editor.action.pause.label") : t("integration.editor.action.activate.label") }}
|
|
550
720
|
</Button>
|
|
551
721
|
</template>
|
|
552
722
|
</template>
|
|
@@ -561,14 +731,58 @@ function formatDateTime(iso) {
|
|
|
561
731
|
</div>
|
|
562
732
|
|
|
563
733
|
<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">
|
|
564
|
-
{{
|
|
565
|
-
need{{ reauthCredentials.length === 1 ? "s" : "" }} reconnecting
|
|
734
|
+
{{ t("integration.editor.reauth.banner", reauthCredentials.length) }}
|
|
566
735
|
({{ reauthCredentials.map((c) => c.name).join(", ") }}) —
|
|
567
736
|
<NuxtLink to="/integrations/credentials" class="font-medium underline">
|
|
568
|
-
fix
|
|
737
|
+
{{ t("integration.editor.reauth.action.fix") }}
|
|
569
738
|
</NuxtLink>.
|
|
570
739
|
</div>
|
|
571
740
|
|
|
741
|
+
<!-- Manifest validation (PO-186): an inactive workflow with warnings can't
|
|
742
|
+
be activated. The banner names what's incomplete; the Activate button
|
|
743
|
+
stays disabled until the next save recomputes an empty warnings set.
|
|
744
|
+
Amber (not red) — the work is saved fine, it just isn't runnable yet.
|
|
745
|
+
One line per NODE, listing its fields (PO-209): the messages behind
|
|
746
|
+
them ride along as the line's title, and the line leads to the node. -->
|
|
747
|
+
<div
|
|
748
|
+
v-if="!openRunId && !workflow.active && isInvalid"
|
|
749
|
+
class="border-b border-amber-500/20 bg-amber-500/5 px-4 py-2 text-sm text-amber-800"
|
|
750
|
+
>
|
|
751
|
+
<p class="font-medium">
|
|
752
|
+
{{ t("integration.editor.validation.banner.title", { summary: warningSummary }) }}
|
|
753
|
+
</p>
|
|
754
|
+
<ul
|
|
755
|
+
class="mt-1 space-y-0.5"
|
|
756
|
+
:class="warningsExpanded ? 'max-h-36 overflow-y-auto pr-1' : ''"
|
|
757
|
+
>
|
|
758
|
+
<li
|
|
759
|
+
v-for="(g, i) in warningsExpanded ? warningGroups : warningGroups.slice(0, WARN_COLLAPSED)"
|
|
760
|
+
:key="`${g.nodeId ?? 'unattributed'}-${i}`"
|
|
761
|
+
class="flex gap-1.5"
|
|
762
|
+
>
|
|
763
|
+
<span class="select-none text-amber-500">•</span>
|
|
764
|
+
<!-- Attributed to a node: the whole line jumps there and opens its
|
|
765
|
+
settings. Otherwise there is nowhere to go — state the finding. -->
|
|
766
|
+
<button
|
|
767
|
+
v-if="g.nodeId"
|
|
768
|
+
type="button"
|
|
769
|
+
class="text-left underline decoration-amber-700/30 underline-offset-2 hover:decoration-amber-800"
|
|
770
|
+
:title="g.messages.join('\n')"
|
|
771
|
+
:aria-label="t('integration.editor.validation.node.ariaFix', g.fields.length, { label: g.label })"
|
|
772
|
+
@click="revealNode(g.nodeId)"
|
|
773
|
+
>
|
|
774
|
+
<span class="font-medium">{{ g.label }}</span> — {{ g.fields.join(", ") }}
|
|
775
|
+
</button>
|
|
776
|
+
<span v-else>{{ g.messages.join(" ") }}</span>
|
|
777
|
+
</li>
|
|
778
|
+
<li v-if="!warningsExpanded && warningGroups.length > WARN_COLLAPSED" class="pl-3">
|
|
779
|
+
<button type="button" class="text-amber-700/90 underline underline-offset-2 hover:text-amber-900" @click="warningsExpanded = true">
|
|
780
|
+
{{ t("integration.editor.validation.banner.more", { n: warningGroups.length - WARN_COLLAPSED }) }}
|
|
781
|
+
</button>
|
|
782
|
+
</li>
|
|
783
|
+
</ul>
|
|
784
|
+
</div>
|
|
785
|
+
|
|
572
786
|
<!-- Only the FAILED build state banners (its message matters); the
|
|
573
787
|
working states live as the header chip — and inside a run the
|
|
574
788
|
workflow's build state is noise, so nothing shows at all. -->
|
|
@@ -588,7 +802,7 @@ function formatDateTime(iso) {
|
|
|
588
802
|
{{ openRunError }}
|
|
589
803
|
</div>
|
|
590
804
|
<div v-else class="flex items-center gap-2 p-12 text-sm text-muted-foreground">
|
|
591
|
-
<Loader2 :size="16" class="animate-spin" />
|
|
805
|
+
<Loader2 :size="16" class="animate-spin" /> {{ t("integration.editor.run.loading") }}
|
|
592
806
|
</div>
|
|
593
807
|
</div>
|
|
594
808
|
</div>
|
|
@@ -598,16 +812,23 @@ function formatDateTime(iso) {
|
|
|
598
812
|
</template>
|
|
599
813
|
|
|
600
814
|
<!-- Editor tab — fills the remaining height inside the panel via
|
|
601
|
-
the flex wrapper in the studio layout.
|
|
815
|
+
the flex wrapper in the studio layout.
|
|
816
|
+
|
|
817
|
+
overlay-open: every lightbox this page owns renders as a sibling of
|
|
818
|
+
the tabs, so the editor stays mounted underneath one and would keep
|
|
819
|
+
its keyboard shortcuts live (Delete deleting the selected node behind
|
|
820
|
+
the dialog — PO-208). Add new page-level dialogs to this list. -->
|
|
602
821
|
<div v-else-if="tab === 'editor'" class="flex-1 min-h-0">
|
|
603
822
|
<WorkflowEditor
|
|
604
823
|
:key="editorResetKey"
|
|
824
|
+
ref="editorRef"
|
|
605
825
|
:workflow="workflow"
|
|
606
826
|
:run-busy="running || syncLoading || waitingForBuild"
|
|
607
827
|
:run-waiting="waitingForBuild"
|
|
828
|
+
:overlay-open="settingsOpen || duplicateOpen || syncOpen || runPayloadOpen || deactivationOpen"
|
|
608
829
|
@change="onChange"
|
|
609
830
|
@run="runFromEditor"
|
|
610
|
-
@toggle-active="
|
|
831
|
+
@toggle-active="onToggleActive"
|
|
611
832
|
@open-settings="settingsOpen = true"
|
|
612
833
|
/>
|
|
613
834
|
</div>
|
|
@@ -624,19 +845,19 @@ function formatDateTime(iso) {
|
|
|
624
845
|
:loading="runsLoading"
|
|
625
846
|
:pagination="false"
|
|
626
847
|
:show-workflow="false"
|
|
627
|
-
empty-text="
|
|
848
|
+
:empty-text="t('integration.editor.runs.empty')"
|
|
628
849
|
@changed="() => loadRuns()"
|
|
629
850
|
/>
|
|
630
851
|
|
|
631
852
|
<!-- Server pagination — the endpoint pages, so the whole history is reachable. -->
|
|
632
853
|
<div v-if="runsLastPage > 1 || runsTotal > wfRuns.length" class="flex items-center justify-between text-sm text-muted-foreground">
|
|
633
|
-
<span class="tabular-nums">
|
|
854
|
+
<span class="tabular-nums">{{ t("integration.editor.runs.pagination.page", { page: runsPage, lastPage: runsLastPage }) }} · {{ t("integration.editor.runs.pagination.count", runsTotal) }}</span>
|
|
634
855
|
<div class="flex gap-2">
|
|
635
856
|
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsPage <= 1 || runsLoading" @click="goRunsPage(runsPage - 1)">
|
|
636
|
-
|
|
857
|
+
{{ t("integration.editor.runs.pagination.newer") }}
|
|
637
858
|
</Button>
|
|
638
859
|
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsPage >= runsLastPage || runsLoading" @click="goRunsPage(runsPage + 1)">
|
|
639
|
-
|
|
860
|
+
{{ t("integration.editor.runs.pagination.older") }}
|
|
640
861
|
</Button>
|
|
641
862
|
</div>
|
|
642
863
|
</div>
|
|
@@ -654,7 +875,7 @@ function formatDateTime(iso) {
|
|
|
654
875
|
</template>
|
|
655
876
|
|
|
656
877
|
<div v-else class="p-12 text-center text-sm text-muted-foreground">
|
|
657
|
-
|
|
878
|
+
{{ t("integration.editor.state.loading") }}
|
|
658
879
|
</div>
|
|
659
880
|
</div>
|
|
660
881
|
|
|
@@ -662,31 +883,31 @@ function formatDateTime(iso) {
|
|
|
662
883
|
<Dialog v-model:open="runPayloadOpen">
|
|
663
884
|
<DialogContent class="sm:max-w-lg">
|
|
664
885
|
<DialogHeader>
|
|
665
|
-
<DialogTitle>
|
|
886
|
+
<DialogTitle>{{ t("integration.editor.runPayload.dialog.title") }}</DialogTitle>
|
|
666
887
|
<DialogDescription class="text-sm">
|
|
667
|
-
|
|
888
|
+
{{ t("integration.editor.runPayload.dialog.description") }}
|
|
668
889
|
</DialogDescription>
|
|
669
890
|
</DialogHeader>
|
|
670
891
|
<div v-if="eventTriggerTopic" class="flex flex-wrap items-center gap-1.5">
|
|
671
|
-
<span class="text-xs text-muted-foreground">
|
|
892
|
+
<span class="text-xs text-muted-foreground">{{ t("integration.editor.runPayload.prefill.label") }} <code class="font-mono">{{ eventTriggerTopic }}</code>:</span>
|
|
672
893
|
<Button
|
|
673
894
|
variant="outline"
|
|
674
895
|
class="h-7 text-xs"
|
|
675
896
|
:disabled="!catalogSample"
|
|
676
|
-
:title="catalogSample ? '
|
|
897
|
+
:title="catalogSample ? t('integration.editor.runPayload.action.sampleEvent.titleAvailable') : t('integration.editor.runPayload.action.sampleEvent.titleUnavailable')"
|
|
677
898
|
@click="insertSampleEvent"
|
|
678
899
|
>
|
|
679
|
-
|
|
900
|
+
{{ t("integration.editor.runPayload.action.sampleEvent") }}
|
|
680
901
|
</Button>
|
|
681
902
|
<Button
|
|
682
903
|
variant="outline"
|
|
683
904
|
class="h-7 text-xs"
|
|
684
905
|
:disabled="prefillBusy !== null"
|
|
685
|
-
title="
|
|
906
|
+
:title="t('integration.editor.runPayload.action.latestEvent.title')"
|
|
686
907
|
@click="insertLatestEvent"
|
|
687
908
|
>
|
|
688
909
|
<Loader2 v-if="prefillBusy === 'latest'" :size="12" class="animate-spin" />
|
|
689
|
-
|
|
910
|
+
{{ t("integration.editor.runPayload.action.latestEvent") }}
|
|
690
911
|
</Button>
|
|
691
912
|
</div>
|
|
692
913
|
<div class="space-y-1">
|
|
@@ -700,35 +921,35 @@ function formatDateTime(iso) {
|
|
|
700
921
|
<Button
|
|
701
922
|
variant="outline"
|
|
702
923
|
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
703
|
-
:title="buildFailed ? '
|
|
924
|
+
:title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
|
|
704
925
|
class="h-8 text-sm"
|
|
705
926
|
@click="runFromPayloadDialog('sync')"
|
|
706
927
|
>
|
|
707
928
|
<Loader2 v-if="syncLoading || waitingMode === 'sync'" :size="14" class="animate-spin" />
|
|
708
929
|
<Play v-else :size="14" />
|
|
709
|
-
{{ waitingMode === "sync" ? "
|
|
930
|
+
{{ waitingMode === "sync" ? t("integration.editor.run.waitingForBuild") : t("integration.editor.runPayload.action.run", { mode: t("integration.editor.mode.word.sync") }) }}
|
|
710
931
|
</Button>
|
|
711
932
|
<Button
|
|
712
933
|
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
713
|
-
:title="buildFailed ? '
|
|
934
|
+
:title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
|
|
714
935
|
class="h-8 text-sm"
|
|
715
936
|
@click="runFromPayloadDialog('async')"
|
|
716
937
|
>
|
|
717
938
|
<Loader2 v-if="running || waitingMode === 'async'" :size="14" class="animate-spin" />
|
|
718
939
|
<Play v-else :size="14" />
|
|
719
|
-
{{ waitingMode === "async" ? "
|
|
940
|
+
{{ waitingMode === "async" ? t("integration.editor.run.waitingForBuild") : t("integration.editor.runPayload.action.run", { mode: t("integration.editor.mode.word.async") }) }}
|
|
720
941
|
</Button>
|
|
721
942
|
</template>
|
|
722
943
|
<Button
|
|
723
944
|
v-else
|
|
724
945
|
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
725
|
-
:title="buildFailed ? '
|
|
946
|
+
:title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
|
|
726
947
|
class="h-8 text-sm"
|
|
727
948
|
@click="runFromPayloadDialog(primaryMode)"
|
|
728
949
|
>
|
|
729
950
|
<Loader2 v-if="running || syncLoading || waitingForBuild" :size="14" class="animate-spin" />
|
|
730
951
|
<Play v-else :size="14" />
|
|
731
|
-
{{ waitingForBuild ? "
|
|
952
|
+
{{ waitingForBuild ? t("integration.editor.run.waitingForBuild") : t("integration.editor.runPayload.action.run", { mode: t(primaryMode === "sync" ? "integration.editor.mode.word.sync" : "integration.editor.mode.word.async") }) }}
|
|
732
953
|
</Button>
|
|
733
954
|
</div>
|
|
734
955
|
</DialogContent>
|
|
@@ -759,5 +980,37 @@ function formatDateTime(iso) {
|
|
|
759
980
|
/>
|
|
760
981
|
|
|
761
982
|
<WorkflowDuplicateDialog v-model:open="duplicateOpen" :workflow="duplicateTarget" @duplicated="onDuplicated" />
|
|
983
|
+
|
|
984
|
+
<!-- Active-save blocked by manifest validation: keep the work by saving it
|
|
985
|
+
as paused. Cancelling persists nothing (the 422 already rolled back). -->
|
|
986
|
+
<AlertDialog :open="deactivationOpen" @update:open="(v) => {
|
|
987
|
+
if (!v) deactivationOpen = false;
|
|
988
|
+
}">
|
|
989
|
+
<AlertDialogContent>
|
|
990
|
+
<AlertDialogHeader>
|
|
991
|
+
<AlertDialogTitle>{{ t("integration.editor.deactivate.dialog.title") }}</AlertDialogTitle>
|
|
992
|
+
<AlertDialogDescription>
|
|
993
|
+
{{ t("integration.editor.deactivate.dialog.description.pre") }}
|
|
994
|
+
<span class="font-medium">{{ t("integration.editor.deactivate.dialog.description.paused") }}</span>
|
|
995
|
+
{{ t("integration.editor.deactivate.dialog.description.post") }}
|
|
996
|
+
</AlertDialogDescription>
|
|
997
|
+
</AlertDialogHeader>
|
|
998
|
+
<ul class="max-h-48 space-y-0.5 overflow-y-auto rounded-md border border-amber-500/20 bg-amber-500/5 px-3 py-2 text-sm text-amber-800">
|
|
999
|
+
<li v-for="(w, i) in deactivationIssues" :key="`${w.path}-${i}`" class="flex gap-1.5">
|
|
1000
|
+
<span class="select-none text-amber-500">•</span>
|
|
1001
|
+
<span><span v-if="w.where" class="font-medium">{{ w.where }}:</span> {{ w.message }}</span>
|
|
1002
|
+
</li>
|
|
1003
|
+
</ul>
|
|
1004
|
+
<AlertDialogFooter>
|
|
1005
|
+
<Button variant="outline" :disabled="saving" @click="deactivationOpen = false">
|
|
1006
|
+
{{ t("integration.editor.deactivate.dialog.cancel") }}
|
|
1007
|
+
</Button>
|
|
1008
|
+
<Button class="bg-emerald-600 text-white hover:bg-emerald-700" :disabled="saving" @click="confirmDeactivate">
|
|
1009
|
+
<Loader2 v-if="saving" :size="12" class="animate-spin" />
|
|
1010
|
+
{{ t("integration.editor.deactivate.dialog.confirm") }}
|
|
1011
|
+
</Button>
|
|
1012
|
+
</AlertDialogFooter>
|
|
1013
|
+
</AlertDialogContent>
|
|
1014
|
+
</AlertDialog>
|
|
762
1015
|
</IntegrationsLayout>
|
|
763
1016
|
</template>
|