@revenexx/studio-integrations 0.6.0 → 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 (110) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/README.md +31 -0
  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 +29 -28
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +38 -37
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +16 -15
  13. package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
  14. package/dist/runtime/components/Integrations/Layout.vue +28 -3
  15. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +15 -14
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +9 -8
  17. package/dist/runtime/components/Integrations/NodeConfigField.vue +43 -42
  18. package/dist/runtime/components/Integrations/NodeInspector.vue +225 -216
  19. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +1 -1
  20. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +3 -2
  21. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +1 -1
  22. package/dist/runtime/components/Integrations/NodePalette.vue +30 -29
  23. package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
  24. package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
  25. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  26. package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
  27. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  28. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  29. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  30. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  31. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  32. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  33. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  34. package/dist/runtime/components/Integrations/StickyNoteView.vue +37 -23
  35. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  36. package/dist/runtime/components/Integrations/TemplateCard.vue +16 -6
  37. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +10 -9
  38. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  39. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +36 -35
  40. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  41. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
  42. package/dist/runtime/components/Integrations/WorkflowEditor.vue +82 -79
  43. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +24 -23
  44. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
  45. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +19 -20
  46. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +52 -39
  47. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +20 -19
  48. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
  49. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +12 -9
  50. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +27 -26
  51. package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
  52. package/dist/runtime/composables/integrationsApi.js +4 -2
  53. package/dist/runtime/composables/useCredentialTypes.js +4 -2
  54. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -4
  55. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  56. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  57. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  58. package/dist/runtime/composables/useIntegrationsNodes.js +3 -1
  59. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  60. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  61. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  62. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  63. package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
  64. package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
  65. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  66. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  67. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  68. package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
  69. package/dist/runtime/composables/useIntegrationsTriggers.js +4 -2
  70. package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -3
  71. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  72. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  73. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  74. package/dist/runtime/data/integrationTemplates.d.ts +9 -0
  75. package/dist/runtime/data/integrationTemplates.js +7 -1
  76. package/dist/runtime/data/payloadForm.js +2 -1
  77. package/dist/runtime/data/workflowIconCatalog.js +2 -1
  78. package/dist/runtime/i18n/messages/de.json +1207 -0
  79. package/dist/runtime/i18n/messages/en.json +1207 -0
  80. package/dist/runtime/i18n/translate.d.ts +83 -0
  81. package/dist/runtime/i18n/translate.js +41 -0
  82. package/dist/runtime/pages/integrations/audit.vue +52 -34
  83. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
  84. package/dist/runtime/pages/integrations/credentials.vue +70 -52
  85. package/dist/runtime/pages/integrations/index.vue +50 -56
  86. package/dist/runtime/pages/integrations/runs.vue +33 -21
  87. package/dist/runtime/pages/integrations/schedules.vue +49 -28
  88. package/dist/runtime/pages/integrations/secrets.vue +34 -34
  89. package/dist/runtime/pages/integrations/templates/[slug].vue +77 -68
  90. package/dist/runtime/pages/integrations/templates/index.vue +35 -27
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +164 -99
  92. package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
  93. package/dist/runtime/plugins/locale.d.ts +10 -0
  94. package/dist/runtime/plugins/locale.js +6 -0
  95. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  96. package/dist/runtime/plugins/realtime.client.js +4 -0
  97. package/dist/runtime/utils/credentialState.d.ts +24 -1
  98. package/dist/runtime/utils/credentialState.js +26 -11
  99. package/dist/runtime/utils/localize.d.ts +18 -5
  100. package/dist/runtime/utils/localize.js +32 -9
  101. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  102. package/dist/runtime/utils/pollBudget.js +17 -0
  103. package/dist/runtime/utils/schedule.d.ts +0 -25
  104. package/dist/runtime/utils/schedule.js +37 -25
  105. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  106. package/dist/runtime/utils/statusStyle.js +9 -1
  107. package/dist/runtime/utils/validationFindings.d.ts +2 -2
  108. package/dist/runtime/utils/validationFindings.js +4 -3
  109. package/dist/types.d.mts +2 -6
  110. package/package.json +3 -2
