@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.
- package/CHANGELOG.md +428 -0
- package/README.md +48 -4
- 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 +43 -41
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
- package/dist/runtime/components/Integrations/Layout.vue +48 -4
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
- package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
- package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -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 +38 -23
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +8 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
- package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
- 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 +7 -5
- 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 +14 -7
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
- package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
- 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 +28 -16
- package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
- package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
- package/dist/runtime/composables/useLoadGate.d.ts +17 -0
- package/dist/runtime/composables/useLoadGate.js +8 -0
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
- package/dist/runtime/data/branchReorder.d.ts +37 -0
- package/dist/runtime/data/branchReorder.js +71 -0
- package/dist/runtime/data/handleId.d.ts +23 -0
- package/dist/runtime/data/handleId.js +6 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
- package/dist/runtime/data/integrationNodeCatalog.js +14 -0
- package/dist/runtime/data/integrationTemplates.d.ts +48 -0
- package/dist/runtime/data/integrationTemplates.js +21 -0
- package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
- package/dist/runtime/data/nodeIcons.d.ts +2 -0
- package/dist/runtime/data/nodeIcons.js +73 -0
- package/dist/runtime/data/nodeInputData.d.ts +16 -4
- package/dist/runtime/data/nodeInputData.js +4 -0
- package/dist/runtime/data/payloadForm.d.ts +9 -2
- package/dist/runtime/data/payloadForm.js +29 -4
- package/dist/runtime/data/portTone.d.ts +17 -0
- package/dist/runtime/data/portTone.js +7 -0
- package/dist/runtime/data/webhookAuth.d.ts +18 -0
- package/dist/runtime/data/webhookAuth.js +19 -0
- package/dist/runtime/data/workflowIconCatalog.js +4 -6
- 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 +140 -37
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
- package/dist/runtime/pages/integrations/credentials.vue +180 -64
- package/dist/runtime/pages/integrations/index.vue +91 -92
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +88 -23
- package/dist/runtime/pages/integrations/secrets.vue +57 -35
- package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
- package/dist/runtime/pages/integrations/templates/index.vue +82 -103
- package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -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/types/integrations.d.ts +65 -6
- package/dist/runtime/utils/apiError.d.ts +47 -0
- package/dist/runtime/utils/apiError.js +32 -0
- package/dist/runtime/utils/cockpitSources.d.ts +153 -0
- package/dist/runtime/utils/cockpitSources.js +14 -0
- package/dist/runtime/utils/credentialState.d.ts +24 -1
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/loadGate.d.ts +61 -0
- package/dist/runtime/utils/loadGate.js +22 -0
- package/dist/runtime/utils/localize.d.ts +46 -4
- package/dist/runtime/utils/localize.js +34 -7
- package/dist/runtime/utils/notify.d.ts +59 -0
- package/dist/runtime/utils/notify.js +21 -0
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
- package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
- package/dist/runtime/utils/schedule.d.ts +116 -0
- package/dist/runtime/utils/schedule.js +219 -0
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +101 -0
- package/dist/runtime/utils/validationFindings.js +54 -0
- package/dist/runtime/utils/whenIdle.d.ts +42 -0
- package/dist/runtime/utils/whenIdle.js +12 -0
- package/dist/types.d.mts +2 -6
- 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 "
|
|
34
|
+
return t("integration.credentials.shared.authKind.apiKey");
|
|
39
35
|
case "oauth2-authcode":
|
|
40
36
|
case "oauth2-client-credentials":
|
|
41
|
-
return "
|
|
37
|
+
return t("integration.credentials.shared.authKind.oauth2");
|
|
42
38
|
case "basic-auth":
|
|
43
|
-
return "
|
|
39
|
+
return t("integration.credentials.shared.authKind.basicAuth");
|
|
44
40
|
case "static":
|
|
45
|
-
return "
|
|
41
|
+
return t("integration.credentials.shared.authKind.static");
|
|
46
42
|
case "secret":
|
|
47
|
-
return "
|
|
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
|
|
54
|
-
if (!
|
|
55
|
-
return (Array.isArray(
|
|
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(
|
|
65
|
-
return
|
|
60
|
+
function fieldsOf(t2) {
|
|
61
|
+
return t2?.fields ?? [];
|
|
66
62
|
}
|
|
67
|
-
function isOAuthAuthcode(
|
|
68
|
-
return
|
|
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 =
|
|
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(
|
|
85
|
-
pickedType.value =
|
|
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 =
|
|
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("
|
|
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 ?? "
|
|
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>
|
|
152
|
+
<DialogTitle>{{ t("integration.credentials.shared.dialog.title.create") }}</DialogTitle>
|
|
157
153
|
<DialogDescription class="text-sm">
|
|
158
|
-
|
|
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
|
-
|
|
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">{{
|
|
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">{{
|
|
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
|
-
|
|
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">
|
|
199
|
-
<Input id="cred-name" v-model="form.name" class="bg-white" placeholder="
|
|
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>{{
|
|
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="
|
|
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
|
-
{{
|
|
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
|
-
|
|
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 ? "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
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: "
|
|
38
|
-
{ key: "details", label: "
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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(
|
|
57
|
-
return
|
|
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
|
-
() =>
|
|
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
|
-
|
|
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
|
-
|
|
134
|
+
pendingDelete.value = false;
|
|
117
135
|
deleting.value = true;
|
|
118
136
|
try {
|
|
119
137
|
await remove(props.credential.id);
|
|
120
|
-
toast.success("
|
|
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 ?? "
|
|
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("
|
|
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 ?? "
|
|
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
|
-
|
|
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
|
-
|
|
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">
|
|
211
|
-
<Input
|
|
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>{{
|
|
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="
|
|
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="
|
|
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
|
-
{{
|
|
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 ? "
|
|
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
|
-
|
|
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
|
-
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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
|
-
|
|
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="
|
|
382
|
+
@click="pendingDelete = true"
|
|
331
383
|
>
|
|
332
384
|
<Loader2 v-if="deleting" :size="14" class="mr-1 animate-spin" />
|
|
333
|
-
<
|
|
334
|
-
|
|
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>
|