@revenexx/studio-integrations 0.1.4 → 0.3.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.
Files changed (188) hide show
  1. package/README.md +31 -0
  2. package/dist/module.d.mts +2 -2
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.d.vue.ts +13 -0
  5. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +279 -0
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue.d.ts +13 -0
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.d.vue.ts +20 -0
  8. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +342 -0
  9. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue.d.ts +20 -0
  10. package/dist/runtime/components/Integrations/CredentialLogo.d.vue.ts +14 -0
  11. package/dist/runtime/components/Integrations/CredentialLogo.vue +37 -0
  12. package/dist/runtime/components/Integrations/CredentialLogo.vue.d.ts +14 -0
  13. package/dist/runtime/components/Integrations/CredentialRevenexxSource.d.vue.ts +12 -0
  14. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +364 -0
  15. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue.d.ts +12 -0
  16. package/dist/runtime/components/Integrations/CredentialTypePicker.d.vue.ts +15 -0
  17. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +71 -0
  18. package/dist/runtime/components/Integrations/CredentialTypePicker.vue.d.ts +15 -0
  19. package/dist/runtime/components/Integrations/DeadLettersPanel.d.vue.ts +5 -0
  20. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +151 -0
  21. package/dist/runtime/components/Integrations/DeadLettersPanel.vue.d.ts +5 -0
  22. package/dist/runtime/components/Integrations/EditorTip.d.vue.ts +17 -0
  23. package/dist/runtime/components/Integrations/EditorTip.vue +46 -0
  24. package/dist/runtime/components/Integrations/EditorTip.vue.d.ts +17 -0
  25. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +9 -0
  26. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +64 -0
  27. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +9 -0
  28. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +19 -0
  29. package/dist/runtime/components/Integrations/ExpressionInput.vue +236 -0
  30. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +19 -0
  31. package/dist/runtime/components/Integrations/GroupFrameView.d.vue.ts +28 -0
  32. package/dist/runtime/components/Integrations/GroupFrameView.vue +222 -0
  33. package/dist/runtime/components/Integrations/GroupFrameView.vue.d.ts +28 -0
  34. package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +14 -0
  35. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +184 -0
  36. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +14 -0
  37. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.d.vue.ts +16 -0
  38. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +102 -0
  39. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue.d.ts +16 -0
  40. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +23 -0
  41. package/dist/runtime/components/Integrations/NodeConfigField.vue +587 -0
  42. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +23 -0
  43. package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.d.vue.ts +14 -0
  44. package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.vue +80 -0
  45. package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.vue.d.ts +14 -0
  46. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +22 -3
  47. package/dist/runtime/components/Integrations/NodeInspector.vue +1074 -294
  48. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +22 -3
  49. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +9 -0
  50. package/dist/runtime/components/Integrations/NodeLogo.vue +38 -0
  51. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +9 -0
  52. package/dist/runtime/components/Integrations/NodePalette.d.vue.ts +22 -1
  53. package/dist/runtime/components/Integrations/NodePalette.vue +411 -90
  54. package/dist/runtime/components/Integrations/NodePalette.vue.d.ts +22 -1
  55. package/dist/runtime/components/Integrations/QueryBuilder.d.vue.ts +9 -0
  56. package/dist/runtime/components/Integrations/QueryBuilder.vue +239 -0
  57. package/dist/runtime/components/Integrations/QueryBuilder.vue.d.ts +9 -0
  58. package/dist/runtime/components/Integrations/{RunDetailDialog.d.vue.ts → RunActionsMenu.d.vue.ts} +3 -4
  59. package/dist/runtime/components/Integrations/RunActionsMenu.vue +86 -0
  60. package/dist/runtime/components/Integrations/{RunDetailDialog.vue.d.ts → RunActionsMenu.vue.d.ts} +3 -4
  61. package/dist/runtime/components/Integrations/RunDetailPanel.d.vue.ts +14 -0
  62. package/dist/runtime/components/Integrations/RunDetailPanel.vue +777 -0
  63. package/dist/runtime/components/Integrations/RunDetailPanel.vue.d.ts +14 -0
  64. package/dist/runtime/components/Integrations/RunPayloadInput.d.vue.ts +13 -0
  65. package/dist/runtime/components/Integrations/RunPayloadInput.vue +175 -0
  66. package/dist/runtime/components/Integrations/RunPayloadInput.vue.d.ts +13 -0
  67. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +27 -0
  68. package/dist/runtime/components/Integrations/RunResponseModal.vue +84 -0
  69. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +27 -0
  70. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +21 -0
  71. package/dist/runtime/components/Integrations/RunsTable.vue +114 -0
  72. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +21 -0
  73. package/dist/runtime/components/Integrations/SftpAccountImportDialog.d.vue.ts +11 -0
  74. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +91 -0
  75. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue.d.ts +11 -0
  76. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +28 -0
  77. package/dist/runtime/components/Integrations/StickyNoteView.vue +208 -0
  78. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +28 -0
  79. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +29 -0
  80. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +201 -0
  81. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +29 -0
  82. package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +32 -0
  83. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +138 -0
  84. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +32 -0
  85. package/dist/runtime/components/Integrations/WorkflowCreateDialog.d.vue.ts +9 -0
  86. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +322 -0
  87. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue.d.ts +9 -0
  88. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.d.vue.ts +18 -0
  89. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +63 -0
  90. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue.d.ts +18 -0
  91. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +12 -1
  92. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +68 -6
  93. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +12 -1
  94. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +8 -1
  95. package/dist/runtime/components/Integrations/WorkflowEditor.vue +1424 -144
  96. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +8 -1
  97. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +12 -0
  98. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +417 -0
  99. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +12 -0
  100. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +10 -0
  101. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +89 -0
  102. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +10 -0
  103. package/dist/runtime/components/Integrations/WorkflowIconView.d.vue.ts +10 -0
  104. package/dist/runtime/components/Integrations/WorkflowIconView.vue +35 -0
  105. package/dist/runtime/components/Integrations/WorkflowIconView.vue.d.ts +10 -0
  106. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +5 -1
  107. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +114 -84
  108. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +5 -1
  109. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +12 -0
  110. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +654 -0
  111. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +12 -0
  112. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +36 -0
  113. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +136 -0
  114. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +36 -0
  115. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +14 -0
  116. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +63 -0
  117. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +14 -0
  118. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +35 -15
  119. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.d.vue.ts +13 -0
  120. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +265 -0
  121. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue.d.ts +13 -0
  122. package/dist/runtime/components/Integrations/WorkflowsTable.d.vue.ts +4 -1
  123. package/dist/runtime/components/Integrations/WorkflowsTable.vue +122 -39
  124. package/dist/runtime/components/Integrations/WorkflowsTable.vue.d.ts +4 -1
  125. package/dist/runtime/components/Integrations/workflowLayout.d.ts +5 -0
  126. package/dist/runtime/components/Integrations/workflowLayout.js +5 -3
  127. package/dist/runtime/composables/integrationsApi.d.ts +6 -7
  128. package/dist/runtime/composables/integrationsApi.js +6 -12
  129. package/dist/runtime/composables/useCanvasShortcuts.d.ts +37 -0
  130. package/dist/runtime/composables/useCanvasShortcuts.js +62 -0
  131. package/dist/runtime/composables/useCredentialTypes.js +2 -1
  132. package/dist/runtime/composables/useIntegrationsAudit.js +2 -1
  133. package/dist/runtime/composables/useIntegrationsCredentials.js +2 -1
  134. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +22 -0
  135. package/dist/runtime/composables/useIntegrationsDeadLetters.js +50 -0
  136. package/dist/runtime/composables/useIntegrationsNodes.d.ts +4 -0
  137. package/dist/runtime/composables/useIntegrationsNodes.js +8 -4
  138. package/dist/runtime/composables/useIntegrationsRevisions.d.ts +40 -0
  139. package/dist/runtime/composables/useIntegrationsRevisions.js +52 -0
  140. package/dist/runtime/composables/useIntegrationsRuns.d.ts +21 -1
  141. package/dist/runtime/composables/useIntegrationsRuns.js +94 -5
  142. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -1
  143. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -0
  144. package/dist/runtime/composables/useIntegrationsTemplates.js +34 -5
  145. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -1
  146. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +1 -0
  147. package/dist/runtime/composables/useIntegrationsWorkflows.js +59 -2
  148. package/dist/runtime/composables/useMiniMapLayout.d.ts +34 -0
  149. package/dist/runtime/composables/useMiniMapLayout.js +119 -0
  150. package/dist/runtime/composables/useNodeConfigResolve.d.ts +43 -0
  151. package/dist/runtime/composables/useNodeConfigResolve.js +124 -0
  152. package/dist/runtime/composables/useNodeDrag.d.ts +22 -0
  153. package/dist/runtime/composables/useNodeDrag.js +10 -0
  154. package/dist/runtime/composables/usePaletteLayout.d.ts +41 -0
  155. package/dist/runtime/composables/usePaletteLayout.js +113 -0
  156. package/dist/runtime/data/expressionEditor.d.ts +22 -0
  157. package/dist/runtime/data/expressionEditor.js +40 -0
  158. package/dist/runtime/data/integrationNodeCatalog.js +8 -8
  159. package/dist/runtime/data/nodeInputData.d.ts +37 -0
  160. package/dist/runtime/data/nodeInputData.js +62 -0
  161. package/dist/runtime/data/nodeRunData.d.ts +56 -0
  162. package/dist/runtime/data/nodeRunData.js +91 -0
  163. package/dist/runtime/data/payloadForm.d.ts +44 -0
  164. package/dist/runtime/data/payloadForm.js +126 -0
  165. package/dist/runtime/data/workflowIconCatalog.d.ts +34 -0
  166. package/dist/runtime/data/workflowIconCatalog.js +116 -0
  167. package/dist/runtime/pages/integrations/audit.vue +25 -35
  168. package/dist/runtime/pages/integrations/credentials.vue +117 -122
  169. package/dist/runtime/pages/integrations/index.vue +133 -36
  170. package/dist/runtime/pages/integrations/runs.vue +125 -75
  171. package/dist/runtime/pages/integrations/schedules.vue +5 -16
  172. package/dist/runtime/pages/integrations/secrets.vue +1 -3
  173. package/dist/runtime/pages/integrations/workflows/[id].vue +599 -194
  174. package/dist/runtime/pages/integrations/workflows/index.vue +10 -8
  175. package/dist/runtime/types/integrations.d.ts +190 -4
  176. package/dist/runtime/utils/apiError.d.ts +17 -0
  177. package/dist/runtime/utils/apiError.js +8 -0
  178. package/dist/runtime/utils/credentialState.d.ts +28 -0
  179. package/dist/runtime/utils/credentialState.js +29 -0
  180. package/dist/runtime/utils/localize.d.ts +3 -1
  181. package/dist/runtime/utils/localize.js +1 -2
  182. package/dist/runtime/utils/statusStyle.d.ts +6 -0
  183. package/dist/runtime/utils/statusStyle.js +15 -0
  184. package/package.json +20 -3
  185. package/dist/runtime/components/Integrations/RunDetailDialog.vue +0 -169
  186. package/dist/runtime/pages/integrations/workflows/new.d.vue.ts +0 -2
  187. package/dist/runtime/pages/integrations/workflows/new.vue +0 -153
  188. package/dist/runtime/pages/integrations/workflows/new.vue.d.ts +0 -2
