@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
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, reactive, ref, watch } from "vue";
2
+ import { computed, reactive, ref, useId, watch } from "vue";
3
3
  import {
4
4
  AddCircle as Plus,
5
5
  CheckCircle as Check,
@@ -23,31 +23,37 @@ import {
23
23
  SelectValue
24
24
  } from "@revenexx/studio";
25
25
  import { toast } from "vue-sonner";
26
+ import { REVENEXX_SOURCE_KIND_BY_SLUG } from "../../utils/revenexxCredentialSource";
27
+ import { cockpitSources } from "../../utils/cockpitSources";
26
28
  const props = defineProps({
27
29
  type: { type: [Object, null], required: true }
28
30
  });
29
31
  const emit = defineEmits(["update:active", "apply"]);
30
- const KIND_BY_SLUG = {
31
- "revenexx:sftp": "sftp",
32
- "revenexx:smtp": "smtp",
33
- "revenexx:api": "api"
34
- };
35
- const kind = computed(() => props.type ? KIND_BY_SLUG[props.type.slug] ?? null : null);
36
- const SWITCH_LABEL = {
37
- sftp: "Use revenexx SFTP Servers",
38
- smtp: "Use revenexx Mail Servers",
39
- api: "Use revenexx API Keys"
40
- };
41
- const NEW_LABEL = {
42
- sftp: "New SFTP account",
43
- smtp: "New mail credential",
44
- api: "New API key"
45
- };
46
- const { tenants, onlyTenantSlug, load: loadTenants } = useConsoleTenants();
47
- const { markets, activeCode, isGlobal, GLOBAL_MARKET_CODE, fetchMarkets } = useActiveMarket();
48
- const sftp = useConsoleSftp();
49
- const mail = useMailAdmin();
50
- const consoleKeys = useConsoleApiKeys();
32
+ const t = useIntegrationsT();
33
+ const kind = computed(() => props.type ? REVENEXX_SOURCE_KIND_BY_SLUG[props.type.slug] ?? null : null);
34
+ const uid = useId();
35
+ const SWITCH_LABEL = computed(() => ({
36
+ sftp: t("integration.credentials.revenexxSource.switch.sftp"),
37
+ smtp: t("integration.credentials.revenexxSource.switch.smtp"),
38
+ api: t("integration.credentials.revenexxSource.switch.api")
39
+ }));
40
+ const NEW_LABEL = computed(() => ({
41
+ sftp: t("integration.credentials.revenexxSource.new.sftp"),
42
+ smtp: t("integration.credentials.revenexxSource.new.smtp"),
43
+ api: t("integration.credentials.revenexxSource.new.api")
44
+ }));
45
+ const sources = cockpitSources();
46
+ const available = sources !== null;
47
+ const tenants = computed(() => sources?.tenants.tenants.value ?? []);
48
+ const onlyTenantSlug = computed(() => sources?.tenants.onlyTenantSlug.value ?? null);
49
+ const loadTenants = () => sources?.tenants.load() ?? Promise.resolve();
50
+ const markets = computed(() => sources?.market.markets.value ?? []);
51
+ const activeCode = computed(() => sources?.market.activeCode.value ?? "");
52
+ const fetchMarkets = () => sources?.market.fetchMarkets() ?? Promise.resolve();
53
+ const GLOBAL_MARKET_CODE = sources?.market.GLOBAL_MARKET_CODE ?? "__global__";
54
+ const sftp = sources?.sftp ?? null;
55
+ const mail = sources?.mail ?? null;
56
+ const consoleKeys = sources?.keys ?? null;
51
57
  const config = useRuntimeConfig();
52
58
  const sftpHost = config.public.sftpHost ?? "share.revenexx.com";
53
59
  const sftpPort = Number(config.public.sftpPort ?? 42);
@@ -67,12 +73,13 @@ watch(kind, () => {
67
73
  selectedId.value = "";
68
74
  applied.value = null;
69
75
  });
70
- const sftpOptions = computed(() => sftp.accounts.value.filter((a) => (a.is_active || a.status === "active") && a.login_username).map((a) => ({ value: String(a.id), label: a.login_username })));
76
+ const sftpOptions = computed(() => (sftp?.accounts.value ?? []).filter((a) => (a.is_active || a.status === "active") && a.login_username).map((a) => ({ value: String(a.id), label: a.login_username })));
71
77
  const marketOptions = computed(() => [
72
- { code: GLOBAL_MARKET_CODE, label: "Global (tenant baseline)" },
78
+ { code: GLOBAL_MARKET_CODE, label: t("integration.credentials.revenexxSource.market.global") },
73
79
  ...markets.value.map((m) => ({ code: m.code, label: `${m.name} (${m.code})` }))
74
80
  ]);
75
81
  async function ensureLists() {
82
+ if (!sftp || !mail) return;
76
83
  loadingList.value = true;
77
84
  try {
78
85
  await loadTenants();
@@ -86,7 +93,7 @@ async function ensureLists() {
86
93
  await fetchMarkets();
87
94
  }
88
95
  } catch (e) {
89
- toast.error(e?.message ?? "Could not load revenexx resources.");
96
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.loadResources"));
90
97
  } finally {
91
98
  loadingList.value = false;
92
99
  }
@@ -98,14 +105,15 @@ function marketSlug() {
98
105
  return marketCode.value === GLOBAL_MARKET_CODE ? "" : marketCode.value;
99
106
  }
100
107
  async function applySftp() {
101
- const t = tenant.value;
108
+ if (!sftp) return;
109
+ const tenantSlug = tenant.value;
102
110
  const acc = sftp.accounts.value.find((a) => String(a.id) === selectedId.value);
103
- if (!t || !acc) return;
111
+ if (!tenantSlug || !acc) return;
104
112
  applying.value = true;
105
113
  try {
106
- const creds = await sftp.revealPassword(t, acc.id);
114
+ const creds = await sftp.revealPassword(tenantSlug, acc.id);
107
115
  if (!creds) {
108
- toast.error("Account still provisioning \u2014 try again in a moment.");
116
+ toast.error(t("integration.credentials.revenexxSource.error.accountProvisioning"));
109
117
  return;
110
118
  }
111
119
  emit("apply", {
@@ -116,15 +124,20 @@ async function applySftp() {
116
124
  });
117
125
  applied.value = `${acc.login_username}@${sftpHost}:${sftpPort}`;
118
126
  } catch (e) {
119
- toast.error(e?.message ?? "Could not read the SFTP password.");
127
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.readSftpPassword"));
120
128
  } finally {
121
129
  applying.value = false;
122
130
  }
123
131
  }
124
132
  async function applySmtp() {
133
+ if (!mail) return;
125
134
  applying.value = true;
126
135
  try {
127
136
  const cred = await mail.getCredential(selectedId.value);
137
+ if (!cred) {
138
+ toast.error(t("integration.credentials.revenexxSource.error.mailCredentialGone"));
139
+ return;
140
+ }
128
141
  emit("apply", {
129
142
  host: mailSmtpHost,
130
143
  port: mailSmtpPort,
@@ -134,7 +147,7 @@ async function applySmtp() {
134
147
  });
135
148
  applied.value = `${cred.smtp_username}@${mailSmtpHost}:${mailSmtpPort}`;
136
149
  } catch (e) {
137
- toast.error(e?.message ?? "Could not read the mail credential.");
150
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.readMailCredential"));
138
151
  } finally {
139
152
  applying.value = false;
140
153
  }
@@ -165,39 +178,45 @@ function openCreate() {
165
178
  }
166
179
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
167
180
  async function createSftp() {
168
- const t = tenant.value;
169
- if (!t) {
170
- toast.error("No tenant selected.");
181
+ if (!sftp) return;
182
+ const tenantSlug = tenant.value;
183
+ if (!tenantSlug) {
184
+ toast.error(t("integration.credentials.revenexxSource.error.noTenant"));
171
185
  return;
172
186
  }
173
187
  creating.value = true;
174
188
  try {
175
- let acc = await sftp.create(t, createName.value.trim() || void 0);
189
+ let acc = await sftp.create(tenantSlug, createName.value.trim() || void 0);
176
190
  for (let i = 0; i < 24 && !(acc.is_active || acc.status === "active"); i++) {
177
- provisioningNote.value = "Provisioning the account\u2026";
191
+ provisioningNote.value = t("integration.credentials.revenexxSource.status.provisioning");
178
192
  await sleep(2500);
179
- acc = await sftp.refresh(t, acc.id);
180
- if (acc.status === "failed") throw new Error(acc.error_message || "Provisioning failed.");
193
+ acc = await sftp.refresh(tenantSlug, acc.id);
194
+ if (acc.status === "failed") throw new Error(acc.error_message || t("integration.credentials.revenexxSource.error.provisioningFailed"));
181
195
  }
182
196
  if (!(acc.is_active || acc.status === "active")) {
183
- throw new Error("Still provisioning \u2014 pick it from the list once it turns active.");
197
+ throw new Error(t("integration.credentials.revenexxSource.error.stillProvisioning"));
184
198
  }
185
- await sftp.load(t);
199
+ await sftp.load(tenantSlug);
186
200
  selectedId.value = String(acc.id);
187
201
  await applySftp();
188
202
  createOpen.value = false;
189
- toast.success("SFTP account created");
203
+ toast.success(t("integration.credentials.revenexxSource.toast.sftpCreated"));
190
204
  } catch (e) {
191
- toast.error(e?.message ?? "Could not create the SFTP account.");
205
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createSftp"));
192
206
  } finally {
193
207
  creating.value = false;
194
208
  provisioningNote.value = "";
195
209
  }
196
210
  }
197
211
  async function createSmtp() {
212
+ if (!mail) return;
198
213
  creating.value = true;
199
214
  try {
200
215
  const cred = await mail.createCredential({ name: createName.value.trim() || "SMTP", type: "SMTP" });
216
+ if (!cred) {
217
+ toast.error(t("integration.credentials.revenexxSource.error.mailCredentialNotReturned"));
218
+ return;
219
+ }
201
220
  emit("apply", {
202
221
  host: mailSmtpHost,
203
222
  port: mailSmtpPort,
@@ -209,24 +228,25 @@ async function createSmtp() {
209
228
  smtpCreds.value = [{ uuid: cred.uuid, name: cred.name }, ...smtpCreds.value];
210
229
  selectedId.value = cred.uuid;
211
230
  createOpen.value = false;
212
- toast.success("Mail credential created");
231
+ toast.success(t("integration.credentials.revenexxSource.toast.smtpCreated"));
213
232
  } catch (e) {
214
- toast.error(e?.message ?? "Could not create the mail credential.");
233
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createSmtp"));
215
234
  } finally {
216
235
  creating.value = false;
217
236
  }
218
237
  }
219
238
  async function createApiKey() {
239
+ if (!consoleKeys) return;
220
240
  creating.value = true;
221
241
  try {
222
242
  const { secret } = await consoleKeys.create(createName.value.trim() || "Integrations key", ["*"]);
223
243
  emit("apply", { apiKey: secret, tenant: tenant.value ?? "", market: marketSlug() });
224
244
  apiKeyReady.value = true;
225
- applied.value = `Key for ${tenant.value}${marketSlug() ? ` \xB7 ${marketSlug()}` : " \xB7 global"}`;
245
+ applied.value = marketSlug() ? t("integration.credentials.revenexxSource.status.keyForMarket", { tenant: tenant.value, market: marketSlug() }) : t("integration.credentials.revenexxSource.status.keyForGlobal", { tenant: tenant.value });
226
246
  createOpen.value = false;
227
- toast.success("API key created");
247
+ toast.success(t("integration.credentials.revenexxSource.toast.apiKeyCreated"));
228
248
  } catch (e) {
229
- toast.error(e?.message ?? "Could not create the API key.");
249
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createApiKey"));
230
250
  } finally {
231
251
  creating.value = false;
232
252
  }
@@ -244,24 +264,41 @@ function submitCreate() {
244
264
  <div class="flex items-center justify-between gap-3">
245
265
  <div class="min-w-0">
246
266
  <div class="text-sm font-medium">{{ SWITCH_LABEL[kind] }}</div>
267
+ <!-- Two sentences, one per state. Saying WHERE the sources are missing is
268
+ the point: without it an operator in the dev studio or another host
269
+ reads a dead switch and cannot tell a broken feature from one that
270
+ belongs somewhere else. The manual fields stay on screen either way. -->
247
271
  <p class="text-xs text-muted-foreground">
248
- Reuse an existing revenexx resource instead of typing access data by hand.
272
+ <template v-if="available">
273
+ {{ t("integration.credentials.revenexxSource.hint.available") }}
274
+ </template>
275
+ <template v-else>
276
+ {{ t("integration.credentials.revenexxSource.hint.unavailable") }}
277
+ </template>
249
278
  </p>
250
279
  </div>
251
- <Switch :model-value="active" @update:model-value="(v) => active = v" />
280
+ <!-- Disabled rather than hidden: a control that vanishes leaves nothing to
281
+ read the explanation from, and the operator cannot tell whether the
282
+ feature exists at all. -->
283
+ <Switch
284
+ :aria-label="SWITCH_LABEL[kind]"
285
+ :disabled="!available"
286
+ :model-value="active"
287
+ @update:model-value="(v) => active = v"
288
+ />
252
289
  </div>
253
290
 
254
291
  <div v-if="active" class="mt-3 space-y-3">
255
292
  <!-- Tenant (preselected) -->
256
293
  <div class="space-y-1.5">
257
- <Label>Tenant</Label>
294
+ <Label :for="`${uid}-tenant`">{{ t("integration.credentials.revenexxSource.field.tenant.label") }}</Label>
258
295
  <Select :model-value="tenant ?? ''" @update:model-value="(v) => tenant = v">
259
- <SelectTrigger class="bg-white">
260
- <SelectValue placeholder="Select tenant" />
296
+ <SelectTrigger :id="`${uid}-tenant`" class="bg-white">
297
+ <SelectValue :placeholder="t('integration.credentials.revenexxSource.field.tenant.placeholder')" />
261
298
  </SelectTrigger>
262
299
  <SelectContent>
263
- <SelectItem v-for="t in tenants" :key="t.slug" :value="t.slug">
264
- {{ t.name }} ({{ t.slug }})
300
+ <SelectItem v-for="tn in tenants" :key="tn.slug" :value="tn.slug">
301
+ {{ tn.name }} ({{ tn.slug }})
265
302
  </SelectItem>
266
303
  </SelectContent>
267
304
  </Select>
@@ -269,10 +306,10 @@ function submitCreate() {
269
306
 
270
307
  <!-- Market scope — only the Revenexx API credential is market-scoped -->
271
308
  <div v-if="kind === 'api'" class="space-y-1.5">
272
- <Label>Market</Label>
309
+ <Label :for="`${uid}-market`">{{ t("integration.credentials.revenexxSource.field.market.label") }}</Label>
273
310
  <Select :model-value="marketCode" @update:model-value="(v) => marketCode = v">
274
- <SelectTrigger class="bg-white">
275
- <SelectValue placeholder="Select market" />
311
+ <SelectTrigger :id="`${uid}-market`" class="bg-white">
312
+ <SelectValue :placeholder="t('integration.credentials.revenexxSource.field.market.placeholder')" />
276
313
  </SelectTrigger>
277
314
  <SelectContent>
278
315
  <SelectItem v-for="m in marketOptions" :key="m.code" :value="m.code">
@@ -284,11 +321,11 @@ function submitCreate() {
284
321
 
285
322
  <!-- Resource select (SFTP / Mail) -->
286
323
  <div v-if="kind === 'sftp' || kind === 'smtp'" class="space-y-1.5">
287
- <Label>{{ kind === "sftp" ? "SFTP account" : "Mail credential" }}</Label>
324
+ <Label :for="`${uid}-resource`">{{ kind === "sftp" ? t("integration.credentials.revenexxSource.field.resource.sftpLabel") : t("integration.credentials.revenexxSource.field.resource.mailLabel") }}</Label>
288
325
  <div class="flex items-center gap-2">
289
326
  <Select :model-value="selectedId" class="flex-1" @update:model-value="(v) => onSelect(v)">
290
- <SelectTrigger class="bg-white">
291
- <SelectValue :placeholder="loadingList ? 'Loading\u2026' : 'Select one'" />
327
+ <SelectTrigger :id="`${uid}-resource`" class="bg-white">
328
+ <SelectValue :placeholder="loadingList ? t('integration.common.state.loading') : t('integration.credentials.revenexxSource.field.resource.placeholder')" />
292
329
  </SelectTrigger>
293
330
  <SelectContent>
294
331
  <template v-if="kind === 'sftp'">
@@ -301,7 +338,7 @@ function submitCreate() {
301
338
  </Select>
302
339
  <Button variant="outline" size="sm" class="shrink-0 bg-white" :disabled="!tenant" @click="openCreate">
303
340
  <Plus :size="14" class="mr-1" />
304
- New
341
+ {{ t("integration.credentials.revenexxSource.action.new") }}
305
342
  </Button>
306
343
  </div>
307
344
  </div>
@@ -310,7 +347,7 @@ function submitCreate() {
310
347
  <div v-else-if="kind === 'api'">
311
348
  <Button variant="outline" size="sm" class="bg-white" :disabled="!tenant" @click="openCreate">
312
349
  <Plus :size="14" class="mr-1" />
313
- Create new API key
350
+ {{ t("integration.credentials.revenexxSource.action.createApiKey") }}
314
351
  </Button>
315
352
  </div>
316
353
 
@@ -318,7 +355,7 @@ function submitCreate() {
318
355
  <div v-if="applied || applying" class="flex items-center gap-2 rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
319
356
  <Loader2 v-if="applying" :size="14" class="animate-spin" />
320
357
  <Check v-else :size="14" />
321
- <span class="truncate">{{ applying ? "Copying access data\u2026" : `Using ${applied}` }}</span>
358
+ <span class="truncate">{{ applying ? t("integration.credentials.revenexxSource.status.copying") : t("integration.credentials.revenexxSource.status.using", { value: applied }) }}</span>
322
359
  </div>
323
360
  </div>
324
361
 
@@ -326,36 +363,36 @@ function submitCreate() {
326
363
  <Dialog v-model:open="createOpen">
327
364
  <DialogContent class="sm:max-w-md">
328
365
  <DialogHeader>
329
- <DialogTitle>{{ kind ? NEW_LABEL[kind] : "New" }}</DialogTitle>
366
+ <DialogTitle>{{ kind ? NEW_LABEL[kind] : t("integration.credentials.revenexxSource.new.fallback") }}</DialogTitle>
330
367
  <DialogDescription class="text-sm">
331
368
  <template v-if="kind === 'api'">
332
- A full-access key is minted for <b>{{ tenant }}</b> and copied straight into this credential.
369
+ {{ t("integration.credentials.revenexxSource.createDialog.description.api", { tenant }) }}
333
370
  </template>
334
371
  <template v-else-if="kind === 'sftp'">
335
- Reserves a new SFTP account for <b>{{ tenant }}</b> and fills its login here.
372
+ {{ t("integration.credentials.revenexxSource.createDialog.description.sftp", { tenant }) }}
336
373
  </template>
337
374
  <template v-else>
338
- Creates a new SMTP credential and fills its login here.
375
+ {{ t("integration.credentials.revenexxSource.createDialog.description.smtp") }}
339
376
  </template>
340
377
  </DialogDescription>
341
378
  </DialogHeader>
342
379
 
343
380
  <div class="space-y-1.5 py-1">
344
- <Label for="rvnxx-new-name">{{ kind === "sftp" ? "Label" : "Name" }}</Label>
381
+ <Label for="rvnxx-new-name">{{ kind === "sftp" ? t("integration.credentials.revenexxSource.createDialog.field.label") : t("integration.credentials.revenexxSource.createDialog.field.name") }}</Label>
345
382
  <Input
346
383
  id="rvnxx-new-name"
347
384
  v-model="createName"
348
385
  class="bg-white"
349
- :placeholder="kind === 'sftp' ? 'e.g. imports' : 'e.g. Production'"
386
+ :placeholder="kind === 'sftp' ? t('integration.credentials.revenexxSource.createDialog.field.placeholder.sftp') : t('integration.credentials.revenexxSource.createDialog.field.placeholder.default')"
350
387
  />
351
388
  <p v-if="provisioningNote" class="text-xs text-muted-foreground">{{ provisioningNote }}</p>
352
389
  </div>
353
390
 
354
391
  <DialogFooter>
355
- <Button variant="ghost" :disabled="creating" @click="createOpen = false">Cancel</Button>
392
+ <Button variant="ghost" :disabled="creating" @click="createOpen = false">{{ t("integration.common.action.cancel") }}</Button>
356
393
  <Button :disabled="creating" @click="submitCreate">
357
394
  <Loader2 v-if="creating" :size="14" class="mr-1 animate-spin" />
358
- Create
395
+ {{ t("integration.credentials.revenexxSource.action.create") }}
359
396
  </Button>
360
397
  </DialogFooter>
361
398
  </DialogContent>
@@ -2,30 +2,26 @@
2
2
  import { computed, ref } from "vue";
3
3
  import { Magnifier as Search } from "#solar-icons";
4
4
  import { Input } from "@revenexx/studio";
5
+ import { resolveLocalized } from "../../utils/localize";
5
6
  const props = defineProps({
6
7
  types: { type: Array, required: true }
7
8
  });
8
9
  const emit = defineEmits(["pick"]);
10
+ const t = useIntegrationsT();
9
11
  const query = ref("");
10
- function loc(v) {
11
- if (!v) return "";
12
- if (typeof v === "string") return v;
13
- const o = v;
14
- return o.en ?? o.de ?? Object.values(o)[0] ?? "";
15
- }
16
12
  function authKindLabel(k) {
17
13
  switch (k) {
18
14
  case "api-key":
19
- return "API Key";
15
+ return t("integration.credentials.shared.authKind.apiKey");
20
16
  case "oauth2-authcode":
21
17
  case "oauth2-client-credentials":
22
- return "OAuth2";
18
+ return t("integration.credentials.shared.authKind.oauth2");
23
19
  case "basic-auth":
24
- return "Basic Auth";
20
+ return t("integration.credentials.shared.authKind.basicAuth");
25
21
  case "static":
26
- return "Static";
22
+ return t("integration.credentials.shared.authKind.static");
27
23
  case "secret":
28
- return "Secret";
24
+ return t("integration.credentials.shared.authKind.secret");
29
25
  default:
30
26
  return k ?? "";
31
27
  }
@@ -33,7 +29,7 @@ function authKindLabel(k) {
33
29
  const filtered = computed(() => {
34
30
  const q = query.value.trim().toLowerCase();
35
31
  if (!q) return props.types;
36
- return props.types.filter((t) => loc(t.name).toLowerCase().includes(q));
32
+ return props.types.filter((t2) => resolveLocalized(t2.name).toLowerCase().includes(q));
37
33
  });
38
34
  </script>
39
35
 
@@ -43,15 +39,16 @@ const filtered = computed(() => {
43
39
  <Search :size="15" class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
44
40
  <Input
45
41
  v-model="query"
42
+ :aria-label="t('integration.credentials.typePicker.search.aria')"
46
43
  class="bg-white pl-9"
47
- placeholder="Search for an app…"
44
+ :placeholder="t('integration.credentials.typePicker.search.placeholder')"
48
45
  autofocus
49
46
  />
50
47
  </div>
51
48
 
52
49
  <div class="mt-2 max-h-80 overflow-y-auto -mx-1 px-1">
53
50
  <p v-if="!filtered.length" class="px-2 py-6 text-center text-sm text-muted-foreground">
54
- No credential type matches “{{ query }}”.
51
+ {{ t("integration.credentials.typePicker.empty", { query }) }}
55
52
  </p>
56
53
  <button
57
54
  v-for="t in filtered"
@@ -62,7 +59,7 @@ const filtered = computed(() => {
62
59
  >
63
60
  <IntegrationsCredentialLogo :type="t" :size="16" />
64
61
  <div class="min-w-0 flex-1 truncate text-sm">
65
- <span class="font-medium">{{ loc(t.name) || t.slug }}</span>
62
+ <span class="font-medium">{{ resolveLocalized(t.name) || t.slug }}</span>
66
63
  <span class="ml-1.5 text-muted-foreground">({{ authKindLabel(t.auth_kind) }})</span>
67
64
  </div>
68
65
  </button>
@@ -1,11 +1,12 @@
1
1
  <script setup>
2
2
  import { onMounted, ref, watch } from "vue";
3
- import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as Trash2 } from "#solar-icons";
3
+ import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as TrashBin } from "#solar-icons";
4
4
  import { Badge, Button } from "@revenexx/studio";
5
5
  import { toast } from "vue-sonner";
6
6
  const props = defineProps({
7
7
  workflowId: { type: Number, required: true }
8
8
  });
9
+ const t = useIntegrationsT();
9
10
  const { list, replay, discard } = useIntegrationsDeadLetters();
10
11
  const PER_PAGE = 25;
11
12
  const items = ref([]);
@@ -25,7 +26,7 @@ async function load(p = page.value) {
25
26
  lastPage.value = res.lastPage;
26
27
  page.value = res.page;
27
28
  } catch (e) {
28
- error.value = e?.message ?? "Could not load dead letters.";
29
+ error.value = e?.message ?? t("integration.deadLetters.error.load");
29
30
  } finally {
30
31
  loading.value = false;
31
32
  }
@@ -41,10 +42,10 @@ async function onReplay(dl) {
41
42
  try {
42
43
  const res = await replay(dl.id);
43
44
  const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
44
- toast.success(newId ? `Replayed \u2014 started run #${newId}` : "Replayed");
45
+ toast.success(newId ? t("integration.deadLetters.toast.replay.started", { id: newId }) : t("integration.deadLetters.toast.replay.done"));
45
46
  await load();
46
47
  } catch (e) {
47
- toast.error(e?.message ?? "Could not replay.");
48
+ toast.error(e?.message ?? t("integration.deadLetters.toast.replay.error"));
48
49
  } finally {
49
50
  busyId.value = null;
50
51
  }
@@ -53,10 +54,10 @@ async function onDiscard(dl) {
53
54
  busyId.value = dl.id;
54
55
  try {
55
56
  await discard(dl.id);
56
- toast.success("Discarded");
57
+ toast.success(t("integration.deadLetters.toast.discard.done"));
57
58
  await load();
58
59
  } catch (e) {
59
- toast.error(e?.message ?? "Could not discard.");
60
+ toast.error(e?.message ?? t("integration.deadLetters.toast.discard.error"));
60
61
  } finally {
61
62
  busyId.value = null;
62
63
  }
@@ -80,14 +81,14 @@ function fmt(iso) {
80
81
  <div class="space-y-3">
81
82
  <div class="flex items-center justify-between">
82
83
  <div>
83
- <h3 class="text-sm font-semibold">Missed runs</h3>
84
+ <h3 class="text-sm font-semibold">{{ t("integration.deadLetters.panel.title") }}</h3>
84
85
  <p class="mt-0.5 text-sm text-muted-foreground">
85
- Triggers that fired while this workflow couldn't run (bundle building or failed). Replay starts a fresh run from the captured payload.
86
+ {{ t("integration.deadLetters.panel.description") }}
86
87
  </p>
87
88
  </div>
88
89
  <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="loading" @click="() => load()">
89
90
  <Loader2 v-if="loading" :size="12" class="animate-spin" />
90
- Refresh
91
+ {{ t("integration.common.action.refresh") }}
91
92
  </Button>
92
93
  </div>
93
94
 
@@ -107,7 +108,7 @@ function fmt(iso) {
107
108
  </div>
108
109
  </div>
109
110
  <div v-else-if="!items.length" class="rounded-lg border border-black/[0.06] bg-white p-6 text-center text-sm text-muted-foreground">
110
- No missed runs. Every trigger firing made it into a run.
111
+ {{ t("integration.deadLetters.state.empty") }}
111
112
  </div>
112
113
 
113
114
  <div v-else class="rounded-lg border border-black/[0.06] bg-white divide-y divide-black/[0.04]">
@@ -118,7 +119,7 @@ function fmt(iso) {
118
119
  >
119
120
  <Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0 mt-0.5', statusClass(dl.status)]">{{ dl.status }}</Badge>
120
121
  <div class="min-w-0 flex-1">
121
- <div class="text-sm break-words">{{ dl.reason || "No reason recorded." }}</div>
122
+ <div class="text-sm break-words">{{ dl.reason || t("integration.deadLetters.reason.fallback") }}</div>
122
123
  <div class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground font-mono">
123
124
  <span>#{{ dl.id }}</span>
124
125
  <span v-if="dl.source">{{ dl.source }}</span>
@@ -130,21 +131,21 @@ function fmt(iso) {
130
131
  <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="busyId === dl.id" @click="onReplay(dl)">
131
132
  <Loader2 v-if="busyId === dl.id" :size="12" class="animate-spin" />
132
133
  <ReplayIcon v-else :size="12" />
133
- Replay
134
+ {{ t("integration.deadLetters.action.replay") }}
134
135
  </Button>
135
136
  <Button variant="ghost" size="sm" class="h-7 text-sm text-rose-600 hover:bg-rose-500/10" :disabled="busyId === dl.id" @click="onDiscard(dl)">
136
- <Trash2 :size="12" />
137
- Discard
137
+ <TrashBin :size="12" />
138
+ {{ t("integration.deadLetters.action.discard") }}
138
139
  </Button>
139
140
  </div>
140
141
  </div>
141
142
  </div>
142
143
 
143
144
  <div v-if="lastPage > 1" class="flex items-center justify-between text-sm text-muted-foreground">
144
- <span class="tabular-nums">Page {{ page }} / {{ lastPage }} · {{ total }} total</span>
145
+ <span class="tabular-nums">{{ t("integration.deadLetters.pagination.summary", { page, lastPage, total }) }}</span>
145
146
  <div class="flex gap-2">
146
- <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page <= 1 || loading" @click="goPage(page - 1)">← Newer</Button>
147
- <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page >= lastPage || loading" @click="goPage(page + 1)">Older →</Button>
147
+ <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page <= 1 || loading" @click="goPage(page - 1)">{{ t("integration.deadLetters.pagination.newer") }}</Button>
148
+ <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page >= lastPage || loading" @click="goPage(page + 1)">{{ t("integration.deadLetters.pagination.older") }}</Button>
148
149
  </div>
149
150
  </div>
150
151
  </div>
@@ -1,5 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  modelValue: string;
3
+ inputId?: string;
3
4
  };
4
5
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
6
  "update:modelValue": (value: string) => any;
@@ -2,9 +2,11 @@
2
2
  import { computed, onMounted, ref } from "vue";
3
3
  import { Input } from "@revenexx/studio";
4
4
  const props = defineProps({
5
- modelValue: { type: String, required: true }
5
+ modelValue: { type: String, required: true },
6
+ inputId: { type: String, required: false }
6
7
  });
7
8
  const emit = defineEmits(["update:modelValue"]);
9
+ const t = useIntegrationsT();
8
10
  const catalog = typeof useDefinedEvents === "function" ? useDefinedEvents() : { emits: ref([]), listens: ref([]), loading: ref(false), load: async () => {
9
11
  } };
10
12
  onMounted(() => {
@@ -14,8 +16,8 @@ const focused = ref(false);
14
16
  const options = computed(() => {
15
17
  const out = /* @__PURE__ */ new Map();
16
18
  for (const e of catalog.emits.value) out.set(e.topic, { topic: e.topic, source: e.app });
17
- for (const t of catalog.listens.value) {
18
- if (!out.has(t)) out.set(t, { topic: t, source: null });
19
+ for (const t2 of catalog.listens.value) {
20
+ if (!out.has(t2)) out.set(t2, { topic: t2, source: null });
19
21
  }
20
22
  return [...out.values()].sort((a, b) => a.topic.localeCompare(b.topic));
21
23
  });
@@ -34,6 +36,7 @@ function pick(topic) {
34
36
  <template>
35
37
  <div class="relative">
36
38
  <Input
39
+ :id="props.inputId"
37
40
  :model-value="modelValue"
38
41
  placeholder="customer.created"
39
42
  class="h-8 text-sm bg-white font-mono"
@@ -46,7 +49,7 @@ function pick(topic) {
46
49
  class="absolute left-0 right-0 top-full z-30 mt-1 max-h-56 overflow-y-auto rounded-md border border-black/[0.08] bg-white py-1 shadow-md"
47
50
  >
48
51
  <div v-if="catalog.loading.value && !filtered.length" class="px-2.5 py-1.5 text-sm text-muted-foreground">
49
- Loading event catalog…
52
+ {{ t("integration.canvas.eventCombobox.loading") }}
50
53
  </div>
51
54
  <button
52
55
  v-for="o in filtered"
@@ -1,5 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  modelValue: string;
3
+ inputId?: string;
3
4
  };
4
5
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
6
  "update:modelValue": (value: string) => any;
@@ -10,6 +10,10 @@ type __VLS_Props = {
10
10
  placeholder?: string;
11
11
  variables?: Record<string, WorkflowVariable>;
12
12
  upstreamOutputs?: UpstreamOutput[];
13
+ /** Draw the editor as at fault — the field above owns the reason. */
14
+ invalid?: boolean;
15
+ /** The setting's label, so the editable region has an accessible name. */
16
+ label?: string;
13
17
  };
14
18
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
15
19
  "update:modelValue": (value: string) => any;