@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
@@ -19,40 +19,36 @@ import {
19
19
  SelectValue
20
20
  } from "@revenexx/studio";
21
21
  import { toast } from "vue-sonner";
22
+ import { resolveLocalized } from "../../utils/localize";
22
23
  const props = defineProps({
23
24
  open: { type: Boolean, required: true },
24
25
  credentialType: { type: [String, Array], required: false }
25
26
  });
26
27
  const emit = defineEmits(["update:open", "created"]);
28
+ const t = useIntegrationsT();
27
29
  const { create, testInline, load, oauthAuthorizeUrl } = useIntegrationsCredentials();
28
30
  const { load: loadTypes, bySlug } = useCredentialTypes();
29
- function loc(v) {
30
- if (!v) return "";
31
- if (typeof v === "string") return v;
32
- const o = v;
33
- return o.en ?? o.de ?? Object.values(o)[0] ?? "";
34
- }
35
31
  function authKindLabel(k) {
36
32
  switch (k) {
37
33
  case "api-key":
38
- return "API Key";
34
+ return t("integration.credentials.shared.authKind.apiKey");
39
35
  case "oauth2-authcode":
40
36
  case "oauth2-client-credentials":
41
- return "OAuth2";
37
+ return t("integration.credentials.shared.authKind.oauth2");
42
38
  case "basic-auth":
43
- return "Basic Auth";
39
+ return t("integration.credentials.shared.authKind.basicAuth");
44
40
  case "static":
45
- return "Static";
41
+ return t("integration.credentials.shared.authKind.static");
46
42
  case "secret":
47
- return "Secret";
43
+ return t("integration.credentials.shared.authKind.secret");
48
44
  default:
49
45
  return k ?? "";
50
46
  }
51
47
  }
52
48
  const allowedSlugs = computed(() => {
53
- const t = props.credentialType;
54
- if (!t) return [];
55
- return (Array.isArray(t) ? t : [t]).map((s) => s.trim()).filter(Boolean);
49
+ const t2 = props.credentialType;
50
+ if (!t2) return [];
51
+ return (Array.isArray(t2) ? t2 : [t2]).map((s) => s.trim()).filter(Boolean);
56
52
  });
57
53
  const allowedTypes = computed(() => allowedSlugs.value.map((s) => bySlug(s)).filter((x) => !!x));
58
54
  const pickedType = ref(null);
@@ -61,11 +57,11 @@ const sourceActive = ref(false);
61
57
  const saving = ref(false);
62
58
  const testing = ref(false);
63
59
  const testResult = ref(null);
