@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
|
@@ -3,10 +3,16 @@ import { computed, onMounted, reactive, ref } from "vue";
|
|
|
3
3
|
import {
|
|
4
4
|
RefreshCircle as Loader2,
|
|
5
5
|
AddCircle as Plus,
|
|
6
|
-
|
|
6
|
+
TrashBinMinimalistic as TrashBin,
|
|
7
7
|
TestTube as TestIcon
|
|
8
8
|
} from "#solar-icons";
|
|
9
9
|
import {
|
|
10
|
+
AlertDialog,
|
|
11
|
+
AlertDialogContent,
|
|
12
|
+
AlertDialogDescription,
|
|
13
|
+
AlertDialogFooter,
|
|
14
|
+
AlertDialogHeader,
|
|
15
|
+
AlertDialogTitle,
|
|
10
16
|
Badge,
|
|
11
17
|
Button,
|
|
12
18
|
Dialog,
|
|
@@ -24,9 +30,13 @@ import {
|
|
|
24
30
|
SelectTrigger,
|
|
25
31
|
SelectValue
|
|
26
32
|
} from "@revenexx/studio";
|
|
27
|
-
import { toast } from "
|
|
28
|
-
import {
|
|
29
|
-
|
|
33
|
+
import { toast } from "../../utils/notify";
|
|
34
|
+
import { apiFieldErrors, refusalReason } from "../../utils/apiError";
|
|
35
|
+
import { CREDENTIAL_STATES, credentialState, credentialStateLabel } from "../../utils/credentialState";
|
|
36
|
+
import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
|
|
37
|
+
import { resolveLocalized } from "../../utils/localize";
|
|
38
|
+
const t = useIntegrationsT();
|
|
39
|
+
useHead({ title: () => t("integration.credentials.list.head.title") });
|
|
30
40
|
const { credentials, loading, error, load, create, update, remove, testInline, test, oauthAuthorizeUrl } = useIntegrationsCredentials();
|
|
31
41
|
const { types, load: loadTypes, bySlug } = useCredentialTypes();
|
|
32
42
|
onMounted(async () => {
|
|
@@ -35,17 +45,11 @@ onMounted(async () => {
|
|
|
35
45
|
function typeFor(slug) {
|
|
36
46
|
return bySlug(slug);
|
|
37
47
|
}
|
|
38
|
-
function loc(v) {
|
|
39
|
-
if (!v) return "";
|
|
40
|
-
if (typeof v === "string") return v;
|
|
41
|
-
const o = v;
|
|
42
|
-
return o.en ?? o.de ?? Object.values(o)[0] ?? "";
|
|
43
|
-
}
|
|
44
48
|
function typeName(slug) {
|
|
45
|
-
return
|
|
49
|
+
return resolveLocalized(typeFor(slug)?.name) || slug;
|
|
46
50
|
}
|
|
47
|
-
function isOAuthAuthcode(
|
|
48
|
-
return
|
|
51
|
+
function isOAuthAuthcode(t2) {
|
|
52
|
+
return t2?.auth_kind === "oauth2-authcode";
|
|
49
53
|
}
|
|
50
54
|
function formatDateTime(iso) {
|
|
51
55
|
if (!iso) return "\u2014";
|
|
@@ -55,15 +59,32 @@ function formatDateTime(iso) {
|
|
|
55
59
|
return iso;
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
const filterTabs = computed(() => {
|
|
63
|
+
const systems = [...new Set(credentials.value.map((c) => c.credential_type_slug))].map((slug) => ({ value: slug, label: typeName(slug) })).sort((a, b) => a.label.localeCompare(b.label));
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
key: "state",
|
|
67
|
+
label: t("integration.credentials.list.table.column.status"),
|
|
68
|
+
getter: (c) => credentialState(c).key,
|
|
69
|
+
options: CREDENTIAL_STATES.map((key) => ({ value: key, label: credentialStateLabel(key) }))
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "system",
|
|
73
|
+
label: t("integration.credentials.list.table.column.system"),
|
|
74
|
+
getter: (c) => c.credential_type_slug,
|
|
75
|
+
options: systems
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
});
|
|
79
|
+
const columns = computed(() => [
|
|
80
|
+
{ key: "system", label: t("integration.credentials.list.table.column.system"), width: "minmax(160px,220px)", sortable: true, sortGetter: (c) => typeName(c.credential_type_slug).toLowerCase(), searchGetter: (c) => `${typeName(c.credential_type_slug)} ${c.credential_type_slug}` },
|
|
81
|
+
{ key: "name", label: t("integration.credentials.list.table.column.name"), sortable: true, sortGetter: (c) => c.name.toLowerCase(), searchGetter: (c) => c.name },
|
|
82
|
+
{ key: "status", label: t("integration.credentials.list.table.column.status"), width: "150px", sortable: true, sortGetter: (c) => credentialState(c).key },
|
|
83
|
+
{ key: "connected", label: t("integration.credentials.list.table.column.connected"), width: "150px", sortable: true, sortGetter: (c) => c.last_test_ok ? c.last_test_at ?? "" : "" },
|
|
84
|
+
{ key: "created", label: t("integration.credentials.list.table.column.created"), width: "150px", sortable: true, sortGetter: (c) => c.created_at ?? "" },
|
|
85
|
+
{ key: "updated", label: t("integration.credentials.list.table.column.updated"), width: "150px", sortable: true, sortGetter: (c) => c.updated_at ?? "" },
|
|
65
86
|
{ key: "actions", label: "", width: "80px" }
|
|
66
|
-
];
|
|
87
|
+
]);
|
|
67
88
|
const dialogOpen = ref(false);
|
|
68
89
|
const editing = ref(null);
|
|
69
90
|
const pickedType = ref(null);
|
|
@@ -71,17 +92,27 @@ const form = reactive({ name: "", config: {} });
|
|
|
71
92
|
const saving = ref(false);
|
|
72
93
|
const testing = ref(false);
|
|
73
94
|
const testResult = ref(null);
|
|
95
|
+
const saveError = ref(null);
|
|
96
|
+
const fieldErrors = ref({});
|
|
74
97
|
const sourceActive = ref(false);
|
|
75
98
|
const isEdit = computed(() => editing.value !== null);
|
|
76
|
-
function fieldsOf(
|
|
77
|
-
return
|
|
99
|
+
function fieldsOf(t2) {
|
|
100
|
+
return t2?.fields ?? [];
|
|
101
|
+
}
|
|
102
|
+
function configFieldAria(key) {
|
|
103
|
+
return fieldErrors.value[`config.${key}`] ? { "aria-invalid": "true", "aria-describedby": `cred-field-${key}-error` } : {};
|
|
78
104
|
}
|
|
79
|
-
function
|
|
105
|
+
function clearRefusal() {
|
|
106
|
+
saveError.value = null;
|
|
107
|
+
fieldErrors.value = {};
|
|
108
|
+
}
|
|
109
|
+
function openCreate(t2) {
|
|
80
110
|
editing.value = null;
|
|
81
|
-
pickedType.value =
|
|
111
|
+
pickedType.value = t2 ?? null;
|
|
82
112
|
form.name = "";
|
|
83
113
|
form.config = {};
|
|
84
114
|
testResult.value = null;
|
|
115
|
+
clearRefusal();
|
|
85
116
|
sourceActive.value = false;
|
|
86
117
|
dialogOpen.value = true;
|
|
87
118
|
}
|
|
@@ -116,6 +147,7 @@ async function runTest() {
|
|
|
116
147
|
if (!pickedType.value) return;
|
|
117
148
|
testing.value = true;
|
|
118
149
|
testResult.value = null;
|
|
150
|
+
clearRefusal();
|
|
119
151
|
try {
|
|
120
152
|
testResult.value = isEdit.value && editing.value ? await test(editing.value.id) : await testInline(pickedType.value.slug, buildConfigPayload());
|
|
121
153
|
} finally {
|
|
@@ -125,24 +157,28 @@ async function runTest() {
|
|
|
125
157
|
async function submit() {
|
|
126
158
|
if (!pickedType.value || !form.name.trim()) return;
|
|
127
159
|
saving.value = true;
|
|
160
|
+
clearRefusal();
|
|
128
161
|
try {
|
|
129
162
|
if (isEdit.value && editing.value) {
|
|
130
163
|
await update(editing.value.id, { name: form.name.trim(), config: buildConfigPayload() });
|
|
131
|
-
|
|
164
|
+
dialogOpen.value = false;
|
|
165
|
+
toast.success(t("integration.credentials.toast.updated"));
|
|
132
166
|
} else {
|
|
133
167
|
const created = await create({
|
|
134
168
|
credential_type_slug: pickedType.value.slug,
|
|
135
169
|
name: form.name.trim(),
|
|
136
170
|
config: buildConfigPayload()
|
|
137
171
|
});
|
|
138
|
-
|
|
172
|
+
dialogOpen.value = false;
|
|
173
|
+
toast.success(t("integration.credentials.toast.created"));
|
|
139
174
|
if (isOAuthAuthcode(pickedType.value)) {
|
|
140
175
|
await connect(created);
|
|
141
176
|
}
|
|
142
177
|
}
|
|
143
|
-
dialogOpen.value = false;
|
|
144
178
|
} catch (e) {
|
|
145
|
-
|
|
179
|
+
fieldErrors.value = apiFieldErrors(e);
|
|
180
|
+
saveError.value = refusalReason(e, t("integration.credentials.error.save"));
|
|
181
|
+
toast.error(saveError.value);
|
|
146
182
|
} finally {
|
|
147
183
|
saving.value = false;
|
|
148
184
|
}
|
|
@@ -157,7 +193,7 @@ async function connect(c) {
|
|
|
157
193
|
if (ev.data?.type === "integrations-oauth-complete") {
|
|
158
194
|
window.removeEventListener("message", onMessage);
|
|
159
195
|
await load(true);
|
|
160
|
-
toast.success("
|
|
196
|
+
toast.success(t("integration.credentials.toast.connected"));
|
|
161
197
|
}
|
|
162
198
|
};
|
|
163
199
|
window.addEventListener("message", onMessage);
|
|
@@ -169,18 +205,25 @@ async function connect(c) {
|
|
|
169
205
|
}
|
|
170
206
|
}, 1e3);
|
|
171
207
|
} catch (e) {
|
|
172
|
-
toast.error(e?.message ?? "
|
|
208
|
+
toast.error(e?.message ?? t("integration.credentials.error.oauthConnect"));
|
|
173
209
|
} finally {
|
|
174
210
|
connecting.value = null;
|
|
175
211
|
}
|
|
176
212
|
}
|
|
177
|
-
|
|
178
|
-
|
|
213
|
+
const pendingDelete = ref(null);
|
|
214
|
+
const deleting = ref(false);
|
|
215
|
+
async function confirmDelete() {
|
|
216
|
+
const c = pendingDelete.value;
|
|
217
|
+
if (!c) return;
|
|
218
|
+
deleting.value = true;
|
|
179
219
|
try {
|
|
180
220
|
await remove(c.id);
|
|
181
|
-
toast.success("
|
|
221
|
+
toast.success(t("integration.credentials.toast.deleted"));
|
|
222
|
+
pendingDelete.value = null;
|
|
182
223
|
} catch (e) {
|
|
183
|
-
toast.error(e?.message ?? "
|
|
224
|
+
toast.error(e?.message ?? t("integration.credentials.error.delete"));
|
|
225
|
+
} finally {
|
|
226
|
+
deleting.value = false;
|
|
184
227
|
}
|
|
185
228
|
}
|
|
186
229
|
</script>
|
|
@@ -188,13 +231,13 @@ async function destroy(c) {
|
|
|
188
231
|
<template>
|
|
189
232
|
<IntegrationsLayout>
|
|
190
233
|
<PageHeader
|
|
191
|
-
title="
|
|
192
|
-
description="
|
|
234
|
+
:title="t('integration.credentials.list.header.title')"
|
|
235
|
+
:description="t('integration.credentials.list.header.description')"
|
|
193
236
|
>
|
|
194
237
|
<template #actions>
|
|
195
238
|
<Button class="h-7 text-sm" :disabled="!types.length" @click="openCreate()">
|
|
196
239
|
<Plus :size="14" class="mr-1" />
|
|
197
|
-
|
|
240
|
+
{{ t("integration.credentials.list.action.new") }}
|
|
198
241
|
</Button>
|
|
199
242
|
</template>
|
|
200
243
|
</PageHeader>
|
|
@@ -209,10 +252,10 @@ async function destroy(c) {
|
|
|
209
252
|
:columns="columns"
|
|
210
253
|
:row-key="(c2) => c2.id"
|
|
211
254
|
:default-sort="{ column: 'name', direction: 'asc' }"
|
|
212
|
-
:search="{ placeholder: '
|
|
213
|
-
:filter="
|
|
255
|
+
:search="{ placeholder: t('integration.credentials.list.search.placeholder') }"
|
|
256
|
+
:filter-tabs="filterTabs"
|
|
214
257
|
:loading="loading"
|
|
215
|
-
empty-text="
|
|
258
|
+
:empty-text="t('integration.credentials.list.empty', { action: t('integration.credentials.list.action.new') })"
|
|
216
259
|
@row-click="(c2) => openEdit(c2)"
|
|
217
260
|
>
|
|
218
261
|
<template #cell-system="{ row: c }">
|
|
@@ -227,11 +270,15 @@ async function destroy(c) {
|
|
|
227
270
|
</template>
|
|
228
271
|
|
|
229
272
|
<template #cell-status="{ row: c }">
|
|
273
|
+
<!-- The name is part of the accessible label: on its own, "Connected"
|
|
274
|
+
says nothing about *which* credential is, and collides with the
|
|
275
|
+
column header of the same name. -->
|
|
230
276
|
<Badge
|
|
231
277
|
variant="outline"
|
|
232
278
|
class="border text-xs font-medium uppercase"
|
|
233
279
|
:class="credentialState(c).toneClass"
|
|
234
280
|
:title="credentialState(c).message ?? void 0"
|
|
281
|
+
:aria-label="`${c.name}: ${credentialState(c).label}`"
|
|
235
282
|
>
|
|
236
283
|
{{ credentialState(c).label }}
|
|
237
284
|
</Badge>
|
|
@@ -259,12 +306,13 @@ async function destroy(c) {
|
|
|
259
306
|
size="sm"
|
|
260
307
|
class="h-7 text-amber-600"
|
|
261
308
|
:disabled="connecting === c.id"
|
|
309
|
+
:aria-label="t('integration.credentials.list.action.reconnect.aria', { name: c.name })"
|
|
262
310
|
@click="connect(c)"
|
|
263
311
|
>
|
|
264
|
-
|
|
312
|
+
{{ t("integration.credentials.list.action.reconnect") }}
|
|
265
313
|
</Button>
|
|
266
|
-
<button type="button" class="flex size-7 items-center justify-center rounded text-foreground/40 hover:text-rose-600 hover:bg-rose-500/10" title="
|
|
267
|
-
<
|
|
314
|
+
<button type="button" class="flex size-7 items-center justify-center rounded text-foreground/40 hover:text-rose-600 hover:bg-rose-500/10" :title="t('integration.credentials.list.action.delete.aria', { name: c.name })" :aria-label="t('integration.credentials.list.action.delete.aria', { name: c.name })" @click="pendingDelete = c">
|
|
315
|
+
<TrashBin :size="13" />
|
|
268
316
|
</button>
|
|
269
317
|
</div>
|
|
270
318
|
</template>
|
|
@@ -275,9 +323,9 @@ async function destroy(c) {
|
|
|
275
323
|
<Dialog v-model:open="dialogOpen">
|
|
276
324
|
<DialogContent class="sm:max-w-lg">
|
|
277
325
|
<DialogHeader>
|
|
278
|
-
<DialogTitle>{{ isEdit ? "
|
|
326
|
+
<DialogTitle>{{ isEdit ? t("integration.credentials.shared.dialog.title.edit") : t("integration.credentials.shared.dialog.title.create") }}</DialogTitle>
|
|
279
327
|
<DialogDescription class="text-sm">
|
|
280
|
-
{{ isEdit ? "
|
|
328
|
+
{{ isEdit ? t("integration.credentials.list.dialog.description.edit") : t("integration.credentials.list.dialog.description.create") }}
|
|
281
329
|
</DialogDescription>
|
|
282
330
|
</DialogHeader>
|
|
283
331
|
|
|
@@ -285,50 +333,71 @@ async function destroy(c) {
|
|
|
285
333
|
<IntegrationsCredentialTypePicker
|
|
286
334
|
v-if="!pickedType"
|
|
287
335
|
:types="types"
|
|
288
|
-
@pick="(
|
|
289
|
-
pickedType =
|
|
290
|
-
form.name =
|
|
336
|
+
@pick="(picked) => {
|
|
337
|
+
pickedType = picked;
|
|
338
|
+
form.name = resolveLocalized(picked.name);
|
|
339
|
+
clearRefusal();
|
|
291
340
|
}"
|
|
292
341
|
/>
|
|
293
342
|
|
|
294
343
|
<!-- Step 2: dynamic form -->
|
|
295
344
|
<div v-else class="space-y-3 py-1 max-h-[28rem] overflow-y-auto pr-1">
|
|
296
345
|
<div class="space-y-1.5">
|
|
297
|
-
<Label for="cred-name">
|
|
298
|
-
<Input
|
|
346
|
+
<Label for="cred-name">{{ t("integration.credentials.shared.field.name.label") }}</Label>
|
|
347
|
+
<Input
|
|
348
|
+
id="cred-name"
|
|
349
|
+
v-model="form.name"
|
|
350
|
+
class="bg-white"
|
|
351
|
+
:placeholder="t('integration.credentials.list.dialog.field.name.placeholder')"
|
|
352
|
+
:aria-invalid="fieldErrors.name ? 'true' : void 0"
|
|
353
|
+
:aria-describedby="fieldErrors.name ? 'cred-name-error' : void 0"
|
|
354
|
+
/>
|
|
355
|
+
<p v-if="fieldErrors.name" id="cred-name-error" class="text-sm text-rose-700">
|
|
356
|
+
{{ fieldErrors.name }}
|
|
357
|
+
</p>
|
|
299
358
|
</div>
|
|
300
359
|
|
|
301
|
-
<!-- "Use revenexx …" source picker (SFTP / Mail / API key)
|
|
360
|
+
<!-- "Use revenexx …" source picker (SFTP / Mail / API key) — mounted
|
|
361
|
+
only for the types it serves: it resolves cockpit-only sources at
|
|
362
|
+
setup, so mounting it elsewhere would take this form down. -->
|
|
302
363
|
<IntegrationsCredentialRevenexxSource
|
|
364
|
+
v-if="hasRevenexxSource(pickedType?.slug)"
|
|
303
365
|
:type="pickedType"
|
|
304
366
|
@update:active="(v) => sourceActive = v"
|
|
305
367
|
@apply="(c2) => Object.assign(form.config, c2)"
|
|
306
368
|
/>
|
|
307
369
|
|
|
370
|
+
<!-- Each label names its own control (`for`/`id`): without it a screen
|
|
371
|
+
reader reads the field as unlabelled, and the label text cannot be
|
|
372
|
+
used to reach it at all. -->
|
|
308
373
|
<div v-for="f in fieldsOf(pickedType)" v-show="!sourceActive" :key="f.key" class="space-y-1.5">
|
|
309
|
-
<Label>{{
|
|
374
|
+
<Label :for="`cred-field-${f.key}`">{{ resolveLocalized(f.label) }}{{ f.required ? " *" : "" }}</Label>
|
|
310
375
|
|
|
311
376
|
<!-- secret -->
|
|
312
377
|
<Input
|
|
313
378
|
v-if="f.type === 'secret'"
|
|
379
|
+
:id="`cred-field-${f.key}`"
|
|
314
380
|
:model-value="form.config[f.key] ?? ''"
|
|
315
381
|
type="password"
|
|
316
382
|
autocomplete="new-password"
|
|
317
|
-
:placeholder="isEdit ? '
|
|
383
|
+
:placeholder="isEdit ? t('integration.credentials.shared.field.secret.placeholder') : ''"
|
|
318
384
|
class="bg-white"
|
|
385
|
+
v-bind="configFieldAria(f.key)"
|
|
319
386
|
@update:model-value="(v) => form.config[f.key] = v"
|
|
320
387
|
/>
|
|
321
388
|
<!-- number -->
|
|
322
389
|
<Input
|
|
323
390
|
v-else-if="f.type === 'number'"
|
|
391
|
+
:id="`cred-field-${f.key}`"
|
|
324
392
|
type="number"
|
|
325
393
|
:model-value="form.config[f.key] ?? ''"
|
|
326
394
|
class="bg-white"
|
|
395
|
+
v-bind="configFieldAria(f.key)"
|
|
327
396
|
@update:model-value="(v) => form.config[f.key] = v === '' ? void 0 : Number(v)"
|
|
328
397
|
/>
|
|
329
398
|
<!-- boolean -->
|
|
330
399
|
<div v-else-if="f.type === 'boolean'" class="flex items-center gap-2 pt-1">
|
|
331
|
-
<Switch :model-value="Boolean(form.config[f.key])" @update:model-value="(v) => form.config[f.key] = v" />
|
|
400
|
+
<Switch :id="`cred-field-${f.key}`" :model-value="Boolean(form.config[f.key])" v-bind="configFieldAria(f.key)" @update:model-value="(v) => form.config[f.key] = v" />
|
|
332
401
|
</div>
|
|
333
402
|
<!-- select -->
|
|
334
403
|
<Select
|
|
@@ -336,26 +405,36 @@ async function destroy(c) {
|
|
|
336
405
|
:model-value="form.config[f.key] ?? ''"
|
|
337
406
|
@update:model-value="(v) => form.config[f.key] = v"
|
|
338
407
|
>
|
|
339
|
-
<SelectTrigger class="bg-white">
|
|
340
|
-
<SelectValue placeholder="
|
|
408
|
+
<SelectTrigger :id="`cred-field-${f.key}`" class="bg-white" v-bind="configFieldAria(f.key)">
|
|
409
|
+
<SelectValue :placeholder="t('integration.credentials.shared.field.select.placeholder')" />
|
|
341
410
|
</SelectTrigger>
|
|
342
411
|
<SelectContent>
|
|
343
412
|
<SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
|
|
344
|
-
{{
|
|
413
|
+
{{ resolveLocalized(opt.label) }}
|
|
345
414
|
</SelectItem>
|
|
346
415
|
</SelectContent>
|
|
347
416
|
</Select>
|
|
348
417
|
<!-- string (default) -->
|
|
349
418
|
<Input
|
|
350
419
|
v-else
|
|
420
|
+
:id="`cred-field-${f.key}`"
|
|
351
421
|
:model-value="form.config[f.key] ?? ''"
|
|
352
422
|
class="bg-white"
|
|
423
|
+
v-bind="configFieldAria(f.key)"
|
|
353
424
|
@update:model-value="(v) => form.config[f.key] = v"
|
|
354
425
|
/>
|
|
426
|
+
|
|
427
|
+
<!-- The service keys a config refusal as `config.<key>`, which is this
|
|
428
|
+
field. Same rule as the name above: the message belongs where the
|
|
429
|
+
input is, not only in the summary, and the id is what the control's
|
|
430
|
+
`aria-describedby` points at. -->
|
|
431
|
+
<p v-if="fieldErrors[`config.${f.key}`]" :id="`cred-field-${f.key}-error`" class="text-sm text-rose-700">
|
|
432
|
+
{{ fieldErrors[`config.${f.key}`] }}
|
|
433
|
+
</p>
|
|
355
434
|
</div>
|
|
356
435
|
|
|
357
436
|
<p v-if="isOAuthAuthcode(pickedType) && !isEdit" class="text-sm text-muted-foreground">
|
|
358
|
-
|
|
437
|
+
{{ t("integration.credentials.shared.oauthHint") }}
|
|
359
438
|
</p>
|
|
360
439
|
|
|
361
440
|
<div
|
|
@@ -363,30 +442,67 @@ async function destroy(c) {
|
|
|
363
442
|
class="rounded-md px-3 py-2 text-sm"
|
|
364
443
|
:class="testResult.ok ? 'bg-emerald-500/10 text-emerald-700' : 'bg-rose-500/10 text-rose-700'"
|
|
365
444
|
>
|
|
366
|
-
{{ testResult.ok ? "
|
|
445
|
+
{{ testResult.ok ? t("integration.credentials.shared.test.ok") : testResult.error ?? t("integration.credentials.shared.test.failed") }}
|
|
367
446
|
</div>
|
|
447
|
+
|
|
448
|
+
</div>
|
|
449
|
+
|
|
450
|
+
<!-- The save was refused. Outside the scrolling form on purpose: the Save
|
|
451
|
+
button sits in the footer below, so a summary at the end of a form that
|
|
452
|
+
scrolls — five fields already do — would be a refusal rendered under the
|
|
453
|
+
fold, which is the defect this whole change is about. Stated here as well
|
|
454
|
+
as at the field it belongs to, so a refusal with no field — a conflict, a
|
|
455
|
+
gateway error — is not silent either. -->
|
|
456
|
+
<div v-if="saveError && pickedType" role="alert" class="rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
|
|
457
|
+
{{ saveError }}
|
|
368
458
|
</div>
|
|
369
459
|
|
|
370
460
|
<DialogFooter v-if="pickedType" class="sm:justify-between">
|
|
371
|
-
<Button v-if="!isEdit" variant="ghost" size="sm" :disabled="saving || testing" @click="pickedType = null
|
|
372
|
-
|
|
461
|
+
<Button v-if="!isEdit" variant="ghost" size="sm" :disabled="saving || testing" @click="pickedType = null;
|
|
462
|
+
clearRefusal()">
|
|
463
|
+
{{ t("integration.credentials.list.dialog.action.back") }}
|
|
373
464
|
</Button>
|
|
374
465
|
<span v-else />
|
|
375
466
|
<div class="flex items-center gap-2">
|
|
376
467
|
<Button variant="outline" :disabled="saving || testing" @click="runTest">
|
|
377
468
|
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
378
469
|
<TestIcon v-else :size="14" class="mr-1" />
|
|
379
|
-
|
|
470
|
+
{{ t("integration.credentials.shared.action.testConnection") }}
|
|
380
471
|
</Button>
|
|
381
472
|
<Button :disabled="saving || !form.name.trim()" @click="submit">
|
|
382
473
|
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
383
|
-
|
|
474
|
+
{{ t("integration.common.action.save") }}
|
|
384
475
|
</Button>
|
|
385
476
|
</div>
|
|
386
477
|
</DialogFooter>
|
|
387
478
|
</DialogContent>
|
|
388
479
|
</Dialog>
|
|
389
480
|
|
|
481
|
+
<AlertDialog :open="!!pendingDelete" @update:open="(v) => {
|
|
482
|
+
if (!v) pendingDelete = null;
|
|
483
|
+
}">
|
|
484
|
+
<AlertDialogContent>
|
|
485
|
+
<AlertDialogHeader>
|
|
486
|
+
<AlertDialogTitle>{{ t("integration.credentials.shared.confirm.delete.title") }}</AlertDialogTitle>
|
|
487
|
+
<AlertDialogDescription>
|
|
488
|
+
<span v-if="pendingDelete">
|
|
489
|
+
<span class="font-medium text-foreground">{{ pendingDelete.name }}</span>
|
|
490
|
+
{{ t("integration.credentials.shared.confirm.delete.body") }}
|
|
491
|
+
</span>
|
|
492
|
+
</AlertDialogDescription>
|
|
493
|
+
</AlertDialogHeader>
|
|
494
|
+
<AlertDialogFooter>
|
|
495
|
+
<Button variant="outline" :disabled="deleting" @click="pendingDelete = null">
|
|
496
|
+
{{ t("integration.common.action.cancel") }}
|
|
497
|
+
</Button>
|
|
498
|
+
<Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="deleting" @click="confirmDelete">
|
|
499
|
+
<Loader2 v-if="deleting" :size="14" class="animate-spin" />
|
|
500
|
+
{{ deleting ? t("integration.common.state.deleting") : t("integration.common.action.delete") }}
|
|
501
|
+
</Button>
|
|
502
|
+
</AlertDialogFooter>
|
|
503
|
+
</AlertDialogContent>
|
|
504
|
+
</AlertDialog>
|
|
505
|
+
|
|
390
506
|
<!-- Detail / edit view (single-page: connection state + fields + metadata) -->
|
|
391
507
|
<IntegrationsCredentialDetailDialog
|
|
392
508
|
v-model:open="detailOpen"
|