@@ -1,12 +1,36 @@
1
1
  <script setup>
2
- import { computed, onMounted, ref, watch } from "vue";
3
- import { ArrowLeft, ClockCircle as Clock, History as HistoryIcon, RefreshCircle as Loader2, SidebarMinimalistic as PanelLeftOpen, Pause, Play, Diskette as Save, LinkRound as Webhook } from "#solar-icons";
4
- import { Button } from "@revenexx/studio";
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";
4
+ import {
5
+ Badge,
6
+ Button,
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ Textarea
18
+ } from "@revenexx/studio";
5
19
  import WorkflowEditor from "../../../components/Integrations/WorkflowEditor.vue";
20
+ import WorkflowHistoryPanel from "../../../components/Integrations/WorkflowHistoryPanel.vue";
21
+ import WorkflowRunReplayPanel from "../../../components/Integrations/WorkflowRunReplayPanel.vue";
22
+ import WorkflowSettingsDialog from "../../../components/Integrations/WorkflowSettingsDialog.vue";
23
+ import WorkflowDuplicateDialog from "../../../components/Integrations/WorkflowDuplicateDialog.vue";
24
+ import RunsTable from "../../../components/Integrations/RunsTable.vue";
25
+ import RunDetailPanel from "../../../components/Integrations/RunDetailPanel.vue";
26
+ import { toast } from "vue-sonner";
27
+ import { IntegrationsApiError } from "../../../utils/apiError";
28
+ import { payloadFieldsFromBlob } from "../../../data/payloadForm";
29
+ import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
6
30
  const route = useRoute();
7
31
  const router = useRouter();
8
32
  const { workflows, get, update, toggleActive } = useIntegrationsWorkflows();