64
- function fieldsOf(t) {
65
- return t?.fields ?? [];
60
+ function fieldsOf(t2) {
61
+ return t2?.fields ?? [];
66
62
  }
67
- function isOAuthAuthcode(t) {
68
- return t?.auth_kind === "oauth2-authcode";
63
+ function isOAuthAuthcode(t2) {
64
+ return t2?.auth_kind === "oauth2-authcode";
69
65
  }
70
66
  watch(() => props.open, async (o) => {
71
67
  if (!o) return;
@@ -75,18 +71,18 @@ watch(() => props.open, async (o) => {
75
71
  await loadTypes();
76
72
  if (allowedTypes.value.length === 1) {
77
73
  pickedType.value = allowedTypes.value[0];
78
- form.name = loc(pickedType.value.name);
74
+ form.name = resolveLocalized(pickedType.value.name);
79
75
  } else {
80
76
  pickedType.value = null;
81
77
  form.name = "";
82
78
  }
83
79
  }, { immediate: true });
84
- function pickType(t) {
85
- pickedType.value = t;
80
+ function pickType(t2) {
81
+ pickedType.value = t2;
86
82
  form.config = {};
87
83
  testResult.value = null;
88
84
  sourceActive.value = false;
89
- if (!form.name) form.name = loc(t.name);
85
+ if (!form.name) form.name = resolveLocalized(t2.name);
90
86
  }
91
87
  function buildConfigPayload() {
92
88
  const out = {};
@@ -135,14 +131,14 @@ async function submit() {
135
131
  config: buildConfigPayload()
136
132
  });
137
133
  await load(true);
138
- toast.success("Credential created");
134
+ toast.success(t("integration.credentials.toast.created"));
139
135
  if (isOAuthAuthcode(pickedType.value)) {
140
136
  void connect(created.id);
141
137
  }
142
138
  emit("created", created.id);
143
139
  emit("update:open", false);
144
140
  } catch (e) {
145
- toast.error(e?.message ?? "Could not create credential.");
141
+ toast.error(e?.message ?? t("integration.credentials.createDialog.error.create"));
146
142
  } finally {
147
143
  saving.value = false;
148
144
  }
@@ -153,16 +149,16 @@ async function submit() {
153
149
  <Dialog :open="open" @update:open="(v) => emit('update:open', v)">
154
150
  <DialogContent class="sm:max-w-lg">
155
151
  <DialogHeader>
156
- <DialogTitle>New credential</DialogTitle>
152
+ <DialogTitle>{{ t("integration.credentials.shared.dialog.title.create") }}</DialogTitle>
157
153
  <DialogDescription class="text-sm">
158
- Create a credential for this node without leaving the editor.
154
+ {{ t("integration.credentials.createDialog.description") }}
159
155
  </DialogDescription>
160
156
  </DialogHeader>
161
157
 
162
158
  <!-- Choose the type when the field accepts more than one -->
163
159
  <div v-if="!pickedType" class="space-y-2 py-1">
164
160
  <p v-if="!allowedTypes.length" class="text-sm text-muted-foreground">
165
- No credential type available for this field.
161
+ {{ t("integration.credentials.createDialog.noTypeAvailable") }}
166
162
  </p>
167
163
  <button
168
164
  v-for="t in allowedTypes"
@@ -173,7 +169,7 @@ async function submit() {
173
169
  >
174
170
  <IntegrationsCredentialLogo :type="t" :size="18" />
175
171
  <div class="min-w-0 truncate text-sm">
176
- <span class="font-semibold">{{ loc(t.name) }}</span>
172
+ <span class="font-semibold">{{ resolveLocalized(t.name) }}</span>
177
173
  <span class="ml-1.5 text-muted-foreground">({{ authKindLabel(t.auth_kind) }})</span>
178
174
  </div>
179
175
  </button>
@@ -183,20 +179,20 @@ async function submit() {
183
179
  <div v-else class="space-y-3 py-1 max-h-[26rem] overflow-y-auto pr-1">
184
180
  <div class="flex items-center gap-2">
185
181
  <IntegrationsCredentialLogo :type="pickedType" :size="18" />
186
- <div class="text-sm font-medium">{{ loc(pickedType.name) }}</div>
182
+ <div class="text-sm font-medium">{{ resolveLocalized(pickedType.name) }}</div>
187
183
  <button
188
184
  v-if="allowedTypes.length > 1"
189
185
  type="button"
190
186
  class="ml-auto text-sm text-muted-foreground hover:text-foreground"
191
187
  @click="pickedType = null"
192
188
  >
193
- Change type
189
+ {{ t("integration.credentials.createDialog.action.changeType") }}
194
190
  </button>
195
191
  </div>
196
192
 
197
193
  <div class="space-y-1.5">
198
- <Label for="cred-name">Name</Label>
199
- <Input id="cred-name" v-model="form.name" class="bg-white" placeholder="e.g. Production key" />
194
+ <Label for="cred-name">{{ t("integration.credentials.shared.field.name.label") }}</Label>
195
+ <Input id="cred-name" v-model="form.name" class="bg-white" :placeholder="t('integration.credentials.createDialog.field.name.placeholder')" />
200
196
  </div>
201
197
 
202
198
  <!-- "Use revenexx …" source picker (SFTP / Mail / API key) -->
@@ -206,10 +202,14 @@ async function submit() {
206
202
  @apply="(c) => Object.assign(form.config, c)"
207
203
  />
208
204
 
205
+ <!-- Each label names its own control (`for`/`id`): without it a screen
206
+ reader reads the field as unlabelled, and the label text cannot be
207
+ used to reach it at all. -->
209
208
  <div v-for="f in fieldsOf(pickedType)" v-show="!sourceActive" :key="f.key" class="space-y-1.5">
210
- <Label>{{ loc(f.label) }}{{ f.required ? " *" : "" }}</Label>
209
+ <Label :for="`create-field-${f.key}`">{{ resolveLocalized(f.label) }}{{ f.required ? " *" : "" }}</Label>
211
210
  <Input
212
211
  v-if="f.type === 'secret'"
212
+ :id="`create-field-${f.key}`"
213
213
  :model-value="form.config[f.key] ?? ''"
214
214
  type="password"
215
215
  autocomplete="new-password"
@@ -218,30 +218,32 @@ async function submit() {
218
218
  />
219
219
  <Input
220
220
  v-else-if="f.type === 'number'"
221
+ :id="`create-field-${f.key}`"
221
222
  type="number"
222
223
  :model-value="form.config[f.key] ?? ''"
223
224
  class="bg-white"
224
225
  @update:model-value="(v) => form.config[f.key] = v === '' ? void 0 : Number(v)"
225
226
  />
226
227
  <div v-else-if="f.type === 'boolean'" class="flex items-center gap-2 pt-1">
227
- <Switch :model-value="Boolean(form.config[f.key])" @update:model-value="(v) => form.config[f.key] = v" />
228
+ <Switch :id="`create-field-${f.key}`" :model-value="Boolean(form.config[f.key])" @update:model-value="(v) => form.config[f.key] = v" />
228
229
  </div>
229
230
  <Select
230
231
  v-else-if="f.type === 'select'"
231
232
  :model-value="form.config[f.key] ?? ''"
232
233
  @update:model-value="(v) => form.config[f.key] = v"
233
234
  >
234
- <SelectTrigger class="bg-white">
235
- <SelectValue placeholder="Pick one" />
235
+ <SelectTrigger :id="`create-field-${f.key}`" class="bg-white">
236
+ <SelectValue :placeholder="t('integration.credentials.shared.field.select.placeholder')" />
236
237
  </SelectTrigger>
237
238
  <SelectContent>
238
239
  <SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
239
- {{ loc(opt.label) }}
240
+ {{ resolveLocalized(opt.label) }}
240
241
  </SelectItem>
241
242
  </SelectContent>
242
243
  </Select>
243
244
  <Input
244
245
  v-else
246
+ :id="`create-field-${f.key}`"
245
247
  :model-value="form.config[f.key] ?? ''"
246
248
  class="bg-white"
247
249
  @update:model-value="(v) => form.config[f.key] = v"
@@ -249,7 +251,7 @@ async function submit() {
249
251
  </div>
250
252
 
251
253
  <p v-if="isOAuthAuthcode(pickedType)" class="text-sm text-muted-foreground">
252
- After saving, a window opens to connect with the provider.
254
+ {{ t("integration.credentials.shared.oauthHint") }}
253
255
  </p>
254
256
 
255
257
  <div
@@ -257,7 +259,7 @@ async function submit() {
257
259
  class="rounded-md px-3 py-2 text-sm"
258
260
  :class="testResult.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
259
261
  >
260
- {{ testResult.ok ? "Connection ok." : testResult.error ?? "Connection failed." }}
262
+ {{ testResult.ok ? t("integration.credentials.shared.test.ok") : testResult.error ?? t("integration.credentials.shared.test.failed") }}
261
263
  </div>
262
264
  </div>
263
265
 
@@ -266,11 +268,11 @@ async function submit() {
266
268
  <Button variant="outline" :disabled="saving || testing" @click="runTest">
267
269
  <Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
268
270
  <TestIcon v-else :size="14" class="mr-1" />
269
- Connect
271
+ {{ t("integration.credentials.shared.action.testConnection") }}
270
272
  </Button>
271
273
  <Button :disabled="saving || !form.name.trim()" @click="submit">
272
274
  <Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
273
- Save
275
+ {{ t("integration.common.action.save") }}
274
276
  </Button>
275
277
  </div>
276
278
  </DialogFooter>
@@ -5,9 +5,15 @@ 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
+ AlertDialog,
12
+ AlertDialogContent,
13
+ AlertDialogDescription,
14
+ AlertDialogFooter,
15
+ AlertDialogHeader,
16
+ AlertDialogTitle,
11
17
  Button,
12
18
  Dialog,
13
19
  DialogContent,
@@ -23,38 +29,45 @@ import {
23
29
  SelectTrigger,
24
30
  SelectValue
25
31
  } from "@revenexx/studio";
26
- import { toast } from "vue-sonner";
32
+ import { toast } from "../../utils/notify";
33
+ import { apiFieldErrors, refusalReason } from "../../utils/apiError";
27
34
  import { credentialState, relativeTime } from "../../utils/credentialState";
35
+ import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
36
+ import { resolveLocalized } from "../../utils/localize";
28
37
  const props = defineProps({
29
38
  open: { type: Boolean, required: true },
30
39
  credential: { type: [Object, null], required: true },
31
40
  type: { type: [Object, null], required: true }
32
41
  });
33
42
  const emit = defineEmits(["update:open", "changed"]);
43
+ const t = useIntegrationsT();
34
44
  const { update, remove, test, oauthAuthorizeUrl, load } = useIntegrationsCredentials();
35
45
  const tab = ref("credentials");
36
- const tabs = [
37
- { key: "credentials", label: "Credentials" },
38
- { key: "details", label: "Details" }
39
- ];
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
+ ]);
40
50
  const form = reactive({ name: "", config: {} });
41
51
  const sourceActive = ref(false);
42
52
  const saving = ref(false);
43
53
  const deleting = ref(false);
44
54
  const testing = ref(false);
45
55
  const liveTest = ref(null);
46
- const connecting = ref(false);
47
- function loc(v) {
48
- if (!v) return "";
49
- if (typeof v === "string") return v;
50
- const o = v;
51
- return o.en ?? o.de ?? Object.values(o)[0] ?? "";
56
+ const saveError = ref(null);
57
+ const fieldErrors = ref({});
58
+ function configFieldAria(key) {
59
+ return fieldErrors.value[`config.${key}`] ? { "aria-invalid": "true", "aria-describedby": `detail-field-${key}-error` } : {};
60
+ }
61
+ function clearRefusal() {
62
+ saveError.value = null;
63
+ fieldErrors.value = {};
52
64
  }
53
- const typeName = computed(() => loc(props.type?.name) || props.credential?.credential_type_slug || "");
65
+ const connecting = ref(false);
66
+ const typeName = computed(() => resolveLocalized(props.type?.name) || props.credential?.credential_type_slug || "");
54
67
  const isOAuthAuthcode = computed(() => props.type?.auth_kind === "oauth2-authcode");
55
68
  const state = computed(() => props.credential ? credentialState(props.credential) : null);
56
- function fieldsOf(t) {
57
- return t?.fields ?? [];
69
+ function fieldsOf(t2) {
70
+ return t2?.fields ?? [];
58
71
  }
59
72
  function formatDateTime(iso) {
60
73
  if (!iso) return "\u2014";
@@ -65,7 +78,7 @@ function formatDateTime(iso) {
65
78
  }
66
79
  }
67
80
  watch(
68
- () => [props.open, props.credential?.id],
81
+ [() => props.open, () => props.credential?.id],
69
82
  () => {
70
83
  if (!props.open || !props.credential) return;
71
84
  tab.value = "credentials";
@@ -73,6 +86,7 @@ watch(
73
86
  form.config = { ...props.credential.public_config ?? {} };
74
87
  sourceActive.value = false;
75
88
  liveTest.value = null;
89
+ clearRefusal();
76
90
  },
77
91
  { immediate: true }
78
92
  );
@@ -100,28 +114,32 @@ async function runTest() {
100
114
  async function save() {
101
115
  if (!props.credential || !form.name.trim()) return;
102
116
  saving.value = true;
117
+ clearRefusal();
103
118
  try {
104
119
  await update(props.credential.id, { name: form.name.trim(), config: buildConfigPayload() });
105
- toast.success("Credential updated");
106
120
  emit("changed");
107
121
  emit("update:open", false);
122
+ toast.success(t("integration.credentials.toast.updated"));
108
123
  } catch (e) {
109
- toast.error(e?.message ?? "Could not save credential.");
124
+ fieldErrors.value = apiFieldErrors(e);
125
+ saveError.value = refusalReason(e, t("integration.credentials.error.save"));
126
+ toast.error(saveError.value);
110
127
  } finally {
111
128
  saving.value = false;
112
129
  }
113
130
  }
131
+ const pendingDelete = ref(false);
114
132
  async function destroy() {
115
133
  if (!props.credential) return;
116
- if (!confirm(`Delete credential "${props.credential.name}"? Workflows referencing it will fail until re-bound.`)) return;
134
+ pendingDelete.value = false;
117
135
  deleting.value = true;
118
136
  try {
119
137
  await remove(props.credential.id);
120
- toast.success("Credential deleted");
138
+ toast.success(t("integration.credentials.toast.deleted"));
121
139
  emit("changed");
122
140
  emit("update:open", false);
123
141
  } catch (e) {
124
- toast.error(e?.message ?? "Could not delete credential.");
142
+ toast.error(e?.message ?? t("integration.credentials.error.delete"));
125
143
  } finally {
126
144
  deleting.value = false;
127
145
  }
@@ -137,7 +155,7 @@ async function connect() {
137
155
  window.removeEventListener("message", onMessage);
138
156
  await load(true);
139
157
  emit("changed");
140
- toast.success("Connected");
158
+ toast.success(t("integration.credentials.toast.connected"));
141
159
  }
142
160
  };
143
161
  window.addEventListener("message", onMessage);
@@ -150,7 +168,7 @@ async function connect() {
150
168
  }
151
169
  }, 1e3);
152
170
  } catch (e) {
153
- toast.error(e?.message ?? "Could not start OAuth connect.");
171
+ toast.error(e?.message ?? t("integration.credentials.error.oauthConnect"));
154
172
  } finally {
155
173
  connecting.value = false;
156
174
  }
@@ -193,73 +211,101 @@ async function connect() {
193
211
  @click="connect"
194
212
  >
195
213
  <Loader2 v-if="connecting" :size="13" class="mr-1 animate-spin" />
196
- Reconnect
214
+ {{ t("integration.credentials.detail.action.reconnect") }}
197
215
  </Button>
198
216
  </div>
199
217
  <p v-if="state.key === 'failed' && state.message" class="mt-1 text-sm break-words opacity-90">
200
218
  {{ state.message }}
201
219
  </p>
202
220
  <p v-else-if="state.key === 'untested'" class="mt-1 text-sm opacity-80">
203
- Run a test to verify these credentials actually connect.
221
+ {{ t("integration.credentials.detail.state.untestedHint") }}
204
222
  </p>
205
223
  </div>
206
224
 
207
225
  <!-- Editable fields -->
208
226
  <div class="space-y-3">
209
227
  <div class="space-y-1.5">
210
- <Label for="detail-cred-name">Name</Label>
211
- <Input id="detail-cred-name" v-model="form.name" class="bg-white" />
228
+ <Label for="detail-cred-name">{{ t("integration.credentials.shared.field.name.label") }}</Label>
229
+ <Input
230
+ id="detail-cred-name"
231
+ v-model="form.name"
232
+ class="bg-white"
233
+ :aria-invalid="fieldErrors.name ? 'true' : void 0"
234
+ :aria-describedby="fieldErrors.name ? 'detail-cred-name-error' : void 0"
235
+ />
236
+ <p v-if="fieldErrors.name" id="detail-cred-name-error" class="text-sm text-rose-700">
237
+ {{ fieldErrors.name }}
238
+ </p>
212
239
  </div>
213
240
 
214
- <!-- "Use revenexx …" source picker (SFTP / Mail / API key) -->
241
+ <!-- "Use revenexx …" source picker (SFTP / Mail / API key) — mounted
242
+ only for the types it serves, which is tidiness rather than
243
+ protection: it renders nothing for any other type anyway. It used
244
+ to be protection, because the component resolved cockpit-only
245
+ sources at setup and mounting it elsewhere took this form down;
246
+ since PO-222 it asks instead (`utils/cockpitSources.ts`). -->
215
247
  <IntegrationsCredentialRevenexxSource
248
+ v-if="hasRevenexxSource(type?.slug)"
216
249
  :type="type"
217
250
  @update:active="(v) => sourceActive = v"
218
251
  @apply="(c) => Object.assign(form.config, c)"
219
252
  />
220
253
 
254
+ <!-- Each label names its own control (`for`/`id`): without it a
255
+ screen reader reads the field as unlabelled, and the label text
256
+ cannot be used to reach it at all. -->
221
257
  <div v-for="f in fieldsOf(type)" v-show="!sourceActive" :key="f.key" class="space-y-1.5">
222
- <Label>{{ loc(f.label) }}{{ f.required ? " *" : "" }}</Label>
258
+ <Label :for="`detail-field-${f.key}`">{{ resolveLocalized(f.label) }}{{ f.required ? " *" : "" }}</Label>
223
259
 
224
260
  <Input
225
261
  v-if="f.type === 'secret'"
262
+ :id="`detail-field-${f.key}`"
226
263
  :model-value="form.config[f.key] ?? ''"
227
264
  type="password"
228
265
  autocomplete="new-password"
229
- placeholder="Leave blank to keep current"
266
+ :placeholder="t('integration.credentials.shared.field.secret.placeholder')"
230
267
  class="bg-white"
268
+ v-bind="configFieldAria(f.key)"
231
269
  @update:model-value="(v) => form.config[f.key] = v"
232
270
  />
233
271
  <Input
234
272
  v-else-if="f.type === 'number'"
273
+ :id="`detail-field-${f.key}`"
235
274
  type="number"
236
275
  :model-value="form.config[f.key] ?? ''"
237
276
  class="bg-white"
277
+ v-bind="configFieldAria(f.key)"
238
278
  @update:model-value="(v) => form.config[f.key] = v === '' ? void 0 : Number(v)"
239
279
  />
240
280
  <div v-else-if="f.type === 'boolean'" class="flex items-center gap-2 pt-1">
241
- <Switch :model-value="Boolean(form.config[f.key])" @update:model-value="(v) => form.config[f.key] = v" />
281
+ <Switch :id="`detail-field-${f.key}`" :model-value="Boolean(form.config[f.key])" v-bind="configFieldAria(f.key)" @update:model-value="(v) => form.config[f.key] = v" />
242
282
  </div>
243
283
  <Select
244
284
  v-else-if="f.type === 'select'"
245
285
  :model-value="form.config[f.key] ?? ''"
246
286
  @update:model-value="(v) => form.config[f.key] = v"
247
287
  >
248
- <SelectTrigger class="bg-white">
249
- <SelectValue placeholder="Pick one" />
288
+ <SelectTrigger :id="`detail-field-${f.key}`" class="bg-white" v-bind="configFieldAria(f.key)">
289
+ <SelectValue :placeholder="t('integration.credentials.shared.field.select.placeholder')" />
250
290
  </SelectTrigger>
251
291
  <SelectContent>
252
292
  <SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
253
- {{ loc(opt.label) }}
293
+ {{ resolveLocalized(opt.label) }}
254
294
  </SelectItem>
255
295
  </SelectContent>
256
296
  </Select>
257
297
  <Input
258
298
  v-else
299
+ :id="`detail-field-${f.key}`"
259
300
  :model-value="form.config[f.key] ?? ''"
260
301
  class="bg-white"
302
+ v-bind="configFieldAria(f.key)"
261
303
  @update:model-value="(v) => form.config[f.key] = v"
262
304
  />
305
+
306
+ <p v-if="fieldErrors[`config.${f.key}`]" :id="`detail-field-${f.key}-error`" class="text-sm text-rose-700">
307
+ {{ fieldErrors[`config.${f.key}`] }}
308
+ </p>
263
309
  </div>
264
310
 
265
311
  <!-- Live test feedback (from the test the operator just ran) -->
@@ -268,7 +314,13 @@ async function connect() {
268
314
  class="rounded-md px-3 py-2 text-sm"
269
315
  :class="liveTest.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
270
316
  >
271
- {{ 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") }}
318
+ </div>
319
+
320
+ <!-- The save was refused. Same rule as the create dialog: stated here, and at
321
+ the field where the service keyed it. -->
322
+ <div v-if="saveError" role="alert" class="rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
323
+ {{ saveError }}
272
324
  </div>
273
325
 
274
326
  <div class="flex items-center gap-2 pt-1">
@@ -276,11 +328,11 @@ async function connect() {
276
328
  <Button variant="outline" :disabled="testing || saving" @click="runTest">
277
329
  <Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
278
330
  <TestIcon v-else :size="14" class="mr-1" />
279
- Connect
331
+ {{ t("integration.credentials.shared.action.testConnection") }}
280
332
  </Button>
281
333
  <Button :disabled="saving || !form.name.trim()" @click="save">
282
334
  <Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
283
- Save
335
+ {{ t("integration.common.action.save") }}
284
336
  </Button>
285
337
  </div>
286
338
  </div>
@@ -290,53 +342,74 @@ async function connect() {
290
342
  <!-- Tab 2: Details -->
291
343
  <div v-else class="space-y-5 max-h-[62vh] overflow-y-auto pr-1 pt-4">
292
344
  <div class="grid grid-cols-[9rem_1fr] gap-x-4 gap-y-2 text-sm">
293
- <span class="text-muted-foreground">Type</span>
345
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.type") }}</span>
294
346
  <span>{{ typeName }}</span>
295
347
 
296
- <span class="text-muted-foreground">Type slug</span>
348
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.typeSlug") }}</span>
297
349
  <span class="font-mono text-xs">{{ credential.credential_type_slug }}</span>
298
350
 
299
- <span class="text-muted-foreground">Version</span>
351
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.version") }}</span>
300
352
  <span class="font-mono text-xs">{{ type?.version ?? "\u2014" }}</span>
301
353
 
302
- <span class="text-muted-foreground">Auth kind</span>
354
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.authKind") }}</span>
303
355
  <span class="font-mono text-xs">{{ type?.auth_kind ?? "\u2014" }}</span>
304
356
 
305
- <span class="text-muted-foreground">Last tested</span>
357
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.lastTested") }}</span>
306
358
  <span class="tabular-nums">{{ formatDateTime(credential.last_test_at) }}</span>
307
359
 
308
- <span class="text-muted-foreground">Credential ID</span>
360
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.id") }}</span>
309
361
  <span class="font-mono text-xs break-all">{{ credential.id }}</span>
310
362
 
311
- <span class="text-muted-foreground">Created</span>
363
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.created") }}</span>
312
364
  <span class="tabular-nums">{{ formatDateTime(credential.created_at) }}</span>
313
365
 
314
- <span class="text-muted-foreground">Updated</span>
366
+ <span class="text-muted-foreground">{{ t("integration.credentials.detail.field.updated") }}</span>
315
367
  <span class="tabular-nums">{{ formatDateTime(credential.updated_at) }}</span>
316
368
  </div>
317
369
 
318
370
  <!-- Danger zone -->
319
371
  <div class="rounded-lg border border-rose-500/30 bg-rose-500/[0.03] p-3">
320
- <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>
321
373
  <div class="mt-1.5 flex items-center justify-between gap-3">
322
374
  <p class="text-sm text-muted-foreground">
323
- Deleting this credential breaks any workflow that references it until it's re-bound.
375
+ {{ t("integration.credentials.detail.dangerZone.description") }}
324
376
  </p>
325
377
  <Button
326
378
  variant="outline"
327
379
  size="sm"
328
380
  class="shrink-0 border-rose-500/40 text-rose-700 hover:bg-rose-500/10"
329
381
  :disabled="deleting"
330
- @click="destroy"
382
+ @click="pendingDelete = true"
331
383
  >
332
384
  <Loader2 v-if="deleting" :size="14" class="mr-1 animate-spin" />
333
- <Trash2 v-else :size="14" class="mr-1" />
334
- Delete credential
385
+ <TrashBin v-else :size="14" class="mr-1" />
386
+ {{ t("integration.credentials.detail.action.deleteCredential") }}
335
387
  </Button>
336
388
  </div>
337
389
  </div>
338
390
  </div>
339
391
  </div>
392
+
393
+ <AlertDialog :open="pendingDelete" @update:open="(v) => pendingDelete = v">
394
+ <AlertDialogContent>
395
+ <AlertDialogHeader>
396
+ <AlertDialogTitle>{{ t("integration.credentials.shared.confirm.delete.title") }}</AlertDialogTitle>
397
+ <AlertDialogDescription>
398
+ <span class="font-medium text-foreground">{{ credential?.name }}</span>
399
+ {{ t("integration.credentials.shared.confirm.delete.body") }}
400
+ </AlertDialogDescription>
401
+ </AlertDialogHeader>
402
+ <AlertDialogFooter>
403
+ <Button variant="outline" :disabled="deleting" @click="pendingDelete = false">
404
+ {{ t("integration.common.action.cancel") }}
405
+ </Button>
406
+ <Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="deleting" @click="destroy">
407
+ <Loader2 v-if="deleting" :size="14" class="animate-spin" />
408
+ {{ deleting ? t("integration.common.state.deleting") : t("integration.common.action.delete") }}
409
+ </Button>
410
+ </AlertDialogFooter>
411
+ </AlertDialogContent>
412
+ </AlertDialog>
340
413
  </DialogContent>
341
414
  </Dialog>
342
415
  </template>