@@ -5,7 +5,7 @@ import {
5
5
  CloseCircle as XCircle,
6
6
  RefreshCircle as Loader2,
7
7
  TestTube as TestIcon,
8
- TrashBinTrash as Trash2
8
+ TrashBinMinimalistic as TrashBin
9
9
  } from "#solar-icons";
10
10
  import {
11
11
  AlertDialog,
@@ -40,12 +40,13 @@ const props = defineProps({
40
40
  type: { type: [Object, null], required: true }
41
41
  });
42
42
  const emit = defineEmits(["update:open", "changed"]);
43
+ const t = useIntegrationsT();
43
44
  const { update, remove, test, oauthAuthorizeUrl, load } = useIntegrationsCredentials();
44
45
  const tab = ref("credentials");
45
- const tabs = [
46
- { key: "credentials", label: "Credentials" },
47
- { key: "details", label: "Details" }
48
- ];
46
+ const tabs = computed(() => [
47
+ { key: "credentials", label: t("integration.credentials.detail.tab.credentials") },
48
+ { key: "details", label: t("integration.credentials.detail.tab.details") }
49
+ ]);
49
50
  const form = reactive({ name: "", config: {} });
50
51
  const sourceActive = ref(false);
51
52
  const saving = ref(false);
@@ -65,8 +66,8 @@ const connecting = ref(false);
65
66
  const typeName = computed(() => resolveLocalized(props.type?.name) || props.credential?.credential_type_slug || "");
66
67
  const isOAuthAuthcode = computed(() => props.type?.auth_kind === "oauth2-authcode");
67
68
  const state = computed(() => props.credential ? credentialState(props.credential) : null);
68
- function fieldsOf(t) {
69
- return t?.fields ?? [];
69
+ function fieldsOf(t2) {
70
+ return t2?.fields ?? [];
70
71
  }
71
72
  function formatDateTime(iso) {
72
73
  if (!iso) return "\u2014";
@@ -118,10 +119,10 @@ async function save() {
118
119
  await update(props.credential.id, { name: form.name.trim(), config: buildConfigPayload() });
119
120
  emit("changed");
120
121
  emit("update:open", false);
121
- toast.success("Credential updated");
122
+ toast.success(t("integration.credentials.toast.updated"));
122
123
  } catch (e) {
123
124
  fieldErrors.value = apiFieldErrors(e);
124
- saveError.value = refusalReason(e, "Could not save credential.");
125
+ saveError.value = refusalReason(e, t("integration.credentials.error.save"));
125
126
  toast.error(saveError.value);
126
127
  } finally {
127
128
  saving.value = false;
@@ -134,11 +135,11 @@ async function destroy() {
134
135
  deleting.value = true;
135
136
  try {
136
137
  await remove(props.credential.id);
137
- toast.success("Credential deleted");
138
+ toast.success(t("integration.credentials.toast.deleted"));
138
139
  emit("changed");
139
140
  emit("update:open", false);
140
141
  } catch (e) {
141
- toast.error(e?.message ?? "Could not delete credential.");
142
+ toast.error(e?.message ?? t("integration.credentials.error.delete"));
142
143
  } finally {
143
144
  deleting.value = false;
144
145
  }
@@ -154,7 +155,7 @@ async function connect() {
154
155
  window.removeEventListener("message", onMessage);
155
156
  await load(true);
156
157
  emit("changed");
157
- toast.success("Connected");
158
+ toast.success(t("integration.credentials.toast.connected"));
158
159
  }
159
160
  };
160
161
  window.addEventListener("message", onMessage);
@@ -167,7 +168,7 @@ async function connect() {
167
168
  }
168
169
  }, 1e3);
169
170
  } catch (e) {
170
- toast.error(e?.message ?? "Could not start OAuth connect.");
171
+ toast.error(e?.message ?? t("integration.credentials.error.oauthConnect"));
171
172
  } finally {
172
173
  connecting.value = false;
173
174
  }
@@ -210,21 +211,21 @@ async function connect() {
210
211
  @click="connect"
211
212
  >
212
213
  <Loader2 v-if="connecting" :size="13" class="mr-1 animate-spin" />
213
- Reconnect
214
+ {{ t("integration.credentials.detail.action.reconnect") }}
214
215
  </Button>
215
216
  </div>
216
217
  <p v-if="state.key === 'failed' && state.message" class="mt-1 text-sm break-words opacity-90">
217
218
  {{ state.message }}
218
219
  </p>
219
220
  <p v-else-if="state.key === 'untested'" class="mt-1 text-sm opacity-80">
220
- Run a test to verify these credentials actually connect.
221
+ {{ t("integration.credentials.detail.state.untestedHint") }}
221
222
  </p>
222
223
  </div>
223
224
 
224
225
  <!-- Editable fields -->
225
226
  <div class="space-y-3">
226
227
  <div class="space-y-1.5">
227
- <Label for="detail-cred-name">Name</Label>
228
+ <Label for="detail-cred-name">{{ t("integration.credentials.shared.field.name.label") }}</Label>
228
229
  <Input
229
230
  id="detail-cred-name"
230
231
  v-model="form.name"
@@ -262,7 +263,7 @@ async function connect() {
262
263
  :model-value="form.config[f.key] ?? ''"
263
264
  type="password"
264
265
  autocomplete="new-password"
265
- placeholder="Leave blank to keep current"
266
+ :placeholder="t('integration.credentials.shared.field.secret.placeholder')"
266
267
  class="bg-white"
267
268
  v-bind="configFieldAria(f.key)"
268
269
  @update:model-value="(v) => form.config[f.key] = v"
@@ -285,7 +286,7 @@ async function connect() {
285
286
  @update:model-value="(v) => form.config[f.key] = v"
286
287
  >
287
288
  <SelectTrigger :id="`detail-field-${f.key}`" class="bg-white" v-bind="configFieldAria(f.key)">
288
- <SelectValue placeholder="Pick one" />
289
+ <SelectValue :placeholder="t('integration.credentials.shared.field.select.placeholder')" />
289
290
  </SelectTrigger>
290
291
  <SelectContent>
291
292
  <SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
@@ -313,7 +314,7 @@ async function connect() {
313
314
  class="rounded-md px-3 py-2 text-sm"
314
315
  :class="liveTest.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
315
316
  >
316
- {{ liveTest.ok ? "Connection ok." : liveTest.error ?? "Connection failed." }}
317
+ {{ liveTest.ok ? t("integration.credentials.shared.test.ok") : liveTest.error ?? t("integration.credentials.shared.test.failed") }}
317
318
  </div>
318
319
 
319
320
  <!-- The save was refused. Same rule as the create dialog: stated here, and at
@@ -327,11 +328,11 @@ async function connect() {
327
328
  <Button variant="outline" :disabled="testing || saving" @click="runTest">
328
329
  <Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
329
330
  <TestIcon v-else :size="14" class="mr-1" />
330
- Connect
331
+ {{ t("integration.credentials.shared.action.testConnection") }}
331
332
  </Button>
332
333
  <Button :disabled="saving || !form.name.trim()" @click="save">
333
334
  <Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
334
- Save
335
+ {{ t("integration.common.action.save") }}
335
336
  </Button>
336
337
  </div>
337
338
  </div>
@@ -341,37 +342,37 @@ async function connect() {
341
342
  <!-- Tab 2: Details -->
342
343
  <div v-else class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
343
344
  <div class="grid grid-cols-[9rem_1fr] gap-x-4 gap-y-2 text-sm">
344
- <span class="text-muted-foreground">Type</span>
345
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.type") }}</span>
345
346
  <span>{{ typeName }}</span>
346
347
 
347
- <span class="text-muted-foreground">Type slug</span>
348
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.typeSlug") }}</span>
348
349
  <span class="font-mono text-xs">{{ credential.credential_type_slug }}</span>
349
350
 
350
- <span class="text-muted-foreground">Version</span>
351
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.version") }}</span>
351
352
  <span class="font-mono text-xs">{{ type?.version ?? "\u2014" }}</span>
352
353
 
353
- <span class="text-muted-foreground">Auth kind</span>
354
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.authKind") }}</span>
354
355
  <span class="font-mono text-xs">{{ type?.auth_kind ?? "\u2014" }}</span>
355
356
 
356
- <span class="text-muted-foreground">Last tested</span>
357
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.lastTested") }}</span>
357
358
  <span class="tabular-nums">{{ formatDateTime(credential.last_test_at) }}</span>
358
359
 
359
- <span class="text-muted-foreground">Credential ID</span>
360
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.id") }}</span>
360
361
  <span class="font-mono text-xs break-all">{{ credential.id }}</span>
361
362
 
362
- <span class="text-muted-foreground">Created</span>
363
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.created") }}</span>
363
364
  <span class="tabular-nums">{{ formatDateTime(credential.created_at) }}</span>
364
365
 
365
- <span class="text-muted-foreground">Updated</span>
366
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.updated") }}</span>
366
367
  <span class="tabular-nums">{{ formatDateTime(credential.updated_at) }}</span>
367
368
  </div>
368
369
 
369
370
  <!-- Danger zone -->
370
371
  <div class="rounded-lg border border-rose-500/30 bg-rose-500/[0.03] p-3">
371
- <div class="text-sm font-semibold text-rose-700">Danger zone</div>
372
+ <div class="text-sm font-semibold text-rose-700">{{ t("integration.credentials.detail.dangerZone.title") }}</div>
372
373
  <div class="mt-1.5 flex items-center justify-between gap-3">
373
374
  <p class="text-sm text-muted-foreground">
374
- Deleting this credential breaks any workflow that references it until it's re-bound.
375
+ {{ t("integration.credentials.detail.dangerZone.description") }}
375
376
  </p>
376
377
  <Button
377
378
  variant="outline"
@@ -381,8 +382,8 @@ async function connect() {
381
382
  @click="pendingDelete = true"
382
383
  >
383
384
  <Loader2 v-if="deleting" :size="14" class="mr-1 animate-spin" />
384
- <Trash2 v-else :size="14" class="mr-1" />
385
- Delete credential
385
+ <TrashBin v-else :size="14" class="mr-1" />
386
+ {{ t("integration.credentials.detail.action.deleteCredential") }}
386
387
  </Button>
387
388
  </div>
388
389
  </div>
@@ -392,19 +393,19 @@ async function connect() {
392
393
  <AlertDialog :open="pendingDelete" @update:open="(v) => pendingDelete = v">
393
394
  <AlertDialogContent>
394
395
  <AlertDialogHeader>
395
- <AlertDialogTitle>Delete credential?</AlertDialogTitle>
396
+ <AlertDialogTitle>{{ t("integration.credentials.shared.confirm.delete.title") }}</AlertDialogTitle>
396
397
  <AlertDialogDescription>
397
398
  <span class="font-medium text-foreground">{{ credential?.name }}</span>
398
- will be removed. Workflows that reference it will fail until they are re-bound to another credential.
399
+ {{ t("integration.credentials.shared.confirm.delete.body") }}
399
400
  </AlertDialogDescription>
400
401
  </AlertDialogHeader>
401
402
  <AlertDialogFooter>
402
403
  <Button variant="outline" :disabled="deleting" @click="pendingDelete = false">
403
- Cancel
404
+ {{ t("integration.common.action.cancel") }}
404
405
  </Button>
405
406
  <Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="deleting" @click="destroy">
406
407
  <Loader2 v-if="deleting" :size="14" class="animate-spin" />
407
- {{ deleting ? "Deleting\u2026" : "Delete" }}
408
+ {{ deleting ? t("integration.common.state.deleting") : t("integration.common.action.delete") }}
408
409
  </Button>
409
410
  </AlertDialogFooter>
410
411
  </AlertDialogContent>
@@ -29,18 +29,19 @@ const props = defineProps({
29
29
  type: { type: [Object, null], required: true }
30
30
  });
31
31
  const emit = defineEmits(["update:active", "apply"]);
32
+ const t = useIntegrationsT();
32
33
  const kind = computed(() => props.type ? REVENEXX_SOURCE_KIND_BY_SLUG[props.type.slug] ?? null : null);
33
34
  const uid = useId();
34
- const SWITCH_LABEL = {
35
- sftp: "Use revenexx SFTP Servers",
36
- smtp: "Use revenexx Mail Servers",
37
- api: "Use revenexx API Keys"
38
- };
39
- const NEW_LABEL = {
40
- sftp: "New SFTP account",
41
- smtp: "New mail credential",
42
- api: "New API key"
43
- };
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
+ }));
44
45
  const sources = cockpitSources();
45
46
  const available = sources !== null;
46
47
  const tenants = computed(() => sources?.tenants.tenants.value ?? []);
@@ -74,7 +75,7 @@ watch(kind, () => {
74
75
  });
75
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 })));
76
77
  const marketOptions = computed(() => [
77
- { code: GLOBAL_MARKET_CODE, label: "Global (tenant baseline)" },
78
+ { code: GLOBAL_MARKET_CODE, label: t("integration.credentials.revenexxSource.market.global") },
78
79
  ...markets.value.map((m) => ({ code: m.code, label: `${m.name} (${m.code})` }))
79
80
  ]);
80
81
  async function ensureLists() {
@@ -92,7 +93,7 @@ async function ensureLists() {
92
93
  await fetchMarkets();
93
94
  }
94
95
  } catch (e) {
95
- toast.error(e?.message ?? "Could not load revenexx resources.");
96
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.loadResources"));
96
97
  } finally {
97
98
  loadingList.value = false;
98
99
  }
@@ -105,14 +106,14 @@ function marketSlug() {
105
106
  }
106
107
  async function applySftp() {
107
108
  if (!sftp) return;
108
- const t = tenant.value;
109
+ const tenantSlug = tenant.value;
109
110
  const acc = sftp.accounts.value.find((a) => String(a.id) === selectedId.value);
110
- if (!t || !acc) return;
111
+ if (!tenantSlug || !acc) return;
111
112
  applying.value = true;
112
113
  try {
113
- const creds = await sftp.revealPassword(t, acc.id);
114
+ const creds = await sftp.revealPassword(tenantSlug, acc.id);
114
115
  if (!creds) {
115
- toast.error("Account still provisioning \u2014 try again in a moment.");
116
+ toast.error(t("integration.credentials.revenexxSource.error.accountProvisioning"));
116
117
  return;
117
118
  }
118
119
  emit("apply", {
@@ -123,7 +124,7 @@ async function applySftp() {
123
124
  });
124
125
  applied.value = `${acc.login_username}@${sftpHost}:${sftpPort}`;
125
126
  } catch (e) {
126
- toast.error(e?.message ?? "Could not read the SFTP password.");
127
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.readSftpPassword"));
127
128
  } finally {
128
129
  applying.value = false;
129
130
  }
@@ -134,7 +135,7 @@ async function applySmtp() {
134
135
  try {
135
136
  const cred = await mail.getCredential(selectedId.value);
136
137
  if (!cred) {
137
- toast.error("That mail credential is no longer there.");
138
+ toast.error(t("integration.credentials.revenexxSource.error.mailCredentialGone"));
138
139
  return;
139
140
  }
140
141
  emit("apply", {
@@ -146,7 +147,7 @@ async function applySmtp() {
146
147
  });
147
148
  applied.value = `${cred.smtp_username}@${mailSmtpHost}:${mailSmtpPort}`;
148
149
  } catch (e) {
149
- toast.error(e?.message ?? "Could not read the mail credential.");
150
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.readMailCredential"));
150
151
  } finally {
151
152
  applying.value = false;
152
153
  }
@@ -178,30 +179,30 @@ function openCreate() {
178
179
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
179
180
  async function createSftp() {
180
181
  if (!sftp) return;
181
- const t = tenant.value;
182
- if (!t) {
183
- toast.error("No tenant selected.");
182
+ const tenantSlug = tenant.value;
183
+ if (!tenantSlug) {
184
+ toast.error(t("integration.credentials.revenexxSource.error.noTenant"));
184
185
  return;
185
186
  }
186
187
  creating.value = true;
187
188
  try {
188
- let acc = await sftp.create(t, createName.value.trim() || void 0);
189
+ let acc = await sftp.create(tenantSlug, createName.value.trim() || void 0);
189
190
  for (let i = 0; i < 24 && !(acc.is_active || acc.status === "active"); i++) {
190
- provisioningNote.value = "Provisioning the account\u2026";
191
+ provisioningNote.value = t("integration.credentials.revenexxSource.status.provisioning");
191
192
  await sleep(2500);
192
- acc = await sftp.refresh(t, acc.id);
193
- 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"));
194
195
  }
195
196
  if (!(acc.is_active || acc.status === "active")) {
196
- 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"));
197
198
  }
198
- await sftp.load(t);
199
+ await sftp.load(tenantSlug);
199
200
  selectedId.value = String(acc.id);
200
201
  await applySftp();
201
202
  createOpen.value = false;
202
- toast.success("SFTP account created");
203
+ toast.success(t("integration.credentials.revenexxSource.toast.sftpCreated"));
203
204
  } catch (e) {
204
- toast.error(e?.message ?? "Could not create the SFTP account.");
205
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createSftp"));
205
206
  } finally {
206
207
  creating.value = false;
207
208
  provisioningNote.value = "";
@@ -213,7 +214,7 @@ async function createSmtp() {
213
214
  try {
214
215
  const cred = await mail.createCredential({ name: createName.value.trim() || "SMTP", type: "SMTP" });
215
216
  if (!cred) {
216
- toast.error("The mail credential was not returned \u2014 pick it from the list once it appears.");
217
+ toast.error(t("integration.credentials.revenexxSource.error.mailCredentialNotReturned"));
217
218
  return;
218
219
  }
219
220
  emit("apply", {
@@ -227,9 +228,9 @@ async function createSmtp() {
227
228
  smtpCreds.value = [{ uuid: cred.uuid, name: cred.name }, ...smtpCreds.value];
228
229
  selectedId.value = cred.uuid;
229
230
  createOpen.value = false;
230
- toast.success("Mail credential created");
231
+ toast.success(t("integration.credentials.revenexxSource.toast.smtpCreated"));
231
232
  } catch (e) {
232
- toast.error(e?.message ?? "Could not create the mail credential.");
233
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createSmtp"));
233
234
  } finally {
234
235
  creating.value = false;
235
236
  }
@@ -241,11 +242,11 @@ async function createApiKey() {
241
242
  const { secret } = await consoleKeys.create(createName.value.trim() || "Integrations key", ["*"]);
242
243
  emit("apply", { apiKey: secret, tenant: tenant.value ?? "", market: marketSlug() });
243
244
  apiKeyReady.value = true;
244
- 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 });
245
246
  createOpen.value = false;
246
- toast.success("API key created");
247
+ toast.success(t("integration.credentials.revenexxSource.toast.apiKeyCreated"));
247
248
  } catch (e) {
248
- toast.error(e?.message ?? "Could not create the API key.");
249
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createApiKey"));
249
250
  } finally {
250
251
  creating.value = false;
251
252
  }
@@ -269,11 +270,10 @@ function submitCreate() {
269
270
  belongs somewhere else. The manual fields stay on screen either way. -->
270
271
  <p class="text-xs text-muted-foreground">
271
272
  <template v-if="available">
272
- Reuse an existing revenexx resource instead of typing access data by hand.
273
+ {{ t("integration.credentials.revenexxSource.hint.available") }}
273
274
  </template>
274
275
  <template v-else>
275
- Not available here — the managed revenexx resources live in the cockpit.
276
- Enter the access data by hand instead.
276
+ {{ t("integration.credentials.revenexxSource.hint.unavailable") }}
277
277
  </template>
278
278
  </p>
279
279
  </div>
@@ -291,14 +291,14 @@ function submitCreate() {
291
291
  <div v-if="active" class="mt-3 space-y-3">
292
292
  <!-- Tenant (preselected) -->
293
293
  <div class="space-y-1.5">
294
- <Label :for="`${uid}-tenant`">Tenant</Label>
294
+ <Label :for="`${uid}-tenant`">{{ t("integration.credentials.revenexxSource.field.tenant.label") }}</Label>
295
295
  <Select :model-value="tenant ?? ''" @update:model-value="(v) => tenant = v">
296
296
  <SelectTrigger :id="`${uid}-tenant`" class="bg-white">
297
- <SelectValue placeholder="Select tenant" />
297
+ <SelectValue :placeholder="t('integration.credentials.revenexxSource.field.tenant.placeholder')" />
298
298
  </SelectTrigger>
299
299
  <SelectContent>
300
- <SelectItem v-for="t in tenants" :key="t.slug" :value="t.slug">
301
- {{ t.name }} ({{ t.slug }})
300
+ <SelectItem v-for="tn in tenants" :key="tn.slug" :value="tn.slug">
301
+ {{ tn.name }} ({{ tn.slug }})
302
302
  </SelectItem>
303
303
  </SelectContent>
304
304
  </Select>
@@ -306,10 +306,10 @@ function submitCreate() {
306
306
 
307
307
  <!-- Market scope — only the Revenexx API credential is market-scoped -->
308
308
  <div v-if="kind === 'api'" class="space-y-1.5">
309
- <Label :for="`${uid}-market`">Market</Label>
309
+ <Label :for="`${uid}-market`">{{ t("integration.credentials.revenexxSource.field.market.label") }}</Label>
310
310
  <Select :model-value="marketCode" @update:model-value="(v) => marketCode = v">
311
311
  <SelectTrigger :id="`${uid}-market`" class="bg-white">
312
- <SelectValue placeholder="Select market" />
312
+ <SelectValue :placeholder="t('integration.credentials.revenexxSource.field.market.placeholder')" />
313
313
  </SelectTrigger>
314
314
  <SelectContent>
315
315
  <SelectItem v-for="m in marketOptions" :key="m.code" :value="m.code">
@@ -321,11 +321,11 @@ function submitCreate() {
321
321
 
322
322
  <!-- Resource select (SFTP / Mail) -->
323
323
  <div v-if="kind === 'sftp' || kind === 'smtp'" class="space-y-1.5">
324
- <Label :for="`${uid}-resource`">{{ 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>
325
325
  <div class="flex items-center gap-2">
326
326
  <Select :model-value="selectedId" class="flex-1" @update:model-value="(v) => onSelect(v)">
327
327
  <SelectTrigger :id="`${uid}-resource`" class="bg-white">
328
- <SelectValue :placeholder="loadingList ? 'Loading\u2026' : 'Select one'" />
328
+ <SelectValue :placeholder="loadingList ? t('integration.common.state.loading') : t('integration.credentials.revenexxSource.field.resource.placeholder')" />
329
329
  </SelectTrigger>
330
330
  <SelectContent>
331
331
  <template v-if="kind === 'sftp'">
@@ -338,7 +338,7 @@ function submitCreate() {
338
338
  </Select>
339
339
  <Button variant="outline" size="sm" class="shrink-0 bg-white" :disabled="!tenant" @click="openCreate">
340
340
  <Plus :size="14" class="mr-1" />
341
- New
341
+ {{ t("integration.credentials.revenexxSource.action.new") }}
342
342
  </Button>
343
343
  </div>
344
344
  </div>
@@ -347,7 +347,7 @@ function submitCreate() {
347
347
  <div v-else-if="kind === 'api'">
348
348
  <Button variant="outline" size="sm" class="bg-white" :disabled="!tenant" @click="openCreate">
349
349
  <Plus :size="14" class="mr-1" />
350
- Create new API key
350
+ {{ t("integration.credentials.revenexxSource.action.createApiKey") }}
351
351
  </Button>
352
352
  </div>
353
353
 
@@ -355,7 +355,7 @@ function submitCreate() {
355
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">
356
356
  <Loader2 v-if="applying" :size="14" class="animate-spin" />
357
357
  <Check v-else :size="14" />
358
- <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>
359
359
  </div>
360
360
  </div>
361
361
 
@@ -363,36 +363,36 @@ function submitCreate() {
363
363
  <Dialog v-model:open="createOpen">
364
364
  <DialogContent class="sm:max-w-md">
365
365
  <DialogHeader>
366
- <DialogTitle>{{ kind ? NEW_LABEL[kind] : "New" }}</DialogTitle>
366
+ <DialogTitle>{{ kind ? NEW_LABEL[kind] : t("integration.credentials.revenexxSource.new.fallback") }}</DialogTitle>
367
367
  <DialogDescription class="text-sm">
368
368
  <template v-if="kind === 'api'">
369
- 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 }) }}
370
370
  </template>
371
371
  <template v-else-if="kind === 'sftp'">
372
- Reserves a new SFTP account for <b>{{ tenant }}</b> and fills its login here.
372
+ {{ t("integration.credentials.revenexxSource.createDialog.description.sftp", { tenant }) }}
373
373
  </template>
374
374
  <template v-else>
375
- Creates a new SMTP credential and fills its login here.
375
+ {{ t("integration.credentials.revenexxSource.createDialog.description.smtp") }}
376
376
  </template>
377
377
  </DialogDescription>
378
378
  </DialogHeader>
379
379
 
380
380
  <div class="space-y-1.5 py-1">
381
- <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>
382
382
  <Input
383
383
  id="rvnxx-new-name"
384
384
  v-model="createName"
385
385
  class="bg-white"
386
- :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')"
387
387
  />
388
388
  <p v-if="provisioningNote" class="text-xs text-muted-foreground">{{ provisioningNote }}</p>
389
389
  </div>
390
390
 
391
391
  <DialogFooter>
392
- <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>
393
393
  <Button :disabled="creating" @click="submitCreate">
394
394
  <Loader2 v-if="creating" :size="14" class="mr-1 animate-spin" />
395
- Create
395
+ {{ t("integration.credentials.revenexxSource.action.create") }}
396
396
  </Button>
397
397
  </DialogFooter>
398
398
  </DialogContent>
@@ -7,20 +7,21 @@ const props = defineProps({
7
7
  types: { type: Array, required: true }
8
8
  });
9
9
  const emit = defineEmits(["pick"]);
10
+ const t = useIntegrationsT();
10
11
  const query = ref("");
11
12
  function authKindLabel(k) {
12
13
  switch (k) {
13
14
  case "api-key":
14
- return "API Key";
15
+ return t("integration.credentials.shared.authKind.apiKey");
15
16
  case "oauth2-authcode":
16
17
  case "oauth2-client-credentials":
17
- return "OAuth2";
18
+ return t("integration.credentials.shared.authKind.oauth2");
18
19
  case "basic-auth":
19
- return "Basic Auth";
20
+ return t("integration.credentials.shared.authKind.basicAuth");
20
21
  case "static":
21
- return "Static";
22
+ return t("integration.credentials.shared.authKind.static");
22
23
  case "secret":
23
- return "Secret";
24
+ return t("integration.credentials.shared.authKind.secret");
24
25
  default:
25
26
  return k ?? "";
26
27
  }
@@ -28,7 +29,7 @@ function authKindLabel(k) {
28
29
  const filtered = computed(() => {
29
30
  const q = query.value.trim().toLowerCase();
30
31
  if (!q) return props.types;
31
- return props.types.filter((t) => resolveLocalized(t.name).toLowerCase().includes(q));
32
+ return props.types.filter((t2) => resolveLocalized(t2.name).toLowerCase().includes(q));
32
33
  });
33
34
  </script>
34
35
 
@@ -38,16 +39,16 @@ const filtered = computed(() => {
38
39
  <Search :size="15" class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
39
40
  <Input
40
41
  v-model="query"
41
- aria-label="Search for an app"
42
+ :aria-label="t('integration.credentials.typePicker.search.aria')"
42
43
  class="bg-white pl-9"
43
- placeholder="Search for an app…"
44
+ :placeholder="t('integration.credentials.typePicker.search.placeholder')"
44
45
  autofocus
45
46
  />
46
47
  </div>
47
48
 
48
49
  <div class="mt-2 max-h-80 overflow-y-auto -mx-1 px-1">
49
50
  <p v-if="!filtered.length" class="px-2 py-6 text-center text-sm text-muted-foreground">
50
- No credential type matches “{{ query }}”.
51
+ {{ t("integration.credentials.typePicker.empty", { query }) }}
51
52
  </p>
52
53
  <button
53
54
  v-for="t in filtered"