9
- const { focused, toggle: toggleFocus, autoEnable } = useIntegrationsFocus();
33
+ const { autoEnable } = useIntegrationsFocus();
10
34
  autoEnable();
11
35
  const id = computed(() => Number(route.params.id));
12
36
  const workflow = computed(() => workflows.value.find((w) => w.id === id.value) ?? null);
@@ -14,11 +38,9 @@ useHead({ title: () => `${workflow.value?.name ?? "Workflow"} \xB7 Revenexx Cock
14
38
  const tab = ref("editor");
15
39
  const tabs = [
16
40
  { key: "editor", label: "Editor" },
41
+ { key: "history", label: "History" },
17
42
  { key: "runs", label: "Runs" },
18
- { key: "triggers", label: "Triggers" },
19
- { key: "variables", label: "Variables" },
20
- { key: "settings", label: "Settings" },
21
- { key: "history", label: "History" }
43
+ { key: "dead-letters", label: "Missed runs" }
22
44
  ];
23
45
  const dirtyBlob = ref(null);
24
46
  const isDirty = computed(() => dirtyBlob.value !== null);
@@ -32,50 +54,360 @@ const reauthCredentials = computed(() => {
32
54
  const blob = JSON.stringify(dirtyBlob.value ?? workflow.value?.blob ?? {});
33
55
  return needsReauth.value.filter((c) => blob.includes(c.id));
34
56
  });
57
+ const buildBlocked = computed(() => {
58
+ const s = workflow.value?.buildStatus;
59
+ return s === "pending" || s === "building" || s === "failed";
60
+ });
61
+ const buildWorking = computed(() => workflow.value?.buildStatus === "building" || workflow.value?.buildStatus === "pending");
62
+ const buildNotice = computed(() => {
63
+ switch (workflow.value?.buildStatus) {
64
+ case "building":
65
+ return { tone: "info", text: "The workflow bundle is still building \u2014 runs are unavailable until it is ready." };
66
+ case "pending":
67
+ return { tone: "info", text: "The workflow bundle build is queued \u2014 runs are unavailable until it is ready." };
68
+ case "failed":
69
+ return {
70
+ tone: "error",
71
+ text: `The workflow bundle failed to build${workflow.value?.buildError ? `: ${workflow.value.buildError}` : "."} Save the workflow again to retry the build.`
72
+ };
73
+ default:
74
+ return null;
75
+ }
76
+ });
35
77
  watch(id, async () => {
36
78
  const w = await get(id.value);
37
79
  if (!w) await router.push("/integrations/workflows");
38
80
  dirtyBlob.value = null;
81
+ if (tab.value === "runs") void loadRuns(1);
39
82
  }, { immediate: true });
40
83
  function onChange(blob) {
41
84
  dirtyBlob.value = blob;
42
85
  }
43
- const togglingTrigger = ref(null);
44
- async function toggleTrigger(trg) {
86
+ const editorResetKey = ref(0);
87
+ function discardChanges() {
88
+ dirtyBlob.value = null;
89
+ editorResetKey.value++;
90
+ }
91
+ const duplicateOpen = ref(false);
92
+ const duplicateTarget = computed(() => workflow.value ? { id: workflow.value.id, name: workflow.value.name } : null);
93
+ async function onDuplicated(copy) {
94
+ toast.success(`Duplicated as \u201C${copy.name}\u201D`);
95
+ await router.push(`/integrations/workflows/${copy.id}`);
96
+ }
97
+ async function onRevisionRestored() {
98
+ if (!workflow.value) return;
99
+ await get(workflow.value.id);
100
+ dirtyBlob.value = null;
101
+ }
102
+ const { trigger: triggerRun, loadForWorkflow, fetchRun, retryRun } = useIntegrationsRuns();
103
+ const running = ref(false);
104
+ const payloadText = ref("{}");
105
+ const payloadError = ref(null);
106
+ const primaryMode = computed(() => workflow.value?.execution_mode === "sync_only" ? "sync" : "async");
107
+ const syncOpen = ref(false);
108
+ const syncLoading = ref(false);
109
+ const syncResponse = ref(null);
110
+ const syncError = ref(null);
111
+ const runModalMode = ref("sync");
112
+ const runModalRunId = ref(null);
113
+ function extractRunId(res) {
114
+ if (res && typeof res === "object" && "id" in res) {
115
+ const id2 = res.id;
116
+ if (id2 !== null && id2 !== void 0 && id2 !== "") return String(id2);
117
+ }
118
+ return null;
119
+ }
120
+ function viewRunFromModal(id2) {
121
+ syncOpen.value = false;
122
+ void router.push({ query: { ...route.query, run: id2 } });
123
+ }
124
+ function syncErrorMessage(e) {
125
+ if (e instanceof IntegrationsApiError) {
126
+ if (e.status === 504) return "Sync run timed out";
127
+ if (e.status === 422 && e.body?.status === "failed") return "Sync run failed";
128
+ return e.message || "Sync run failed";
129
+ }
130
+ return e?.message || "Sync run failed";
131
+ }
132
+ async function run(mode) {
45
133
  if (!workflow.value) return;
46
- togglingTrigger.value = trg.id;
134
+ let payload;
135
+ try {
136
+ payload = JSON.parse(payloadText.value || "{}");
137
+ } catch {
138
+ payloadError.value = "Trigger payload is not valid JSON.";
139
+ return;
140
+ }
141
+ payloadError.value = null;
142
+ runModalMode.value = mode;
143
+ runModalRunId.value = null;
144
+ syncOpen.value = true;
145
+ syncLoading.value = true;
146
+ syncResponse.value = null;
147
+ syncError.value = null;
148
+ if (mode === "sync") {
149
+ try {
150
+ syncResponse.value = await triggerRun(workflow.value.id, payload, "sync");
151
+ runModalRunId.value = extractRunId(syncResponse.value);
152
+ } catch (e) {
153
+ syncError.value = syncErrorMessage(e);
154
+ if (e instanceof IntegrationsApiError) {
155
+ runModalRunId.value = extractRunId(e.body?.run);
156
+ }
157
+ } finally {
158
+ syncLoading.value = false;
159
+ void loadRuns(1);
160
+ }
161
+ return;
162
+ }
163
+ running.value = true;
164
+ try {
165
+ const res = await triggerRun(workflow.value.id, payload, "async");
166
+ runModalRunId.value = extractRunId(res);
167
+ } catch (e) {
168
+ syncError.value = e.message || "Could not start the run.";
169
+ } finally {
170
+ running.value = false;
171
+ syncLoading.value = false;
172
+ void loadRuns(1);
173
+ }
174
+ }
175
+ const payloadDriven = computed(() => payloadFieldsFromBlob(dirtyBlob.value ?? workflow.value?.blob ?? { nodes: [] }).length > 0);
176
+ async function runFromEditor() {
177
+ if (!workflow.value) return;
178
+ if (payloadDriven.value) {
179
+ runPayloadOpen.value = true;
180
+ return;
181
+ }
182
+ await run(primaryMode.value);
183
+ }
184
+ const runPayloadOpen = ref(false);
185
+ const definedEvents = useDefinedEvents();
186
+ const eventLog = useIntegrationsEvents();
187
+ const prefillBusy = ref(null);
188
+ const eventTriggerTopic = computed(() => {
189
+ const trigger = (workflow.value?.blob.triggers ?? []).find((t) => t.type === "event");
190
+ const topic = trigger?.config?.event_type;
191
+ return typeof topic === "string" && topic.trim() !== "" ? topic.trim() : null;
192
+ });
193
+ watch(runPayloadOpen, (open) => {
194
+ if (open && eventTriggerTopic.value) void definedEvents.load();
195
+ });
196
+ const catalogSample = computed(() => {
197
+ if (!eventTriggerTopic.value) return null;
198
+ return definedEvents.emits.value.find((e) => e.topic === eventTriggerTopic.value)?.sample ?? null;
199
+ });
200
+ function envelope(topic, data, extra = {}) {
201
+ return JSON.stringify({
202
+ topic,
203
+ id: extra.id ?? "evt_sample00000000000000000000",
204
+ tenant_id: extra.tenant_id ?? "",
205
+ data,
206
+ metadata: extra.metadata ?? {},
207
+ time: extra.time ?? (/* @__PURE__ */ new Date()).toISOString()
208
+ }, null, 2);
209
+ }
210
+ function insertSampleEvent() {
211
+ const topic = eventTriggerTopic.value;
212
+ if (!topic || !catalogSample.value) return;
213
+ payloadText.value = envelope(topic, catalogSample.value);
214
+ payloadError.value = null;
215
+ }
216
+ async function insertLatestEvent() {
217
+ const topic = eventTriggerTopic.value;
218
+ if (!topic || prefillBusy.value) return;
219
+ prefillBusy.value = "latest";
220
+ try {
221
+ await eventLog.loadEvents();
222
+ const match = eventLog.events.value.filter((e) => e.topic === topic).sort((a, b) => (b.time ?? "").localeCompare(a.time ?? ""))[0];
223
+ if (!match) {
224
+ toast.info(`No received ${topic} event in the log yet.`);
225
+ return;
226
+ }
227
+ payloadText.value = envelope(topic, match.data, {
228
+ id: match.id,
229
+ tenant_id: match.tenantId ?? "",
230
+ metadata: match.metadata,
231
+ time: match.time
232
+ });
233
+ payloadError.value = null;
234
+ } finally {
235
+ prefillBusy.value = null;
236
+ }
237
+ }
238
+ async function runFromPayloadDialog(mode) {
239
+ await run(mode);
240
+ if (payloadError.value) return;
241
+ runPayloadOpen.value = false;
242
+ }
243
+ const RUNS_PER_PAGE = 25;
244
+ const wfRuns = ref([]);
245
+ const runsLoading = ref(false);
246
+ const runsError = ref(null);
247
+ const runsPage = ref(1);
248
+ const runsTotal = ref(0);
249
+ const runsLastPage = ref(1);
250
+ let runsPoll = null;
251
+ function stopRunsPoll() {
252
+ if (runsPoll) {
253
+ clearInterval(runsPoll);
254
+ runsPoll = null;
255
+ }
256
+ }
257
+ function startRunsPoll() {
258
+ stopRunsPoll();
259
+ runsPoll = setInterval(() => {
260
+ if (runsPage.value === 1) void loadRuns(1, { silent: true });
261
+ }, 3e3);
262
+ }
263
+ onUnmounted(stopRunsPoll);
264
+ async function loadRuns(page = runsPage.value, opts = {}) {
265
+ if (!workflow.value) return;
266
+ if (!opts.silent) runsLoading.value = true;
267
+ runsError.value = null;
268
+ try {
269
+ const res = await loadForWorkflow(workflow.value.id, workflow.value.name, {
270
+ reconcile: page === 1,
271
+ page,
272
+ perPage: RUNS_PER_PAGE
273
+ });
274
+ wfRuns.value = res.runs;
275
+ runsTotal.value = res.total;
276
+ runsLastPage.value = res.lastPage;
277
+ runsPage.value = res.page;
278
+ } catch (e) {
279
+ runsError.value = e.message || "Could not load runs.";
280
+ } finally {
281
+ if (!opts.silent) runsLoading.value = false;
282
+ }
283
+ }
284
+ function goRunsPage(p) {
285
+ const next = Math.min(Math.max(1, p), runsLastPage.value);
286
+ if (next !== runsPage.value) void loadRuns(next);
287
+ }
288
+ watch(tab, (t) => {
289
+ if (t === "runs") {
290
+ void loadRuns(1);
291
+ startRunsPoll();
292
+ } else stopRunsPoll();
293
+ });
294
+ const settingsOpen = ref(false);
295
+ const openRunId = computed(() => route.query.run != null ? String(route.query.run) : null);
296
+ const runView = ref("details");
297
+ const runViews = [
298
+ { key: "details", label: "Details" },
299
+ { key: "timeline", label: "Timeline" },
300
+ { key: "replay", label: "Replay" }
301
+ ];
302
+ const openRun = ref(null);
303
+ const openRunError = ref(null);
304
+ async function loadOpenRun() {
305
+ if (!openRunId.value || !workflow.value) return;
306
+ openRunError.value = null;
307
+ try {
308
+ openRun.value = await fetchRun(workflow.value.id, openRunId.value, workflow.value.name);
309
+ } catch (e) {
310
+ openRunError.value = e?.message ?? "Could not load the run.";
311
+ }
312
+ }
313
+ watch(openRunId, () => {
314
+ runView.value = route.query.view === "replay" ? "replay" : "details";
315
+ }, { immediate: true });
316
+ watch([openRunId, () => workflow.value?.id], () => {
317
+ openRun.value = null;
318
+ openRunError.value = null;
319
+ if (openRunId.value && workflow.value) void loadOpenRun();
320
+ }, { immediate: true });
321
+ function closeRun(target) {
322
+ tab.value = target;
323
+ void router.replace({ query: { ...route.query, run: void 0, view: void 0 } });
324
+ }
325
+ const canRetryOpenRun = computed(() => !!openRun.value && ["succeeded", "failed", "cancelled"].includes(openRun.value.status));
326
+ const retryBusy = ref(false);
327
+ async function retryOpenRun() {
328
+ if (!openRun.value || retryBusy.value) return;
329
+ retryBusy.value = true;
330
+ try {
331
+ const res = await retryRun(openRun.value.workflowId, openRun.value.id);
332
+ const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
333
+ toast.success(newId && newId !== openRun.value.id ? `Started run #${newId}` : "Retry requested");
334
+ void loadRuns(1, { silent: true });
335
+ } catch (e) {
336
+ toast.error(e?.message ?? "Could not retry the run.");
337
+ } finally {
338
+ retryBusy.value = false;
339
+ }
340
+ }
341
+ const EXECUTION_MODES = [
342
+ { value: "async_only", label: "Async only", hint: "Runs are always queued; the trigger returns immediately." },
343
+ { value: "sync_only", label: "Sync only", hint: "Runs block until they finish and return the result inline." },
344
+ { value: "caller_decides", label: "Caller decides", hint: "The caller chooses sync or async per run." }
345
+ ];
346
+ const executionModeDraft = ref("async_only");
347
+ watch(workflow, (w) => {
348
+ if (w) executionModeDraft.value = w.execution_mode ?? "async_only";
349
+ }, { immediate: true });
350
+ const savingMode = ref(false);
351
+ function modeLabel(m) {
352
+ return m === "sync_only" ? "Sync" : m === "caller_decides" ? "Caller decides" : "Async";
353
+ }
354
+ async function saveExecutionMode(mode) {
355
+ if (!workflow.value || mode === workflow.value.execution_mode) return;
356
+ const prev = workflow.value.execution_mode ?? "async_only";
357
+ executionModeDraft.value = mode;
358
+ savingMode.value = true;
47
359
  saveError.value = null;
48
- trg.active = !trg.active;
49
360
  try {
50
361
  await update(workflow.value.id, {
51
362
  name: workflow.value.name,
52
363
  description: workflow.value.description,
53
364
  blob: workflow.value.blob,
54
365
  active: workflow.value.active,
55
- executionMode: workflow.value.execution_mode
366
+ executionMode: mode
56
367
  });
368
+ await get(workflow.value.id);
57
369
  } catch (e) {
58
- trg.active = !trg.active;
59
- saveError.value = e.message || "Could not update the trigger.";
370
+ executionModeDraft.value = prev;
371
+ saveError.value = e.message || "Could not update the execution mode.";
60
372
  } finally {
61
- togglingTrigger.value = null;
373
+ savingMode.value = false;
62
374
  }
63
375
  }
