@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.
Files changed (171) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/README.md +48 -4
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +10 -2
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  12. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
  13. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  14. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  15. package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
  16. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  17. package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
  18. package/dist/runtime/components/Integrations/Layout.vue +48 -4
  19. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
  20. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
  21. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  22. package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
  23. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  24. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  25. package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
  26. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  27. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  28. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  29. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  30. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  31. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  32. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  33. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  34. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
  35. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  36. package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
  37. package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
  38. package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
  39. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  40. package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
  41. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  42. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  43. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  44. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  45. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  46. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  47. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  48. package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
  49. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  50. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  51. package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
  52. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  53. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  54. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
  55. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  56. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  57. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
  58. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  59. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  60. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
  61. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  62. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
  63. package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
  64. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
  65. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
  66. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  67. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
  68. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  69. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  70. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
  71. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  72. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
  73. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
  74. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  75. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
  76. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  77. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
  78. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
  79. package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
  80. package/dist/runtime/composables/integrationsApi.js +4 -2
  81. package/dist/runtime/composables/useCredentialTypes.js +8 -6
  82. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  83. package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
  84. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  85. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  86. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  87. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  88. package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
  89. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  90. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  91. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  92. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  93. package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
  94. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  95. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  96. package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
  97. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  98. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  99. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  100. package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
  101. package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
  102. package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
  103. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  104. package/dist/runtime/composables/useLoadGate.js +8 -0
  105. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  106. package/dist/runtime/data/branchReorder.d.ts +37 -0
  107. package/dist/runtime/data/branchReorder.js +71 -0
  108. package/dist/runtime/data/handleId.d.ts +23 -0
  109. package/dist/runtime/data/handleId.js +6 -0
  110. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  111. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  112. package/dist/runtime/data/integrationTemplates.d.ts +48 -0
  113. package/dist/runtime/data/integrationTemplates.js +21 -0
  114. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  115. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  116. package/dist/runtime/data/nodeIcons.js +73 -0
  117. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  118. package/dist/runtime/data/nodeInputData.js +4 -0
  119. package/dist/runtime/data/payloadForm.d.ts +9 -2
  120. package/dist/runtime/data/payloadForm.js +29 -4
  121. package/dist/runtime/data/portTone.d.ts +17 -0
  122. package/dist/runtime/data/portTone.js +7 -0
  123. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  124. package/dist/runtime/data/webhookAuth.js +19 -0
  125. package/dist/runtime/data/workflowIconCatalog.js +4 -6
  126. package/dist/runtime/i18n/messages/de.json +1207 -0
  127. package/dist/runtime/i18n/messages/en.json +1207 -0
  128. package/dist/runtime/i18n/translate.d.ts +83 -0
  129. package/dist/runtime/i18n/translate.js +41 -0
  130. package/dist/runtime/pages/integrations/audit.vue +140 -37
  131. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
  132. package/dist/runtime/pages/integrations/credentials.vue +180 -64
  133. package/dist/runtime/pages/integrations/index.vue +91 -92
  134. package/dist/runtime/pages/integrations/runs.vue +33 -21
  135. package/dist/runtime/pages/integrations/schedules.vue +88 -23
  136. package/dist/runtime/pages/integrations/secrets.vue +57 -35
  137. package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
  138. package/dist/runtime/pages/integrations/templates/index.vue +82 -103
  139. package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
  140. package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
  141. package/dist/runtime/plugins/locale.d.ts +10 -0
  142. package/dist/runtime/plugins/locale.js +6 -0
  143. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  144. package/dist/runtime/plugins/realtime.client.js +4 -0
  145. package/dist/runtime/types/integrations.d.ts +65 -6
  146. package/dist/runtime/utils/apiError.d.ts +47 -0
  147. package/dist/runtime/utils/apiError.js +32 -0
  148. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  149. package/dist/runtime/utils/cockpitSources.js +14 -0
  150. package/dist/runtime/utils/credentialState.d.ts +24 -1
  151. package/dist/runtime/utils/credentialState.js +26 -11
  152. package/dist/runtime/utils/loadGate.d.ts +61 -0
  153. package/dist/runtime/utils/loadGate.js +22 -0
  154. package/dist/runtime/utils/localize.d.ts +46 -4
  155. package/dist/runtime/utils/localize.js +34 -7
  156. package/dist/runtime/utils/notify.d.ts +59 -0
  157. package/dist/runtime/utils/notify.js +21 -0
  158. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  159. package/dist/runtime/utils/pollBudget.js +17 -0
  160. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  161. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  162. package/dist/runtime/utils/schedule.d.ts +116 -0
  163. package/dist/runtime/utils/schedule.js +219 -0
  164. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  165. package/dist/runtime/utils/statusStyle.js +9 -1
  166. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  167. package/dist/runtime/utils/validationFindings.js +54 -0
  168. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  169. package/dist/runtime/utils/whenIdle.js +12 -0
  170. package/dist/types.d.mts +2 -6
  171. package/package.json +10 -3
@@ -14,6 +14,7 @@ const props = defineProps({
14
14
  run: { type: Object, required: true }
15
15
  });
16
16
  const emit = defineEmits(["changed"]);
17
+ const t = useIntegrationsT();
17
18
  const router = useRouter();
18
19
  const { cancelRun, terminateRun, retryRun, resumeRun } = useIntegrationsRuns();
