@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.
- package/CHANGELOG.md +227 -0
- package/README.md +31 -0
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +10 -2
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +29 -28
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +38 -37
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
- package/dist/runtime/components/Integrations/ExpressionInput.vue +16 -15
- package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
- package/dist/runtime/components/Integrations/Layout.vue +28 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +15 -14
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +9 -8
- package/dist/runtime/components/Integrations/NodeConfigField.vue +43 -42
- package/dist/runtime/components/Integrations/NodeInspector.vue +225 -216
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +1 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +3 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +1 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +30 -29
- package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
- package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
- package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +37 -23
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/TemplateCard.vue +16 -6
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +10 -9
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +36 -35
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +82 -79
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +24 -23
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +19 -20
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +52 -39
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +20 -19
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +12 -9
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +27 -26
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +4 -2
- package/dist/runtime/composables/useIntegrationsCredentials.js +6 -4
- package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
- package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
- package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +3 -1
- package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
- package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
- package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
- package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
- package/dist/runtime/composables/useIntegrationsT.js +9 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
- package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +4 -2
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -3
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +14 -0
- package/dist/runtime/data/integrationTemplates.d.ts +9 -0
- package/dist/runtime/data/integrationTemplates.js +7 -1
- package/dist/runtime/data/payloadForm.js +2 -1
- package/dist/runtime/data/workflowIconCatalog.js +2 -1
- package/dist/runtime/i18n/messages/de.json +1207 -0
- package/dist/runtime/i18n/messages/en.json +1207 -0
- package/dist/runtime/i18n/translate.d.ts +83 -0
- package/dist/runtime/i18n/translate.js +41 -0
- package/dist/runtime/pages/integrations/audit.vue +52 -34
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
- package/dist/runtime/pages/integrations/credentials.vue +70 -52
- package/dist/runtime/pages/integrations/index.vue +50 -56
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +49 -28
- package/dist/runtime/pages/integrations/secrets.vue +34 -34
- package/dist/runtime/pages/integrations/templates/[slug].vue +77 -68
- package/dist/runtime/pages/integrations/templates/index.vue +35 -27
- package/dist/runtime/pages/integrations/workflows/[id].vue +164 -99
- package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
- package/dist/runtime/plugins/locale.d.ts +10 -0
- package/dist/runtime/plugins/locale.js +6 -0
- package/dist/runtime/plugins/realtime.client.d.ts +32 -0
- package/dist/runtime/plugins/realtime.client.js +4 -0
- package/dist/runtime/utils/credentialState.d.ts +24 -1
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/localize.d.ts +18 -5
- package/dist/runtime/utils/localize.js +32 -9
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/schedule.d.ts +0 -25
- package/dist/runtime/utils/schedule.js +37 -25
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +2 -2
- package/dist/runtime/utils/validationFindings.js +4 -3
- package/dist/types.d.mts +2 -6
- 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
|
-
|
|
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: "
|
|
47
|
-
{ key: "details", label: "
|
|
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(
|
|
69
|
-
return
|
|
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("
|
|
122
|
+
toast.success(t("integration.credentials.toast.updated"));
|
|
122
123
|
} catch (e) {
|
|
123
124
|
fieldErrors.value = apiFieldErrors(e);
|
|
124
|
-
saveError.value = refusalReason(e, "
|
|
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("
|
|
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 ?? "
|
|
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("
|
|
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 ?? "
|
|
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
|
-
|
|
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
|
-
|
|
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">
|
|
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="
|
|
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="
|
|
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 ? "
|
|
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
|
-
|
|
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
|
-
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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
|
-
|
|
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
|
-
<
|
|
385
|
-
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
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 ? "
|
|
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: "
|
|
36
|
-
smtp: "
|
|
37
|
-
api: "
|
|
38
|
-
};
|
|
39
|
-
const NEW_LABEL = {
|
|
40
|
-
sftp: "
|
|
41
|
-
smtp: "
|
|
42
|
-
api: "
|
|
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: "
|
|
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 ?? "
|
|
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
|
|
109
|
+
const tenantSlug = tenant.value;
|
|
109
110
|
const acc = sftp.accounts.value.find((a) => String(a.id) === selectedId.value);
|
|
110
|
-
if (!
|
|
111
|
+
if (!tenantSlug || !acc) return;
|
|
111
112
|
applying.value = true;
|
|
112
113
|
try {
|
|
113
|
-
const creds = await sftp.revealPassword(
|
|
114
|
+
const creds = await sftp.revealPassword(tenantSlug, acc.id);
|
|
114
115
|
if (!creds) {
|
|
115
|
-
toast.error("
|
|
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 ?? "
|
|
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("
|
|
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 ?? "
|
|
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
|
|
182
|
-
if (!
|
|
183
|
-
toast.error("
|
|
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(
|
|
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 = "
|
|
191
|
+
provisioningNote.value = t("integration.credentials.revenexxSource.status.provisioning");
|
|
191
192
|
await sleep(2500);
|
|
192
|
-
acc = await sftp.refresh(
|
|
193
|
-
if (acc.status === "failed") throw new Error(acc.error_message || "
|
|
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("
|
|
197
|
+
throw new Error(t("integration.credentials.revenexxSource.error.stillProvisioning"));
|
|
197
198
|
}
|
|
198
|
-
await sftp.load(
|
|
199
|
+
await sftp.load(tenantSlug);
|
|
199
200
|
selectedId.value = String(acc.id);
|
|
200
201
|
await applySftp();
|
|
201
202
|
createOpen.value = false;
|
|
202
|
-
toast.success("
|
|
203
|
+
toast.success(t("integration.credentials.revenexxSource.toast.sftpCreated"));
|
|
203
204
|
} catch (e) {
|
|
204
|
-
toast.error(e?.message ?? "
|
|
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("
|
|
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("
|
|
231
|
+
toast.success(t("integration.credentials.revenexxSource.toast.smtpCreated"));
|
|
231
232
|
} catch (e) {
|
|
232
|
-
toast.error(e?.message ?? "
|
|
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 =
|
|
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("
|
|
247
|
+
toast.success(t("integration.credentials.revenexxSource.toast.apiKeyCreated"));
|
|
247
248
|
} catch (e) {
|
|
248
|
-
toast.error(e?.message ?? "
|
|
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
|
-
|
|
273
|
+
{{ t("integration.credentials.revenexxSource.hint.available") }}
|
|
273
274
|
</template>
|
|
274
275
|
<template v-else>
|
|
275
|
-
|
|
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`">
|
|
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="
|
|
297
|
+
<SelectValue :placeholder="t('integration.credentials.revenexxSource.field.tenant.placeholder')" />
|
|
298
298
|
</SelectTrigger>
|
|
299
299
|
<SelectContent>
|
|
300
|
-
<SelectItem v-for="
|
|
301
|
-
{{
|
|
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`">
|
|
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="
|
|
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" ? "
|
|
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 ? '
|
|
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
|
-
|
|
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
|
-
|
|
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 ? "
|
|
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] : "
|
|
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
|
-
|
|
369
|
+
{{ t("integration.credentials.revenexxSource.createDialog.description.api", { tenant }) }}
|
|
370
370
|
</template>
|
|
371
371
|
<template v-else-if="kind === 'sftp'">
|
|
372
|
-
|
|
372
|
+
{{ t("integration.credentials.revenexxSource.createDialog.description.sftp", { tenant }) }}
|
|
373
373
|
</template>
|
|
374
374
|
<template v-else>
|
|
375
|
-
|
|
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" ? "
|
|
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' ? '
|
|
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">
|
|
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
|
-
|
|
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 "
|
|
15
|
+
return t("integration.credentials.shared.authKind.apiKey");
|
|
15
16
|
case "oauth2-authcode":
|
|
16
17
|
case "oauth2-client-credentials":
|
|
17
|
-
return "
|
|
18
|
+
return t("integration.credentials.shared.authKind.oauth2");
|
|
18
19
|
case "basic-auth":
|
|
19
|
-
return "
|
|
20
|
+
return t("integration.credentials.shared.authKind.basicAuth");
|
|
20
21
|
case "static":
|
|
21
|
-
return "
|
|
22
|
+
return t("integration.credentials.shared.authKind.static");
|
|
22
23
|
case "secret":
|
|
23
|
-
return "
|
|
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((
|
|
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="
|
|
42
|
+
:aria-label="t('integration.credentials.typePicker.search.aria')"
|
|
42
43
|
class="bg-white pl-9"
|
|
43
|
-
placeholder="
|
|
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
|
-
|
|
51
|
+
{{ t("integration.credentials.typePicker.empty", { query }) }}
|
|
51
52
|
</p>
|
|
52
53
|
<button
|
|
53
54
|
v-for="t in filtered"
|