64
- const { trigger: triggerRun } = useIntegrationsRuns();
65
- const running = ref(false);
66
- const runResult = ref(null);
67
- const runError = ref(null);
68
- async function runNow() {
376
+ const savingMeta = ref(false);
377
+ const tagSuggestions = computed(() => {
378
+ const all = /* @__PURE__ */ new Set();
379
+ for (const wf of workflows.value) for (const t of wf.blob.tags ?? []) all.add(t);
380
+ return [...all].sort();
381
+ });
382
+ async function saveWorkflowMeta(meta) {
69
383
  if (!workflow.value) return;
70
- running.value = true;
71
- runError.value = null;
72
- runResult.value = null;
384
+ savingMeta.value = true;
385
+ saveError.value = null;
73
386
  try {
74
- runResult.value = await triggerRun(workflow.value.id, {});
387
+ const blob = JSON.parse(JSON.stringify(dirtyBlob.value ?? workflow.value.blob));
388
+ blob.name = meta.name;
389
+ if (meta.description) blob.description = meta.description;
390
+ else delete blob.description;
391
+ if (meta.tags.length) blob.tags = [...meta.tags];
392
+ else delete blob.tags;
393
+ if (meta.icon) blob.icon = meta.icon;
394
+ else delete blob.icon;
395
+ await update(workflow.value.id, {
396
+ name: meta.name,
397
+ description: meta.description || null,
398
+ blob,
399
+ active: workflow.value.active,
400
+ executionMode: workflow.value.execution_mode
401
+ });
402
+ await get(workflow.value.id);
403
+ dirtyBlob.value = null;
404
+ toast.success("Appearance saved");
75
405
  } catch (e) {
76
- runError.value = e.message || "Could not start the run.";
406
+ const msg = e.message || "Could not save the appearance.";
407
+ saveError.value = msg;
408
+ toast.error(msg);
77
409
  } finally {
78
- running.value = false;
410
+ savingMeta.value = false;
79
411
  }
80
412
  }
81
413
  async function save() {
@@ -113,40 +445,85 @@ function formatDateTime(iso) {
113
445
  remaining height below the page header. -->
114
446
  <div class="flex h-full flex-col">
115
447
  <template v-if="workflow">
116
- <PageHeader
117
- :title="workflow.name"
118
- >
448
+ <PageHeader :title="workflow.name">
449
+ <template #title>
450
+ <span class="inline-flex min-w-0 items-center gap-1.5">
451
+ <NuxtLink to="/integrations/workflows" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary">Workflows</NuxtLink>
452
+ <Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
453
+ <!-- Run open: the breadcrumb extends — name and Runs become crumbs
454
+ back into the workflow surface. -->
455
+ <template v-if="openRunId">
456
+ <button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('editor')">{{ workflow.name }}</button>
457
+ <Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
458
+ <button type="button" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('runs')">Runs</button>
459
+ <Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
460
+ <!-- The Temporal run id IS the run's identity; the internal
461
+ number only fills in until the run is handed off. -->
462
+ <span class="truncate font-mono">{{ openRun?.temporalRunId ?? `Run #${openRunId}` }}</span>
463
+ </template>
464
+ <template v-else>
465
+ <span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
466
+ <WorkflowIconView :icon="workflow.blob.icon" :size="16" />
467
+ </span>
468
+ <span class="truncate">{{ workflow.name }}</span>
469
+ <Badge variant="outline" class="ml-1.5 border text-xs font-medium uppercase text-muted-foreground" :title="`Execution mode: ${modeLabel(workflow.execution_mode)}`">
470
+ {{ modeLabel(workflow.execution_mode) }}
471
+ </Badge>
472
+ </template>
473
+ </span>
474
+ </template>
119
475
  <template #actions>
120
- <span v-if="isDirty" class="text-sm text-amber-600">Unsaved changes</span>
121
- <Button
122
- variant="ghost"
123
- size="icon"
124
- class="size-7"
125
- :title="focused ? 'Show studio sidebar' : 'Hide studio sidebar'"
126
- @click="toggleFocus"
127
- >
128
- <PanelLeftOpen :size="14" :class="focused ? '' : 'rotate-180'" />
129
- </Button>
130
- <Button variant="outline" class="h-7 text-sm" @click="toggleActive(workflow.id)">
131
- <component :is="workflow.active ? Pause : Play" :size="12" />
132
- {{ workflow.active ? "Pause" : "Activate" }}
133
- </Button>
134
- <Button class="h-7 text-sm" :disabled="!isDirty || saving" @click="save">
135
- <Loader2 v-if="saving" :size="12" class="animate-spin" />
136
- <Save v-else :size="12" />
137
- {{ saving ? "Saving\u2026" : "Save" }}
138
- </Button>
139
- <NuxtLink to="/integrations/workflows">
140
- <Button variant="ghost" class="h-7 text-sm">
476
+ <!-- Run open: run-scoped actions replace the workflow Save. -->
477
+ <template v-if="openRunId">
478
+ <Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" @click="retryOpenRun">
479
+ <Loader2 v-if="retryBusy" :size="12" class="animate-spin" />
480
+ <RetryIcon v-else :size="12" />
481
+ Retry
482
+ </Button>
483
+ <Button class="h-7 text-sm" @click="closeRun('editor')">
141
484
  <ArrowLeft :size="12" />
142
- Workflows
485
+ Back to Workflow
143
486
  </Button>
144
- </NuxtLink>
487
+ </template>
488
+ <template v-else>
489
+ <span
490
+ v-if="buildWorking"
491
+ class="inline-flex h-7 items-center gap-1.5 rounded-md border border-sky-500/25 bg-sky-500/10 px-2.5 text-sm text-sky-700"
492
+ :title="buildNotice?.text"
493
+ >
494
+ <Loader2 :size="12" class="animate-spin" />
495
+ {{ workflow.buildStatus === "building" ? "Building bundle\u2026" : "Build queued\u2026" }}
496
+ </span>
497
+ <span v-if="isDirty" class="text-sm text-amber-600">Unsaved changes</span>
498
+ <Button variant="outline" class="h-7 text-sm" :disabled="saving" title="Duplicate this workflow" @click="duplicateOpen = true">
499
+ <CopyIcon :size="12" />
500
+ Duplicate
501
+ </Button>
502
+ <Button variant="outline" class="h-7 text-sm" :disabled="!isDirty || saving" title="Discard unsaved changes" @click="discardChanges">
503
+ Cancel
504
+ </Button>
505
+ <Button class="h-7 text-sm" :disabled="!isDirty || saving" @click="save">
506
+ <Loader2 v-if="saving" :size="12" class="animate-spin" />
507
+ <Save v-else :size="12" />
508
+ {{ saving ? "Saving\u2026" : "Save" }}
509
+ </Button>
510
+ <Button
511
+ :variant="workflow.active ? 'outline' : 'default'"
512
+ class="h-7 text-sm"
513
+ :class="workflow.active ? 'text-amber-700 hover:text-amber-800' : 'bg-emerald-600 text-white hover:bg-emerald-700'"
514
+ :title="workflow.active ? 'Pause this workflow' : 'Activate this workflow'"
515
+ @click="toggleActive(workflow.id)"
516
+ >
517
+ <component :is="workflow.active ? PauseIcon : Play" :size="12" />
518
+ {{ workflow.active ? "Pause" : "Activate" }}
519
+ </Button>
520
+ </template>
145
521
  </template>
146
522
 
147
523
  </PageHeader>
148
524
 
149
- <TabBar v-model="tab" :tabs="tabs" class="shrink-0 px-4" />
525
+ <TabBar v-if="!openRunId" v-model="tab" :tabs="tabs" class="shrink-0 px-4" />
526
+ <TabBar v-else v-model="runView" :tabs="runViews" class="shrink-0 px-4" />
150
527
 
151
528
  <div v-if="saveError" class="border-b border-rose-500/20 bg-rose-500/5 px-4 py-2 text-sm text-rose-700">
152
529
  {{ saveError }}
@@ -161,166 +538,194 @@ function formatDateTime(iso) {
161
538
  </NuxtLink>.
162
539
  </div>
163
540
 
164
- <!-- Editor tab — fills the remaining height inside the panel via
165
- the flex wrapper in the studio layout. -->
166
- <div v-if="tab === 'editor'" class="flex-1 min-h-0">
167
- <WorkflowEditor :workflow="workflow" @change="onChange" />
168
- </div>
169
-
170
- <!-- Runs — trigger a run now; full run list lives on the Runs page. -->
171
- <div v-else-if="tab === 'runs'" class="p-6 space-y-3">
172
- <div class="rounded-lg border border-black/[0.06] bg-white p-4 flex items-center justify-between gap-3">
173
- <div>
174
- <h3 class="text-sm font-semibold">
175
- Run this workflow
176
- </h3>
177
- <p class="mt-0.5 text-sm text-muted-foreground">
178
- Triggers a manual run via the API. See all executions on the Runs page.
179
- </p>
180
- </div>
181
- <Button :disabled="running" class="h-8 text-sm" @click="runNow">
182
- <Loader2 v-if="running" :size="14" class="animate-spin" />
183
- <Play v-else :size="14" />
184
- {{ running ? "Starting\u2026" : "Run now" }}
185
- </Button>
186
- </div>
187
- <div v-if="runError" class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700">
188
- {{ runError }}
189
- </div>
190
- <pre v-if="runResult" class="overflow-auto rounded-lg border border-black/[0.06] bg-slate-50 p-3 text-xs">{{ runResult }}</pre>
541
+ <!-- Only the FAILED build state banners (its message matters); the
542
+ working states live as the header chip — and inside a run the
543
+ workflow's build state is noise, so nothing shows at all. -->
544
+ <div
545
+ v-if="!openRunId && buildNotice && buildNotice.tone === 'error'"
546
+ class="border-b border-rose-500/20 bg-rose-500/5 px-4 py-2 text-sm text-rose-700"
547
+ >
548
+ {{ buildNotice.text }}
191
549
  </div>
192
550
 
193
- <!-- Triggers tab — list of all triggers with sync to webhooks page -->
194
- <div v-else-if="tab === 'triggers'" class="p-6 space-y-3">
195
- <div
196
- v-for="trg in workflow.blob.triggers"
197
- :key="trg.id"
198
- class="rounded-lg border border-black/[0.06] bg-white p-3 flex items-center gap-3"
199
- >
200
- <component
201
- :is="trg.type === 'schedule' ? Clock : trg.type === 'webhook' ? Webhook : Play"
202
- :size="18"
203
- class="text-emerald-600"
204
- />
205
- <div class="flex-1 min-w-0">
206
- <div class="text-sm font-medium">
207
- {{ trg.type === "schedule" ? "Schedule" : trg.type === "webhook" ? "Webhook" : "Manual" }}
551
+ <!-- Nested run detail — overview/timeline panel or Replay canvas. -->
552
+ <template v-if="openRunId">
553
+ <div v-if="runView !== 'replay'" class="flex-1 overflow-y-auto p-4">
554
+ <div class="w-full">
555
+ <RunDetailPanel v-if="openRun" :run="openRun" :view="runView" @changed="loadOpenRun" />
556
+ <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">
557
+ {{ openRunError }}
208
558
  </div>
209
- <div class="text-sm text-muted-foreground truncate font-mono">
210
- <template v-if="trg.type === 'schedule'">
211
- cron: {{ trg.config.cron }} ({{ trg.config.timezone ?? "UTC" }})
212
- </template>
213
- <template v-else-if="trg.type === 'webhook'">
214
- {{ trg.config.method }} {{ trg.config.path }}
215
- </template>
216
- <template v-else>on demand</template>
559
+ <div v-else class="flex items-center gap-2 p-12 text-sm text-muted-foreground">
560
+ <Loader2 :size="16" class="animate-spin" /> Loading run…
217
561
  </div>
218
562
  </div>
219
- <div class="flex shrink-0 items-center gap-2">
220
- <span class="text-xs" :class="trg.active ? 'text-emerald-600' : 'text-muted-foreground'">
221
- {{ trg.active ? "Active" : "Inactive" }}
222
- </span>
223
- <button
224
- type="button"
225
- :disabled="togglingTrigger === trg.id"
226
- class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:opacity-50"
227
- :class="trg.active ? 'bg-emerald-500' : 'bg-slate-300'"
228
- :aria-pressed="trg.active"
229
- :title="trg.active ? 'Deactivate trigger' : 'Activate trigger'"
230
- @click="toggleTrigger(trg)"
231
- >
232
- <span
233
- class="inline-block size-4 transform rounded-full bg-white shadow transition-transform"
234
- :class="trg.active ? 'translate-x-[18px]' : 'translate-x-0.5'"
235
- />
236
- </button>
237
- </div>
238
563
  </div>
239
- <p class="text-xs text-muted-foreground">
240
- A workflow run needs at least one <span class="font-medium">active</span> trigger.
241
- </p>
564
+ <div v-else class="min-h-0 flex-1">
565
+ <WorkflowRunReplayPanel :workflow="workflow" :initial-run-id="openRunId" single />
566
+ </div>
567
+ </template>
568
+
569
+ <!-- Editor tab — fills the remaining height inside the panel via
570
+ the flex wrapper in the studio layout. -->
571
+ <div v-else-if="tab === 'editor'" class="flex-1 min-h-0">
572
+ <WorkflowEditor
573
+ :key="editorResetKey"
574
+ :workflow="workflow"
575
+ :run-busy="running || syncLoading"
576
+ @change="onChange"
577
+ @run="runFromEditor"
578
+ @toggle-active="toggleActive(workflow.id)"
579
+ @open-settings="settingsOpen = true"
580
+ />
242
581
  </div>
243
582
 
244
- <!-- Variables tab -->
245
- <div v-else-if="tab === 'variables'" class="p-6">
246
- <div v-if="!Object.keys(workflow.blob.variables ?? {}).length" class="rounded-lg border border-black/[0.06] bg-white p-6 text-center text-sm text-muted-foreground">
247
- No variables defined for this workflow.
583
+ <!-- Runs — this workflow's full execution history. Manual runs start
584
+ from the editor's Run split-button (payload via its lightbox). -->
585
+ <div v-else-if="tab === 'runs'" class="p-4 space-y-4 overflow-y-auto">
586
+ <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">
587
+ {{ runsError }}
248
588
  </div>
249
- <div v-else class="rounded-lg border border-black/[0.06] bg-white">
250
- <div class="grid grid-cols-[1fr_120px_1fr] gap-2 border-b border-black/[0.06] px-3 py-2 text-mono text-muted-foreground">
251
- <div>Key</div>
252
- <div>Type</div>
253
- <div>Default</div>
254
- </div>
255
- <div
256
- v-for="(v, key) in workflow.blob.variables ?? {}"
257
- :key="key"
258
- class="grid grid-cols-[1fr_120px_1fr] gap-2 border-b border-black/[0.04] px-3 py-2 text-sm last:border-b-0"
259
- >
260
- <div class="font-mono">
261
- {{ key }}
262
- </div>
263
- <div class="text-muted-foreground">
264
- {{ v.type }}
265
- </div>
266
- <div class="font-mono truncate">
267
- {{ JSON.stringify(v.default) }}
268
- </div>
589
+
590
+ <RunsTable
591
+ :runs="wfRuns"
592
+ :loading="runsLoading"
593
+ :pagination="false"
594
+ :show-workflow="false"
595
+ empty-text="No runs yet — hit Run in the editor."
596
+ @changed="() => loadRuns()"
597
+ />
598
+
599
+ <!-- Server pagination — the endpoint pages, so the whole history is reachable. -->
600
+ <div v-if="runsLastPage > 1 || runsTotal > wfRuns.length" class="flex items-center justify-between text-sm text-muted-foreground">
601
+ <span class="tabular-nums">Page {{ runsPage }} / {{ runsLastPage }} · {{ runsTotal }} run{{ runsTotal === 1 ? "" : "s" }}</span>
602
+ <div class="flex gap-2">
603
+ <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsPage <= 1 || runsLoading" @click="goRunsPage(runsPage - 1)">
604
+ ← Newer
605
+ </Button>
606
+ <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsPage >= runsLastPage || runsLoading" @click="goRunsPage(runsPage + 1)">
607
+ Older →
608
+ </Button>
269
609
  </div>
270
610
  </div>
271
611
  </div>
272
612
 
273
- <!-- Settings -->
274
- <div v-else-if="tab === 'settings'" class="p-6 space-y-4 text-sm">
275
- <div class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-2">
276
- <div class="text-mono text-muted-foreground">
277
- Metadata
278
- </div>
279
- <div class="grid grid-cols-[120px_1fr] gap-y-2 gap-x-3 text-sm">
280
- <div class="text-muted-foreground">
281
- ID
282
- </div><div class="font-mono">
283
- {{ workflow.id }}
284
- </div>
285
- <div class="text-muted-foreground">
286
- Definition
287
- </div><div class="font-mono">
288
- {{ workflow.blob_definition_version }}
289
- </div>
290
- <div class="text-muted-foreground">
291
- Revision
292
- </div><div class="font-mono">
293
- r{{ workflow.revision }}
294
- </div>
295
- <div class="text-muted-foreground">
296
- Created
297
- </div><div>{{ formatDateTime(workflow.created_at) }}</div>
298
- <div class="text-muted-foreground">
299
- Updated
300
- </div><div>{{ formatDateTime(workflow.updated_at) }}</div>
301
- </div>
302
- </div>
613
+ <!-- Dead letters — triggers that never produced a run -->
614
+ <div v-else-if="tab === 'dead-letters'" class="p-6 overflow-y-auto">
615
+ <IntegrationsDeadLettersPanel :workflow-id="workflow.id" />
303
616
  </div>
304
617
 
305
- <!-- History -->
306
- <div v-else-if="tab === 'history'" class="p-6">
307
- <div class="rounded-lg border border-black/[0.06] bg-white p-6 text-center">
308
- <div class="flex size-12 items-center justify-center rounded-full bg-slate-100 text-slate-500 mb-3 mx-auto">
309
- <HistoryIcon :size="22" />
310
- </div>
311
- <h3 class="text-sm font-semibold">
312
- {{ workflow.revision }} revision{{ workflow.revision === 1 ? "" : "s" }}
313
- </h3>
314
- <p class="mt-1 text-sm text-muted-foreground max-w-md mx-auto">
315
- Every save that changes the blob bumps the revision and snapshots the previous one. Once history endpoints land you'll be able to diff and roll back from here.
316
- </p>
317
- </div>
318
- </div>
618
+ <!-- History — n8n-style: read-only canvas preview + revision list. -->
619
+ <div v-else-if="tab === 'history'" class="flex-1 min-h-0">
620
+ <WorkflowHistoryPanel :workflow="workflow" @restored="onRevisionRestored" />
621
+ </div>
319
622
  </template>
320
623
 
321
624
  <div v-else class="p-12 text-center text-sm text-muted-foreground">
322
625
  Loading…
323
626
  </div>
324
627
  </div>
628
+
629
+ <!-- Run-with-payload lightbox — opened by the editor Run split-button. -->
630
+ <Dialog v-model:open="runPayloadOpen">
631
+ <DialogContent class="sm:max-w-lg">
632
+ <DialogHeader>
633
+ <DialogTitle>Run with payload</DialogTitle>
634
+ <DialogDescription class="text-sm">
635
+ Starts a manual run with this trigger payload. The result lands on the Runs tab.
636
+ </DialogDescription>
637
+ </DialogHeader>
638
+ <div v-if="eventTriggerTopic" class="flex flex-wrap items-center gap-1.5">
639
+ <span class="text-xs text-muted-foreground">Prefill <code class="font-mono">{{ eventTriggerTopic }}</code>:</span>
640
+ <Button
641
+ variant="outline"
642
+ class="h-7 text-xs"
643
+ :disabled="!catalogSample"
644
+ :title="catalogSample ? 'Insert the catalog sample payload' : 'The event catalog has no sample for this topic.'"
645
+ @click="insertSampleEvent"
646
+ >
647
+ Sample event
648
+ </Button>
649
+ <Button
650
+ variant="outline"
651
+ class="h-7 text-xs"
652
+ :disabled="prefillBusy !== null"
653
+ title="Insert the latest received event of this topic"
654
+ @click="insertLatestEvent"
655
+ >
656
+ <Loader2 v-if="prefillBusy === 'latest'" :size="12" class="animate-spin" />
657
+ Latest real event
658
+ </Button>
659
+ </div>
660
+ <div class="space-y-1">
661
+ <IntegrationsRunPayloadInput v-if="workflow" v-model="payloadText" :blob="workflow.blob" />
662
+ <p v-if="payloadError" class="text-xs text-rose-600">
663
+ {{ payloadError }}
664
+ </p>
665
+ </div>
666
+ <div v-if="workflow" class="flex items-center justify-end gap-2">
667
+ <template v-if="workflow.execution_mode === 'caller_decides'">
668
+ <Button
669
+ variant="outline"
670
+ :disabled="running || syncLoading || buildBlocked"
671
+ :title="buildBlocked ? 'The workflow bundle is not ready to run yet.' : void 0"
672
+ class="h-8 text-sm"
673
+ @click="runFromPayloadDialog('sync')"
674
+ >
675
+ <Loader2 v-if="syncLoading" :size="14" class="animate-spin" />
676
+ <Play v-else :size="14" />
677
+ Run (sync)
678
+ </Button>
679
+ <Button
680
+ :disabled="running || syncLoading || buildBlocked"
681
+ :title="buildBlocked ? 'The workflow bundle is not ready to run yet.' : void 0"
682
+ class="h-8 text-sm"
683
+ @click="runFromPayloadDialog('async')"
684
+ >
685
+ <Loader2 v-if="running" :size="14" class="animate-spin" />
686
+ <Play v-else :size="14" />
687
+ Run (async)
688
+ </Button>
689
+ </template>
690
+ <Button
691
+ v-else
692
+ :disabled="running || syncLoading || buildBlocked"
693
+ :title="buildBlocked ? 'The workflow bundle is not ready to run yet.' : void 0"
694
+ class="h-8 text-sm"
695
+ @click="runFromPayloadDialog(primaryMode)"
696
+ >
697
+ <Loader2 v-if="running || syncLoading" :size="14" class="animate-spin" />
698
+ <Play v-else :size="14" />
699
+ Run ({{ primaryMode }})
700
+ </Button>
701
+ </div>
702
+ </DialogContent>
703
+ </Dialog>
704
+
705
+ <IntegrationsRunResponseModal
706
+ :open="syncOpen"
707
+ :loading="syncLoading"
708
+ :mode="runModalMode"
709
+ :response="syncResponse"
710
+ :error="syncError"
711
+ :run-id="runModalRunId"
712
+ @update:open="(v) => syncOpen = v"
713
+ @view-run="viewRunFromModal"
714
+ />
715
+
716
+ <WorkflowSettingsDialog
717
+ v-if="workflow"
718
+ v-model:open="settingsOpen"
719
+ :workflow="workflow"
720
+ :modes="EXECUTION_MODES"
721
+ :mode-draft="executionModeDraft"
722
+ :saving-mode="savingMode"
723
+ :saving-meta="savingMeta"
724
+ :tag-suggestions="tagSuggestions"
725
+ @change-mode="(m) => saveExecutionMode(m)"
726
+ @save-meta="saveWorkflowMeta"
727
+ />
728
+
729
+ <WorkflowDuplicateDialog v-model:open="duplicateOpen" :workflow="duplicateTarget" @duplicated="onDuplicated" />
325
730
  </IntegrationsLayout>
326
731
  </template>