19
20
  function viewDetails() {
@@ -26,8 +27,31 @@ const isActive = computed(() => props.run.status === "running" || props.run.stat
26
27
  const isTerminal = computed(() => ["succeeded", "failed", "cancelled"].includes(props.run.status));
27
28
  const canResume = computed(() => props.run.status === "failed");
28
29
  const hasAny = computed(() => isActive.value || isTerminal.value);
29
- const LABEL = { cancel: "Cancel", terminate: "Terminate", retry: "Retry", resume: "Resume" };
30
30
  const busy = ref(false);
31
+ function actionLabel(a) {
32
+ switch (a) {
33
+ case "cancel":
34
+ return t("integration.runs.menu.cancel");
35
+ case "terminate":
36
+ return t("integration.runs.menu.terminate");
37
+ case "retry":
38
+ return t("integration.runs.menu.retry");
39
+ case "resume":
40
+ return t("integration.runs.menu.resume");
41
+ }
42
+ }
43
+ function actionErrorFallback(a) {
44
+ switch (a) {
45
+ case "cancel":
46
+ return t("integration.runs.error.cancel");
47
+ case "terminate":
48
+ return t("integration.runs.error.terminate");
49
+ case "retry":
50
+ return t("integration.runs.error.retry");
51
+ case "resume":
52
+ return t("integration.runs.error.resume");
53
+ }
54
+ }
31
55
  async function runAction(a) {
32
56
  if (busy.value) return;
33
57
  busy.value = true;
@@ -49,18 +73,18 @@ async function runAction(a) {
49
73
  }
50
74
  const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
51
75
  toast.success(
52
- (a === "retry" || a === "resume") && newId && newId !== String(props.run.id) ? `Started run #${newId}` : `${LABEL[a]} requested`
76
+ (a === "retry" || a === "resume") && newId && newId !== String(props.run.id) ? t("integration.runs.toast.action.started", { id: newId }) : t("integration.runs.toast.action.requested", { action: actionLabel(a) })
53
77
  );
54
78
  emit("changed");
55
79
  } catch (e) {
56
- toast.error(e?.message ?? `Could not ${LABEL[a].toLowerCase()} the run.`);
80
+ toast.error(e?.message ?? actionErrorFallback(a));
57
81
  } finally {
58
82
  busy.value = false;
59
83
  }
60
84
  }
61
85
  function confirmTerminate() {
62
- toast(`Terminate run #${props.run.id}?`, {
63
- action: { label: "Terminate", onClick: () => void runAction("terminate") }
86
+ toast(t("integration.runs.toast.terminate.confirm", { id: props.run.id }), {
87
+ action: { label: actionLabel("terminate"), onClick: () => void runAction("terminate") }
64
88
  });
65
89
  }
66
90
  </script>
@@ -68,19 +92,21 @@ function confirmTerminate() {
68
92
  <template>
69
93
  <DropdownMenu>
70
94
  <DropdownMenuTrigger as-child>
71
- <Button variant="ghost" size="icon" class="size-7" :disabled="busy" @click.stop>
95
+ <!-- Icon-only, so it needs a name of its own — the run id is the only
96
+ thing that tells two rows' menus apart. -->
97
+ <Button variant="ghost" size="icon" class="size-7" :disabled="busy" :aria-label="t('integration.runs.menu.aria.actions', { id: run.id })" @click.stop>
72
98
  <MoreHorizontal :size="16" />
73
99
  </Button>
74
100
  </DropdownMenuTrigger>
75
101
  <DropdownMenuContent align="end" class="w-40" @click.stop>
76
- <DropdownMenuItem @select="viewDetails">View details</DropdownMenuItem>
77
- <DropdownMenuItem @select="viewReplay">Replay</DropdownMenuItem>
102
+ <DropdownMenuItem @select="viewDetails">{{ t("integration.runs.menu.viewDetails") }}</DropdownMenuItem>
103
+ <DropdownMenuItem @select="viewReplay">{{ t("integration.runs.menu.replay") }}</DropdownMenuItem>
78
104
  <DropdownMenuSeparator v-if="hasAny" />
79
- <DropdownMenuItem v-if="isActive" @select="runAction('cancel')">Cancel run</DropdownMenuItem>
80
- <DropdownMenuItem v-if="isActive" class="text-rose-600" @select="confirmTerminate">Terminate</DropdownMenuItem>
105
+ <DropdownMenuItem v-if="isActive" @select="runAction('cancel')">{{ actionLabel("cancel") }}</DropdownMenuItem>
106
+ <DropdownMenuItem v-if="isActive" class="text-rose-600" @select="confirmTerminate">{{ actionLabel("terminate") }}</DropdownMenuItem>
81
107
  <DropdownMenuSeparator v-if="isActive && (canResume || isTerminal)" />
82
- <DropdownMenuItem v-if="canResume" @select="runAction('resume')">Resume</DropdownMenuItem>
83
- <DropdownMenuItem v-if="isTerminal" @select="runAction('retry')">Retry</DropdownMenuItem>
108
+ <DropdownMenuItem v-if="canResume" @select="runAction('resume')">{{ actionLabel("resume") }}</DropdownMenuItem>
109
+ <DropdownMenuItem v-if="isTerminal" @select="runAction('retry')">{{ actionLabel("retry") }}</DropdownMenuItem>
84
110
  </DropdownMenuContent>
85
111
  </DropdownMenu>
86
112
  </template>
@@ -4,11 +4,13 @@ import { Download as DownloadIcon, RefreshCircle as RefreshIcon } from "#solar-i
4
4
  import { Badge, Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@revenexx/studio";
5
5
  import { runStatusLabel, statusClass } from "../../utils/statusStyle";
6
6
  import { IntegrationsApiError } from "../../utils/apiError";
7
+ import { createPollBudget, SAFETY_POLL_MS } from "../../utils/pollBudget";
7
8
  const props = defineProps({
8
9
  run: { type: Object, required: true },
9
10
  view: { type: String, required: false, default: "details" }
10
11
  });
11
12
  const emit = defineEmits(["changed"]);
13
+ const t = useIntegrationsT();
12
14
  const {
13
15
  getRun,
14
16
  getDetails,
@@ -23,6 +25,9 @@ const {
23
25
  const DETAILS_POLL_MS = 3e3;
24
26
  const HISTORY_POLL_MS = 1e4;
25
27
  const HISTORY_PAGE_SIZE = 50;
28
+ const { delivering: realtimeDelivering, runTick, lastRunEvent } = useIntegrationsRealtime();
29
+ const summaryBudget = createPollBudget({ intervalMs: DETAILS_POLL_MS, liveIntervalMs: SAFETY_POLL_MS });
30
+ const historyBudget = createPollBudget({ intervalMs: HISTORY_POLL_MS, liveIntervalMs: SAFETY_POLL_MS });
26
31
  const COCKPIT_TERMINAL = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled"]);
27
32
  const API_TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "terminated", "timed_out"]);
28
33
  const runData = ref(null);
@@ -127,9 +132,9 @@ function eventAttributes(ev) {
127
132
  return pick(ev, ["attributes"]);
128
133
  }
129
134
  function eventColor(type) {
130
- const t = type.toLowerCase();
131
- if (t.includes("failed") || t.includes("terminated")) return "text-rose-600";
132
- if (t.includes("completed")) return "text-emerald-600";
135
+ const t2 = type.toLowerCase();
136
+ if (t2.includes("failed") || t2.includes("terminated")) return "text-rose-600";
137
+ if (t2.includes("completed")) return "text-emerald-600";
133
138
  return "text-foreground";
134
139
  }
135
140
  function statusOf(e) {
@@ -141,29 +146,29 @@ function genericMsg(e, fallback) {
141
146
  }
142
147
  function detailsErrMsg(e) {
143
148
  const s = statusOf(e);
144
- if (s === 410) return "History is no longer available (retention expired and no snapshot).";
145
- if (s === 503) return "Temporal is currently unavailable.";
146
- return genericMsg(e, "Could not load Temporal details.");
149
+ if (s === 410) return t("integration.runs.detail.error.details.expired");
150
+ if (s === 503) return t("integration.runs.detail.error.details.unavailable");
151
+ return genericMsg(e, t("integration.runs.detail.error.details.generic"));
147
152
  }
148
153
  function historyErrMsg(e) {
149
154
  const s = statusOf(e);
150
- if (s === 410) return "History is no longer available.";
151
- if (s === 422) return "Pagination cursor invalidated \u2014 reset.";
152
- return genericMsg(e, "Could not load history.");
155
+ if (s === 410) return t("integration.runs.detail.error.history.expired");
156
+ if (s === 422) return t("integration.runs.detail.error.history.cursorInvalid");
157
+ return genericMsg(e, t("integration.runs.detail.error.history.generic"));
153
158
  }
154
159
  function stackErrMsg(e) {
155
160
  const s = statusOf(e);
156
- if (s === 409) return "Workflow is no longer running.";
157
- if (s === 410) return "Unknown to Temporal.";
158
- if (s === 503) return "Temporal unavailable.";
159
- return genericMsg(e, "Could not load stack trace.");
161
+ if (s === 409) return t("integration.runs.detail.error.stack.notRunning");
162
+ if (s === 410) return t("integration.runs.detail.error.stack.unknown");
163
+ if (s === 503) return t("integration.runs.detail.error.stack.unavailable");
164
+ return genericMsg(e, t("integration.runs.detail.error.stack.generic"));
160
165
  }
161
166
  function stepsErrMsg(e) {
162
167
  const s = statusOf(e);
163
- if (s === 409) return "Run has not been handed off to Temporal yet.";
164
- if (s === 410) return "Execution history is no longer available.";
165
- if (s === 503) return "Temporal is currently unavailable.";
166
- return genericMsg(e, "Could not load steps.");
168
+ if (s === 409) return t("integration.runs.detail.notHandedOff");
169
+ if (s === 410) return t("integration.runs.detail.error.steps.expired");
170
+ if (s === 503) return t("integration.runs.detail.error.details.unavailable");
171
+ return genericMsg(e, t("integration.runs.detail.error.steps.generic"));
167
172
  }
168
173
  function fmtMs(ms) {
169
174
  if (ms == null) return "\u2014";
@@ -174,7 +179,7 @@ function fmtMs(ms) {
174
179
  function activityName(a) {
175
180
  const at = a ? a.activity_type ?? a.activityType : void 0;
176
181
  if (typeof at === "string") return at;
177
- return pickStr(at, ["name"]) ?? "Activity";
182
+ return pickStr(at, ["name"]) ?? t("integration.runs.detail.timeline.label.activity");
178
183
  }
179
184
  const timeline = computed(() => {
180
185
  const evs = timelineEvents.value;
@@ -189,7 +194,7 @@ const timeline = computed(() => {
189
194
  const ts = timeOf(ev);
190
195
  if (!Number.isFinite(ts)) continue;
191
196
  if (type === "workflow_execution_started") {
192
- wf = { label: "Workflow", kind: "workflow", sched: ts, start: ts, end: null, ok: true, events: [ev] };
197
+ wf = { label: t("integration.runs.detail.timeline.label.workflow"), kind: "workflow", sched: ts, start: ts, end: null, ok: true, events: [ev] };
193
198
  } else if (type.startsWith("workflow_execution_")) {
194
199
  if (wf) {
195
200
  wf.end = ts;
@@ -199,7 +204,7 @@ const timeline = computed(() => {
199
204
  } else if (type === "activity_task_scheduled") {
200
205
  cycles.set(eventId(ev), { label: activityName(attrsOf(ev)), kind: "activity", sched: ts, start: null, end: null, ok: true, events: [ev] });
201
206
  } else if (type === "workflow_task_scheduled") {
202
- cycles.set(eventId(ev), { label: "Workflow task", kind: "wf-task", sched: ts, start: null, end: null, ok: true, events: [ev] });
207
+ cycles.set(eventId(ev), { label: t("integration.runs.detail.timeline.label.workflowTask"), kind: "wf-task", sched: ts, start: null, end: null, ok: true, events: [ev] });
203
208
  } else if (type === "activity_task_started" || type === "workflow_task_started") {
204
209
  const c = cycles.get(refOf(ev, ["scheduled_event_id", "scheduledEventId"]));
205
210
  if (c) {
@@ -214,7 +219,7 @@ const timeline = computed(() => {
214
219
  c.events.push(ev);
215
220
  }
216
221
  } else if (type === "timer_started") {
217
- cycles.set(eventId(ev), { label: "Timer", kind: "timer", sched: ts, start: ts, end: null, ok: true, events: [ev] });
222
+ cycles.set(eventId(ev), { label: t("integration.runs.detail.timeline.label.timer"), kind: "timer", sched: ts, start: ts, end: null, ok: true, events: [ev] });
218
223
  } else if (type === "timer_fired" || type === "timer_canceled") {
219
224
  const c = cycles.get(refOf(ev, ["started_event_id", "startedEventId"]));
220
225
  if (c) {
@@ -266,8 +271,8 @@ function timelineBarClass(row) {
266
271
  }
267
272
  const timelineDetail = ref(null);
268
273
  function timelineRowStatus(row) {
269
- if (row.durationMs == null) return "running";
270
- return row.ok ? "succeeded" : "failed";
274
+ if (row.durationMs == null) return t("integration.runs.status.running");
275
+ return t(row.ok ? "integration.runs.status.succeeded" : "integration.runs.status.failed");
271
276
  }
272
277
  function fmtClock(ms) {
273
278
  try {
@@ -277,6 +282,7 @@ function fmtClock(ms) {
277
282
  }
278
283
  }
279
284
  async function loadSummary() {
285
+ summaryBudget.spend();
280
286
  summaryLoading.value = true;
281
287
  summaryError.value = null;
282
288
  try {
@@ -353,6 +359,7 @@ async function loadTimelineEvents() {
353
359
  historyError.value = null;
354
360
  return;
355
361
  }
362
+ historyBudget.spend();
356
363
  if (!timelineEvents.value.length) historyLoading.value = true;
357
364
  historyError.value = null;
358
365
  try {
@@ -389,6 +396,7 @@ function startPollingIfNeeded() {
389
396
  stopPolling();
390
397
  if (isTerminal.value) return;
391
398
  summaryTimer = setInterval(() => {
399
+ if (!summaryBudget.due(realtimeDelivering.value)) return;
392
400
  void loadSummary().then(() => {
393
401
  if (isTerminal.value) {
394
402
  stopPolling();
@@ -399,9 +407,23 @@ function startPollingIfNeeded() {
399
407
  });
400
408
  }, DETAILS_POLL_MS);
401
409
  historyTimer = setInterval(() => {
410
+ if (!historyBudget.due(realtimeDelivering.value)) return;
402
411
  void loadTimelineEvents();
403
412
  }, HISTORY_POLL_MS);
404
413
  }
414
+ function refreshFromEvent() {
415
+ void loadSummary().then(() => {
416
+ if (isTerminal.value) stopPolling();
417
+ void loadDetails();
418
+ void loadSteps();
419
+ });
420
+ void loadTimelineEvents();
421
+ }
422
+ watch(runTick, () => {
423
+ const event = lastRunEvent.value;
424
+ if (event && event.topicId !== props.run.id) return;
425
+ refreshFromEvent();
426
+ });
405
427
  function resetState() {
406
428
  runData.value = null;
407
429
  summaryError.value = null;
@@ -422,12 +444,42 @@ function resetState() {
422
444
  actionError.value = null;
423
445
  actionNotice.value = null;
424
446
  }
425
- const ACTION_LABELS = {
426
- cancel: "Cancel",
427
- terminate: "Terminate",
428
- retry: "Retry",
429
- resume: "Resume"
430
- };
447
+ function actionLabel(a) {
448
+ switch (a) {
449
+ case "cancel":
450
+ return t("integration.runs.action.cancel");
451
+ case "terminate":
452
+ return t("integration.runs.action.terminate");
453
+ case "retry":
454
+ return t("integration.runs.action.retry");
455
+ case "resume":
456
+ return t("integration.runs.action.resume");
457
+ }
458
+ }
459
+ function confirmQuestion(a) {
460
+ switch (a) {
461
+ case "cancel":
462
+ return t("integration.runs.confirm.cancel");
463
+ case "terminate":
464
+ return t("integration.runs.confirm.terminate");
465
+ case "retry":
466
+ return t("integration.runs.confirm.retry");
467
+ case "resume":
468
+ return t("integration.runs.confirm.resume");
469
+ }
470
+ }
471
+ function actionErrorFallback(a) {
472
+ switch (a) {
473
+ case "cancel":
474
+ return t("integration.runs.error.cancel");
475
+ case "terminate":
476
+ return t("integration.runs.error.terminate");
477
+ case "retry":
478
+ return t("integration.runs.error.retry");
479
+ case "resume":
480
+ return t("integration.runs.error.resume");
481
+ }
482
+ }
431
483
  const apiStatusRaw = computed(() => pickStr(runData.value, ["status"]));
432
484
  const isActive = computed(() => !isTerminal.value);
433
485
  const canStop = computed(() => isActive.value && hasTemporal.value);
@@ -476,13 +528,13 @@ async function confirmAction() {
476
528
  break;
477
529
  }
478
530
  const newId = pickStr(res, ["id"]);
479
- actionNotice.value = (a === "retry" || a === "resume") && newId && newId !== String(props.run.id) ? `Started run #${newId}.` : `${ACTION_LABELS[a]} requested.`;
531
+ actionNotice.value = (a === "retry" || a === "resume") && newId && newId !== String(props.run.id) ? t("integration.runs.notice.started", { id: newId }) : t("integration.runs.notice.requested", { action: actionLabel(a) });
480
532
  armed.value = null;
481
533
  await loadAll();
482
534
  startPollingIfNeeded();
483
535
  emit("changed");
484
536
  } catch (e) {
485
- actionError.value = genericMsg(e, `Could not ${ACTION_LABELS[a].toLowerCase()} the run.`);
537
+ actionError.value = genericMsg(e, actionErrorFallback(a));
486
538
  } finally {
487
539
  actionBusy.value = false;
488
540
  }
@@ -498,7 +550,7 @@ async function confirmAction() {
498
550
  <div v-if="hasActions" class="flex items-center gap-2">
499
551
  <!-- confirm step: one click arms, second confirms -->
500
552
  <template v-if="armed">
501
- <span class="text-sm text-muted-foreground">{{ ACTION_LABELS[armed] }} this run?</span>
553
+ <span class="text-sm text-muted-foreground">{{ confirmQuestion(armed) }}</span>
502
554
  <Button
503
555
  size="sm"
504
556
  :class="armed === 'terminate' ? 'bg-rose-600 hover:bg-rose-700 text-white' : ''"
@@ -506,14 +558,14 @@ async function confirmAction() {
506
558
  @click="confirmAction"
507
559
  >
508
560
  <RefreshIcon v-if="actionBusy" :size="14" class="mr-1 animate-spin" />
509
- Confirm
561
+ {{ t("integration.common.action.confirm") }}
510
562
  </Button>
511
- <Button variant="ghost" size="sm" :disabled="actionBusy" @click="disarm">Back</Button>
563
+ <Button variant="ghost" size="sm" :disabled="actionBusy" @click="disarm">{{ t("integration.runs.action.back") }}</Button>
512
564
  </template>
513
565
  <template v-else>
514
- <Button v-if="canStop" variant="outline" size="sm" @click="arm('cancel')">Cancel run</Button>
515
- <Button v-if="canStop" variant="outline" size="sm" class="text-rose-600 hover:bg-rose-500/10" @click="arm('terminate')">Terminate</Button>
516
- <Button v-if="canResume" variant="outline" size="sm" @click="arm('resume')">Resume</Button>
566
+ <Button v-if="canStop" variant="outline" size="sm" @click="arm('cancel')">{{ actionLabel("cancel") }}</Button>
567
+ <Button v-if="canStop" variant="outline" size="sm" class="text-rose-600 hover:bg-rose-500/10" @click="arm('terminate')">{{ actionLabel("terminate") }}</Button>
568
+ <Button v-if="canResume" variant="outline" size="sm" @click="arm('resume')">{{ actionLabel("resume") }}</Button>
517
569
  </template>
518
570
  </div>
519
571
 
@@ -529,7 +581,7 @@ async function confirmAction() {
529
581
  <section class="rounded-md border border-black/[0.06] bg-white p-4 space-y-3">
530
582
  <div class="grid gap-x-6 gap-y-3 sm:grid-cols-2 xl:grid-cols-4">
531
583
  <div>
532
- <div class="text-[10px] uppercase tracking-wide text-muted-foreground">Status</div>
584
+ <div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.field.status") }}</div>
533
585
  <div class="mt-0.5">
534
586
  <Badge variant="outline" :class="['border text-xs font-medium uppercase', statusClass(dStatus ?? run.status)]">
535
587
  {{ runStatusLabel(dStatus ?? run.status) }}
@@ -537,31 +589,31 @@ async function confirmAction() {
537
589
  </div>
538
590
  </div>
539
591
  <div>
540
- <div class="text-[10px] uppercase tracking-wide text-muted-foreground">Started</div>
592
+ <div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.field.started") }}</div>
541
593
  <div class="tabular-nums">{{ fmt(startedAt) }}</div>
542
594
  </div>
543
595
  <div>
544
- <div class="text-[10px] uppercase tracking-wide text-muted-foreground">Completed</div>
596
+ <div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.field.completed") }}</div>
545
597
  <div class="tabular-nums">{{ fmt(completedAt) }}</div>
546
598
  </div>
547
599
  <div>
548
- <div class="text-[10px] uppercase tracking-wide text-muted-foreground">History Length</div>
600
+ <div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.field.historyLength") }}</div>
549
601
  <div class="tabular-nums">{{ dHistoryLength ?? "\u2014" }}</div>
550
602
  </div>
551
603
  <div class="min-w-0">
552
- <div class="text-[10px] uppercase tracking-wide text-muted-foreground">Workflow</div>
604
+ <div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.field.workflow") }}</div>
553
605
  <div class="truncate font-mono text-xs leading-5" :title="temporalWorkflowId ?? '\u2014'">{{ temporalWorkflowId ?? "\u2014" }}</div>
554
606
  </div>
555
607
  <div class="min-w-0">
556
- <div class="text-[10px] uppercase tracking-wide text-muted-foreground">Run ID</div>
608
+ <div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.field.runId") }}</div>
557
609
  <div class="truncate font-mono text-xs leading-5" :title="temporalRunId ?? '\u2014'">{{ temporalRunId ?? "\u2014" }}</div>
558
610
  </div>
559
611
  <div class="min-w-0">
560
- <div class="text-[10px] uppercase tracking-wide text-muted-foreground">Workflow Type</div>
612
+ <div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.field.workflowType") }}</div>
561
613
  <div class="truncate font-mono text-xs leading-5">{{ dWorkflowType ?? "\u2014" }}</div>
562
614
  </div>
563
615
  <div class="min-w-0">
564
- <div class="text-[10px] uppercase tracking-wide text-muted-foreground">Task Queue</div>
616
+ <div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.field.taskQueue") }}</div>
565
617
  <div class="truncate font-mono text-xs leading-5">{{ dTaskQueue ?? "\u2014" }}</div>
566
618
  </div>
567
619
  </div>
@@ -578,7 +630,7 @@ async function confirmAction() {
578
630
 
579
631
  <!-- Result — full width below the overview. -->
580
632
  <section v-if="hasResult" class="rounded-md border border-black/[0.06] bg-white p-4 space-y-2">
581
- <h4 class="font-mono text-xs uppercase tracking-wide text-muted-foreground">Result</h4>
633
+ <h4 class="font-mono text-xs uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.result.title") }}</h4>
582
634
  <div v-if="resultLinks.length" class="flex flex-wrap gap-1.5">
583
635
  <a
584
636
  v-for="link in resultLinks"
@@ -598,18 +650,18 @@ async function confirmAction() {
598
650
  <!-- Stack Trace (non-terminal runs only, lazy) -->
599
651
  <section v-if="!isTerminal" class="rounded-md border border-black/[0.06] bg-white p-4 space-y-3">
600
652
  <div class="flex items-center gap-2 border-b border-black/[0.06] pb-2">
601
- <h4 class="font-mono text-xs uppercase tracking-wide text-muted-foreground">Stack Trace</h4>
653
+ <h4 class="font-mono text-xs uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.stackTrace.title") }}</h4>
602
654
  <div class="flex-1" />
603
655
  <Button variant="outline" size="sm" :disabled="stackLoading" @click="loadStack">
604
- {{ stackEnabled ? "\u21BB Reload" : "Load" }}
656
+ {{ stackEnabled ? t("integration.runs.detail.stackTrace.action.reload") : t("integration.runs.detail.stackTrace.action.load") }}
605
657
  </Button>
606
658
  </div>
607
659
 
608
660
  <p v-if="!stackEnabled" class="text-xs text-muted-foreground">
609
- Load to query the running worker for its current call stack.
661
+ {{ t("integration.runs.detail.stackTrace.hint") }}
610
662
  </p>
611
663
  <div v-else-if="stackLoading" class="flex items-center gap-2 text-muted-foreground">
612
- <RefreshIcon :size="14" class="animate-spin" /> Querying worker…
664
+ <RefreshIcon :size="14" class="animate-spin" /> {{ t("integration.runs.detail.stackTrace.loading") }}
613
665
  </div>
614
666
  <p v-else-if="stackError" class="text-rose-700">{{ stackError }}</p>
615
667
  <pre
@@ -624,18 +676,18 @@ async function confirmAction() {
624
676
  <!-- Steps (per-node projection) -->
625
677
  <section v-if="hasTemporal" class="rounded-md border border-black/[0.06] bg-white p-4 space-y-3">
626
678
  <div class="flex items-center gap-2 border-b border-black/[0.06] pb-2">
627
- <h4 class="font-mono text-xs uppercase tracking-wide text-muted-foreground">Steps</h4>
679
+ <h4 class="font-mono text-xs uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.steps.title") }}</h4>
628
680
  <div class="flex-1" />
629
681
  <Button variant="outline" size="sm" :disabled="stepsLoading" @click="toggleStepPayloads">
630
- {{ stepsWithPayloads ? "Hide payloads" : "Show payloads" }}
682
+ {{ stepsWithPayloads ? t("integration.runs.detail.steps.action.hidePayloads") : t("integration.runs.detail.steps.action.showPayloads") }}
631
683
  </Button>
632
684
  </div>
633
685
 
634
686
  <p v-if="stepsError" class="text-rose-700">{{ stepsError }}</p>
635
687
  <div v-else-if="stepsLoading && !steps.length" class="flex items-center gap-2 text-muted-foreground">
636
- <RefreshIcon :size="14" class="animate-spin" /> Loading steps…
688
+ <RefreshIcon :size="14" class="animate-spin" /> {{ t("integration.runs.detail.steps.loading") }}
637
689
  </div>
638
- <p v-else-if="!steps.length" class="text-muted-foreground">No steps projected yet.</p>
690
+ <p v-else-if="!steps.length" class="text-muted-foreground">{{ t("integration.runs.detail.steps.empty") }}</p>
639
691
  <ol v-else class="space-y-1">
640
692
  <li
641
693
  v-for="(st, i) in steps"
@@ -651,7 +703,7 @@ async function confirmAction() {
651
703
  <div class="min-w-0 flex-1">
652
704
  <div class="font-mono text-xs truncate">{{ st.node_slug || st.node_id }}</div>
653
705
  <div class="text-[10px] text-muted-foreground truncate">
654
- {{ st.node_id }}<template v-if="st.branch"> · branch {{ st.branch }}</template><template v-if="st.attempt"> · attempt {{ st.attempt + 1 }}</template>
706
+ {{ st.node_id }}<template v-if="st.branch"> · {{ t("integration.runs.detail.steps.branchLabel") }} {{ st.branch }}</template><template v-if="st.attempt"> · {{ t("integration.runs.detail.steps.attemptLabel") }} {{ st.attempt + 1 }}</template>
655
707
  </div>
656
708
  </div>
657
709
  <span class="text-xs text-muted-foreground tabular-nums shrink-0">{{ fmtMs(st.duration_ms) }}</span>
@@ -663,16 +715,16 @@ async function confirmAction() {
663
715
  <div v-if="expandedStep === i" class="px-3 pb-3 space-y-2">
664
716
  <template v-if="stepsWithPayloads">
665
717
  <div class="space-y-1">
666
- <div class="text-[10px] uppercase tracking-wide text-muted-foreground">Input</div>
718
+ <div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.steps.field.input") }}</div>
667
719
  <pre class="rounded-md bg-muted/40 p-3 text-xs overflow-x-auto max-h-40">{{ st.input === void 0 ? "\u2014" : json(st.input) }}</pre>
668
720
  </div>
669
721
  <div class="space-y-1">
670
- <div class="text-[10px] uppercase tracking-wide text-muted-foreground">Output</div>
722
+ <div class="text-[10px] uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.steps.field.output") }}</div>
671
723
  <pre class="rounded-md bg-muted/40 p-3 text-xs overflow-x-auto max-h-40">{{ st.output === void 0 ? "\u2014" : json(st.output) }}</pre>
672
724
  </div>
673
725
  </template>
674
726
  <p v-else class="text-xs text-muted-foreground">
675
- Enable “Show payloads” to load this step's input and output.
727
+ {{ t("integration.runs.detail.steps.payloadHint") }}
676
728
  </p>
677
729
  </div>
678
730
  </li>
@@ -683,14 +735,14 @@ async function confirmAction() {
683
735
  opens the underlying events in a lightbox. -->
684
736
  <section v-if="hasTemporal" class="rounded-md border border-black/[0.06] bg-white p-4 space-y-3">
685
737
  <div class="flex items-center gap-2 border-b border-black/[0.06] pb-2">
686
- <h4 class="font-mono text-xs uppercase tracking-wide text-muted-foreground">History</h4>
738
+ <h4 class="font-mono text-xs uppercase tracking-wide text-muted-foreground">{{ t("integration.runs.detail.history.title") }}</h4>
687
739
  </div>
688
740
 
689
741
  <p v-if="historyError" class="text-rose-700">{{ historyError }}</p>
690
742
  <div v-else-if="historyLoading && !timeline" class="flex items-center gap-2 text-muted-foreground">
691
- <RefreshIcon :size="14" class="animate-spin" /> Loading history…
743
+ <RefreshIcon :size="14" class="animate-spin" /> {{ t("integration.runs.detail.history.loading") }}
692
744
  </div>
693
- <p v-else-if="!timeline" class="text-muted-foreground">No history events.</p>
745
+ <p v-else-if="!timeline" class="text-muted-foreground">{{ t("integration.runs.detail.history.empty") }}</p>
694
746
  <div v-else class="rounded-md border border-black/[0.05] bg-slate-50/60 p-2">
695
747
  <div class="relative">
696
748
  <div class="pointer-events-none absolute inset-y-0 left-1/4 border-l border-black/[0.05]" />
@@ -735,7 +787,7 @@ async function confirmAction() {
735
787
  </section>
736
788
 
737
789
  <p v-if="!hasTemporal" class="text-muted-foreground">
738
- Run has not been handed off to Temporal yet.
790
+ {{ t("integration.runs.detail.notHandedOff") }}
739
791
  </p>
740
792
  </template>
741
793
 
@@ -1,13 +1,16 @@
1
1
  <script setup>
2
- import { computed, ref, watch } from "vue";
3
- import { Paperclip, TrashBinMinimalistic as Trash2 } from "#solar-icons";
2
+ import { computed, ref, useId, watch } from "vue";
3
+ import { Paperclip, TrashBinMinimalistic as TrashBin } from "#solar-icons";
4
4
  import { Input, Switch, Textarea } from "@revenexx/studio";
5
5
  import { buildPayload, payloadFieldsFromBlob, splitPayload } from "../../data/payloadForm";
6
6
  const props = defineProps({
7
7
  blob: { type: Object, required: true }
8
8
  });
9
9
  const model = defineModel({ type: String, ...{ default: "{}" } });
10
+ const t = useIntegrationsT();
10
11
  const fields = computed(() => payloadFieldsFromBlob(props.blob));
12
+ const uid = useId();
13
+ const fieldId = (path) => `${uid}-${path}`;
11
14
  const textValues = ref({});
12
15
  const files = ref({});
13
16
  const extras = ref({});
@@ -45,7 +48,7 @@ function toggleExpert(on) {
45
48
  return;
46
49
  }
47
50
  if (!hydrateForm()) {
48
- parseNote.value = "The JSON does not parse \u2014 fix it before switching back to the form.";
51
+ parseNote.value = t("integration.runs.payload.parseError");
49
52
  return;
50
53
  }
51
54
  parseNote.value = null;
@@ -94,19 +97,26 @@ function prettyBytes(n) {
94
97
  <div class="space-y-2">
95
98
  <div v-if="fields.length" class="flex items-center justify-between">
96
99
  <span class="text-xs font-medium text-muted-foreground">
97
- {{ expert ? "Trigger payload (JSON)" : "Trigger payload" }}
100
+ {{ expert ? t("integration.runs.payload.label.json") : t("integration.runs.payload.label.form") }}
98
101
  </span>
102
+ <!-- The Switch is a `button role="switch"`, and HTML-AAM does not name a
103
+ button from a wrapping <label> — Chromium does, other engines need
104
+ not. The explicit name is what the promise rests on. -->
99
105
  <label class="flex items-center gap-1.5 text-xs text-muted-foreground">
100
- <Switch :model-value="expert" @update:model-value="toggleExpert" />
101
- Expert (JSON)
106
+ <Switch :aria-label="t('integration.runs.payload.expertToggle.label')" :model-value="expert" @update:model-value="toggleExpert" />
107
+ {{ t("integration.runs.payload.expertToggle.label") }}
102
108
  </label>
103
109
  </div>
104
- <label v-else class="text-xs font-medium text-muted-foreground">Trigger payload (JSON)</label>
110
+ <span v-else class="text-xs font-medium text-muted-foreground">{{ t("integration.runs.payload.label.json") }}</span>
105
111
 
106
112
  <!-- ── Form mode: fields derived from the blob's payload references ──── -->
107
113
  <div v-if="!expert && fields.length" class="space-y-3">
108
114
  <div v-for="field in fields" :key="`${field.kind}:${field.path}`" class="space-y-1">
109
- <label class="text-xs font-medium">{{ field.label }}</label>
115
+ <!-- A file field has no control to point at once a file is attached —
116
+ the drop zone is replaced by the file's own row — so there the
117
+ caption stays a caption and the controls name themselves. -->
118
+ <label v-if="field.kind === 'file'" class="text-xs font-medium">{{ field.label }}</label>
119
+ <label v-else :for="fieldId(field.path)" class="text-xs font-medium">{{ field.label }}</label>
110
120
 
111
121
  <template v-if="field.kind === 'file'">
112
122
  <div
@@ -118,9 +128,10 @@ function prettyBytes(n) {
118
128
  @drop.prevent="onDrop(field.path, $event)"
119
129
  >
120
130
  <Paperclip :size="14" />
121
- Drop a file here or click to choose
131
+ {{ t("integration.runs.payload.file.dropHint") }}
122
132
  <input
123
133
  type="file"
134
+ :aria-label="field.label"
124
135
  class="absolute inset-0 cursor-pointer opacity-0"
125
136
  @change="onPick(field.path, $event)"
126
137
  >
@@ -132,19 +143,21 @@ function prettyBytes(n) {
132
143
  <button
133
144
  type="button"
134
145
  class="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition hover:bg-muted/60 hover:text-rose-600"
135
- title="Remove file"
146
+ :title="t('integration.runs.payload.file.remove.title')"
147
+ :aria-label="t('integration.runs.payload.file.remove.aria', { field: field.label })"
136
148
  @click="detach(field.path)"
137
149
  >
138
- <Trash2 :size="13" />
150
+ <TrashBin :size="13" />
139
151
  </button>
140
152
  </div>
141
153
  <p v-if="files[field.path] && files[field.path].size > WARN_BYTES" class="text-xs text-amber-700">
142
- Large file — payloads beyond ~25 MB can exceed the workflow bus limits. Prefer the SFTP or IO routes for big files.
154
+ {{ t("integration.runs.payload.file.tooLarge") }}
143
155
  </p>
144
156
  </template>
145
157
 
146
158
  <template v-else>
147
159
  <Input
160
+ :id="fieldId(field.path)"
148
161
  :model-value="textValues[field.path] ?? ''"
149
162
  class="h-8 text-sm"
150
163
  :placeholder="`\${{ trigger.payload.${field.path} }}`"
@@ -155,7 +168,7 @@ function prettyBytes(n) {
155
168
  </template>
156
169
  </div>
157
170
  <p v-if="Object.keys(extras).length" class="text-xs text-muted-foreground">
158
- Additional payload keys from the expert view are kept: {{ Object.keys(extras).join(", ") }}
171
+ {{ t("integration.runs.payload.extrasNote", { keys: Object.keys(extras).join(", ") }) }}
159
172
  </p>
160
173
  </div>
161
174
 
@@ -163,6 +176,7 @@ function prettyBytes(n) {
163
176
  <template v-else>
164
177
  <Textarea
165
178
  v-model="model"
179
+ :aria-label="t('integration.runs.payload.label.json')"
166
180
  :rows="8"
167
181
  spellcheck="false"
168
182
  class="font-mono text-xs"