@revenexx/studio-integrations 3.2.0 → 3.3.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 +10 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +34 -6
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +42 -10
- package/dist/runtime/components/Integrations/CredentialNetworkSection.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/CredentialNetworkSection.vue +28 -0
- package/dist/runtime/components/Integrations/CredentialNetworkSection.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +46 -3
- package/dist/runtime/components/Integrations/VpnRoutePicker.d.vue.ts +20 -0
- package/dist/runtime/components/Integrations/VpnRoutePicker.vue +194 -0
- package/dist/runtime/components/Integrations/VpnRoutePicker.vue.d.ts +20 -0
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +15 -9
- package/dist/runtime/composables/useIntegrationsCredentials.js +9 -4
- package/dist/runtime/composables/useVpnConnections.d.ts +19 -0
- package/dist/runtime/composables/useVpnConnections.js +28 -0
- package/dist/runtime/i18n/messages/de.json +35 -0
- package/dist/runtime/i18n/messages/en.json +35 -0
- package/dist/runtime/pages/integrations/credentials.vue +67 -15
- package/dist/runtime/types/integrations.d.ts +58 -1
- package/dist/runtime/utils/configDefaults.d.ts +5 -2
- package/dist/runtime/utils/vpnRoute.d.ts +69 -0
- package/dist/runtime/utils/vpnRoute.js +79 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @revenexx/studio-integrations
|
|
2
2
|
|
|
3
|
+
## 3.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1318abe: A credential can reach its system through a VPN route instead of the public internet. For a credential type that declares which of its fields a route stands in for, both credential forms gain a Network section: public internet, or one of the tenant's VPN connections (with its status) and a route of the kind the type speaks. The fields the route replaces turn read-only and show what they reach; an HTTPS target notes that its certificate is not verified at the gateway; a tenant without connections is pointed at the VPN connections page where the host has one. A credential that goes through a VPN carries a badge in the credentials list and in a node's credential picker, naming the connection, its state when it is not active, and a route that no longer exists. A node setting of the new type `vpn-route` is drawn as the same choice and stores `{ connection, route }`, or nothing for the public internet. Needs the Integrations service's `GET /api/v1/vpn-connections` and the credential `network` field (PO-825).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- f73ff6c: A new credential's form starts from the defaults its system declares. A list read *Select* and a number stood empty even where the system had chosen a value — an FTP credential's encryption, its port — so the screen said nothing had been chosen; they now show that value, and saving without touching them stores it. Picking the system again after going back starts from those defaults instead of the last attempt's values. Both places a credential is added work the same: the credentials list and a node's credential setting. Opening a stored credential shows the default for a field it never stored, rather than an empty field or a switch drawn off; a value it did store still wins, and an untouched form still tests the stored credential.
|
|
12
|
+
|
|
3
13
|
## 3.2.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/dist/module.json
CHANGED
|
@@ -20,10 +20,12 @@ import {
|
|
|
20
20
|
} from "@revenexx/studio";
|
|
21
21
|
import { toast } from "vue-sonner";
|
|
22
22
|
import { resolveLocalized } from "../../utils/localize";
|
|
23
|
+
import { defaultsFor } from "../../utils/configDefaults";
|
|
23
24
|
import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
|
|
24
25
|
import { errorReason } from "../../utils/apiError";
|
|
25
26
|
import { keepOpenBeside } from "../../utils/dialog";
|
|
26
27
|
import { testOutcomeView } from "../../utils/credentialTest";
|
|
28
|
+
import { credentialNetworkPayload, replacedFieldTexts } from "../../utils/vpnRoute";
|
|
27
29
|
const props = defineProps({
|
|
28
30
|
open: { type: Boolean, required: true },
|
|
29
31
|
credentialType: { type: [String, Array], required: false }
|
|
@@ -61,6 +63,13 @@ const sourceActive = ref(false);
|
|
|
61
63
|
const saving = ref(false);
|
|
62
64
|
const testing = ref(false);
|
|
63
65
|
const testResult = ref(null);
|
|
66
|
+
const { connections: vpnConnections } = useVpnConnections();
|
|
67
|
+
const network = ref(null);
|
|
68
|
+
const networkComplete = ref(true);
|
|
69
|
+
const replaced = computed(() => replacedFieldTexts(pickedType.value, network.value, vpnConnections.value));
|
|
70
|
+
function networkPayload() {
|
|
71
|
+
return pickedType.value?.network ? credentialNetworkPayload(network.value) : void 0;
|
|
72
|
+
}
|
|
64
73
|
function fieldsOf(t2) {
|
|
65
74
|
return t2?.fields ?? [];
|
|
66
75
|
}
|
|
@@ -72,10 +81,12 @@ watch(() => props.open, async (o) => {
|
|
|
72
81
|
testResult.value = null;
|
|
73
82
|
sourceActive.value = false;
|
|
74
83
|
form.config = {};
|
|
84
|
+
network.value = null;
|
|
75
85
|
await loadTypes();
|
|
76
86
|
if (allowedTypes.value.length === 1) {
|
|
77
87
|
pickedType.value = allowedTypes.value[0];
|
|
78
88
|
form.name = resolveLocalized(pickedType.value.name);
|
|
89
|
+
form.config = defaultsFor(fieldsOf(pickedType.value));
|
|
79
90
|
} else {
|
|
80
91
|
pickedType.value = null;
|
|
81
92
|
form.name = "";
|
|
@@ -83,9 +94,10 @@ watch(() => props.open, async (o) => {
|
|
|
83
94
|
}, { immediate: true });
|
|
84
95
|
function pickType(t2) {
|
|
85
96
|
pickedType.value = t2;
|
|
86
|
-
form.config =
|
|
97
|
+
form.config = defaultsFor(fieldsOf(t2));
|
|
87
98
|
testResult.value = null;
|
|
88
99
|
sourceActive.value = false;
|
|
100
|
+
network.value = null;
|
|
89
101
|
if (!form.name) form.name = resolveLocalized(t2.name);
|
|
90
102
|
}
|
|
91
103
|
function buildConfigPayload() {
|
|
@@ -101,7 +113,7 @@ async function runTest() {
|
|
|
101
113
|
testing.value = true;
|
|
102
114
|
testResult.value = null;
|
|
103
115
|
try {
|
|
104
|
-
testResult.value = await testInline(pickedType.value.slug, buildConfigPayload());
|
|
116
|
+
testResult.value = await testInline(pickedType.value.slug, buildConfigPayload(), networkPayload());
|
|
105
117
|
} finally {
|
|
106
118
|
testing.value = false;
|
|
107
119
|
}
|
|
@@ -119,7 +131,8 @@ async function submit() {
|
|
|
119
131
|
const created = await create({
|
|
120
132
|
credential_type_slug: pickedType.value.slug,
|
|
121
133
|
name: form.name.trim(),
|
|
122
|
-
config: buildConfigPayload()
|
|
134
|
+
config: buildConfigPayload(),
|
|
135
|
+
network: networkPayload()
|
|
123
136
|
});
|
|
124
137
|
await load(true);
|
|
125
138
|
toast.success(t("integration.credentials.toast.created"));
|
|
@@ -193,13 +206,28 @@ async function submit() {
|
|
|
193
206
|
@apply="(c) => Object.assign(form.config, c)"
|
|
194
207
|
/>
|
|
195
208
|
|
|
209
|
+
<IntegrationsCredentialNetworkSection
|
|
210
|
+
v-model="network"
|
|
211
|
+
:type="pickedType"
|
|
212
|
+
id-prefix="create-network"
|
|
213
|
+
@update:complete="(c) => networkComplete = c"
|
|
214
|
+
/>
|
|
215
|
+
|
|
196
216
|
<!-- Each label names its own control (`for`/`id`): without it a screen
|
|
197
217
|
reader reads the field as unlabelled, and the label text cannot be
|
|
198
218
|
used to reach it at all. -->
|
|
199
219
|
<div v-for="f in fieldsOf(pickedType)" v-show="!sourceActive" :key="f.key" class="space-y-1.5">
|
|
200
220
|
<Label :for="`create-field-${f.key}`">{{ resolveLocalized(f.label) }}{{ f.required ? " *" : "" }}</Label>
|
|
221
|
+
<!-- stood in for by the chosen VPN route: what it reaches, not editable -->
|
|
222
|
+
<Input
|
|
223
|
+
v-if="replaced[f.key]"
|
|
224
|
+
:id="`create-field-${f.key}`"
|
|
225
|
+
:model-value="replaced[f.key]"
|
|
226
|
+
readonly
|
|
227
|
+
class="bg-muted/40"
|
|
228
|
+
/>
|
|
201
229
|
<Input
|
|
202
|
-
v-if="f.type === 'secret'"
|
|
230
|
+
v-else-if="f.type === 'secret'"
|
|
203
231
|
:id="`create-field-${f.key}`"
|
|
204
232
|
:model-value="form.config[f.key] ?? ''"
|
|
205
233
|
type="password"
|
|
@@ -259,12 +287,12 @@ async function submit() {
|
|
|
259
287
|
|
|
260
288
|
<DialogFooter v-if="pickedType">
|
|
261
289
|
<div class="flex items-center gap-2">
|
|
262
|
-
<Button variant="outline" :disabled="saving || testing" @click="runTest">
|
|
290
|
+
<Button variant="outline" :disabled="saving || testing || !networkComplete" @click="runTest">
|
|
263
291
|
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
264
292
|
<TestIcon v-else :size="14" class="mr-1" />
|
|
265
293
|
{{ t("integration.credentials.shared.action.testConnection") }}
|
|
266
294
|
</Button>
|
|
267
|
-
<Button :disabled="saving || !form.name.trim()" @click="submit">
|
|
295
|
+
<Button :disabled="saving || !form.name.trim() || !networkComplete" @click="submit">
|
|
268
296
|
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
269
297
|
{{ t("integration.common.action.save") }}
|
|
270
298
|
</Button>
|
|
@@ -33,6 +33,7 @@ import { toast } from "../../utils/notify";
|
|
|
33
33
|
import { apiRefusal, errorReason } from "../../utils/apiError";
|
|
34
34
|
import { credentialState } from "../../utils/credentialState";
|
|
35
35
|
import { credentialEditDiffers } from "../../utils/credentialEdit";
|
|
36
|
+
import { defaultsFor } from "../../utils/configDefaults";
|
|
36
37
|
import { relativeTime } from "../../utils/relativeTime";
|
|
37
38
|
import { useNow } from "../../composables/useNow";
|
|
38
39
|
import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
|
|
@@ -40,6 +41,7 @@ import { resolveLocalized } from "../../utils/localize";
|
|
|
40
41
|
import { fromModelValue, toItemValue, toModelValue } from "../../utils/selectOptions";
|
|
41
42
|
import { keepOpenBeside } from "../../utils/dialog";
|
|
42
43
|
import { testOutcomeView } from "../../utils/credentialTest";
|
|
44
|
+
import { credentialNetworkPayload, fromCredentialNetwork, replacedFieldTexts, sameVpnRoute } from "../../utils/vpnRoute";
|
|
43
45
|
const props = defineProps({
|
|
44
46
|
open: { type: Boolean, required: true },
|
|
45
47
|
credential: { type: [Object, null], required: true },
|
|
@@ -61,8 +63,17 @@ const deleting = ref(false);
|
|
|
61
63
|
const testing = ref(false);
|
|
62
64
|
const liveTest = ref(null);
|
|
63
65
|
const saveRefusal = ref(null);
|
|
66
|
+
const { connections: vpnConnections } = useVpnConnections();
|
|
67
|
+
const network = ref(null);
|
|
68
|
+
const networkComplete = ref(true);
|
|
69
|
+
const replaced = computed(() => replacedFieldTexts(props.type, network.value, vpnConnections.value));
|
|
70
|
+
const networkChanged = computed(() => !!props.type?.network && !sameVpnRoute(network.value, fromCredentialNetwork(props.credential?.network)));
|
|
71
|
+
function networkPayload() {
|
|
72
|
+
return networkChanged.value ? credentialNetworkPayload(network.value) : void 0;
|
|
73
|
+
}
|
|
64
74
|
const drawnKeys = computed(() => [
|
|
65
75
|
"name",
|
|
76
|
+
...props.type?.network ? ["network"] : [],
|
|
66
77
|
...sourceActive.value ? [] : fieldsOf(props.type).map((f) => `config.${f.key}`)
|
|
67
78
|
]);
|
|
68
79
|
const refusal = computed(() => apiRefusal(saveRefusal.value, drawnKeys.value));
|
|
@@ -80,7 +91,7 @@ async function focusFirstRefusedField() {
|
|
|
80
91
|
await nextTick();
|
|
81
92
|
const key = drawnKeys.value.find((k) => k in fieldErrors.value);
|
|
82
93
|
if (!key) return;
|
|
83
|
-
const id = key === "name" ? "detail-cred-name" : `detail-field-${key.slice("config.".length)}`;
|
|
94
|
+
const id = key === "name" ? "detail-cred-name" : key === "network" ? "detail-network-public" : `detail-field-${key.slice("config.".length)}`;
|
|
84
95
|
document.getElementById(id)?.focus();
|
|
85
96
|
}
|
|
86
97
|
function clearRefusal() {
|
|
@@ -102,14 +113,16 @@ function formatDateTime(iso) {
|
|
|
102
113
|
}
|
|
103
114
|
}
|
|
104
115
|
let viewOpening = 0;
|
|
116
|
+
const storedConfig = computed(() => ({ ...defaultsFor(fieldsOf(props.type)), ...props.credential?.public_config ?? {} }));
|
|
105
117
|
watch(
|
|
106
|
-
[() => props.open, () => props.credential?.id],
|
|
118
|
+
[() => props.open, () => props.credential?.id, () => props.type?.slug],
|
|
107
119
|
() => {
|
|
108
120
|
viewOpening++;
|
|
109
121
|
if (!props.open || !props.credential) return;
|
|
110
122
|
tab.value = "credentials";
|
|
111
123
|
form.name = props.credential.name;
|
|
112
|
-
form.config = { ...
|
|
124
|
+
form.config = { ...storedConfig.value };
|
|
125
|
+
network.value = fromCredentialNetwork(props.credential.network);
|
|
113
126
|
sourceActive.value = false;
|
|
114
127
|
liveTest.value = null;
|
|
115
128
|
clearRefusal();
|
|
@@ -133,8 +146,8 @@ async function runTest() {
|
|
|
133
146
|
clearRefusal();
|
|
134
147
|
try {
|
|
135
148
|
const payload = buildConfigPayload();
|
|
136
|
-
const edited = credentialEditDiffers(fieldsOf(props.type), payload,
|
|
137
|
-
const result = await test(props.credential.id, edited ? payload : void 0);
|
|
149
|
+
const edited = credentialEditDiffers(fieldsOf(props.type), payload, storedConfig.value) || networkChanged.value;
|
|
150
|
+
const result = await test(props.credential.id, edited ? payload : void 0, edited ? networkPayload() : void 0);
|
|
138
151
|
if (result.refusal) {
|
|
139
152
|
if (opening !== viewOpening) return;
|
|
140
153
|
saveRefusal.value = result.refusal;
|
|
@@ -161,10 +174,10 @@ async function save() {
|
|
|
161
174
|
clearRefusal();
|
|
162
175
|
const id = props.credential.id;
|
|
163
176
|
const payload = buildConfigPayload();
|
|
164
|
-
const edited = credentialEditDiffers(fieldsOf(props.type), payload,
|
|
177
|
+
const edited = credentialEditDiffers(fieldsOf(props.type), payload, storedConfig.value) || networkChanged.value;
|
|
165
178
|
try {
|
|
166
179
|
const wasConnected = props.credential.status !== "needs_reauth";
|
|
167
|
-
const updated = await update(id, { name: form.name.trim(), config: payload });
|
|
180
|
+
const updated = await update(id, { name: form.name.trim(), config: payload, network: networkPayload() });
|
|
168
181
|
const lostConsent = wasConnected && updated.status === "needs_reauth";
|
|
169
182
|
emit("changed");
|
|
170
183
|
emit("update:open", false);
|
|
@@ -315,14 +328,33 @@ async function connect() {
|
|
|
315
328
|
@apply="(c) => Object.assign(form.config, c)"
|
|
316
329
|
/>
|
|
317
330
|
|
|
331
|
+
<!-- Keyed by the credential: the picker holds a half-made choice of
|
|
332
|
+
its own, which belongs to the credential it was made on. -->
|
|
333
|
+
<IntegrationsCredentialNetworkSection
|
|
334
|
+
:key="credential.id"
|
|
335
|
+
v-model="network"
|
|
336
|
+
:type="type"
|
|
337
|
+
id-prefix="detail-network"
|
|
338
|
+
:error="fieldErrors.network"
|
|
339
|
+
@update:complete="(c) => networkComplete = c"
|
|
340
|
+
/>
|
|
341
|
+
|
|
318
342
|
<!-- Each label names its own control (`for`/`id`): without it a
|
|
319
343
|
screen reader reads the field as unlabelled, and the label text
|
|
320
344
|
cannot be used to reach it at all. -->
|
|
321
345
|
<div v-for="f in fieldsOf(type)" v-show="!sourceActive" :key="f.key" class="space-y-1.5">
|
|
322
346
|
<Label :for="`detail-field-${f.key}`">{{ resolveLocalized(f.label) }}{{ f.required ? " *" : "" }}</Label>
|
|
323
347
|
|
|
348
|
+
<!-- stood in for by the chosen VPN route: what it reaches, not editable -->
|
|
349
|
+
<Input
|
|
350
|
+
v-if="replaced[f.key]"
|
|
351
|
+
:id="`detail-field-${f.key}`"
|
|
352
|
+
:model-value="replaced[f.key]"
|
|
353
|
+
readonly
|
|
354
|
+
class="bg-muted/40"
|
|
355
|
+
/>
|
|
324
356
|
<Input
|
|
325
|
-
v-if="f.type === 'secret'"
|
|
357
|
+
v-else-if="f.type === 'secret'"
|
|
326
358
|
:id="`detail-field-${f.key}`"
|
|
327
359
|
:model-value="form.config[f.key] ?? ''"
|
|
328
360
|
type="password"
|
|
@@ -405,12 +437,12 @@ async function connect() {
|
|
|
405
437
|
<Button variant="ghost" :disabled="saving" @click="emit('update:open', false)">
|
|
406
438
|
{{ t("integration.common.action.cancel") }}
|
|
407
439
|
</Button>
|
|
408
|
-
<Button variant="outline" :disabled="testing || saving" @click="runTest">
|
|
440
|
+
<Button variant="outline" :disabled="testing || saving || !networkComplete" @click="runTest">
|
|
409
441
|
<Loader2 v-if="testing" :size="14" class="mr-1 animate-spin" />
|
|
410
442
|
<TestIcon v-else :size="14" class="mr-1" />
|
|
411
443
|
{{ t("integration.credentials.shared.action.testConnection") }}
|
|
412
444
|
</Button>
|
|
413
|
-
<Button :disabled="saving || !form.name.trim()" @click="save">
|
|
445
|
+
<Button :disabled="saving || !form.name.trim() || !networkComplete" @click="save">
|
|
414
446
|
<Loader2 v-if="saving" :size="14" class="mr-1 animate-spin" />
|
|
415
447
|
{{ t("integration.common.action.save") }}
|
|
416
448
|
</Button>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Network section of a credential form: how the credential reaches its system —
|
|
3
|
+
* over the public internet, or through a route of one of the tenant's VPN
|
|
4
|
+
* connections (specs/credential-network.md). Drawn only for a type that declares
|
|
5
|
+
* which of its fields a route stands in for; every other type has nothing to choose.
|
|
6
|
+
*/
|
|
7
|
+
import type { CredentialType, VpnRouteValue } from '../../types/integrations.js';
|
|
8
|
+
type __VLS_Props = {
|
|
9
|
+
type: CredentialType | null;
|
|
10
|
+
modelValue: VpnRouteValue | null;
|
|
11
|
+
idPrefix: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
};
|
|
14
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
|
+
"update:complete": (args_0: boolean) => any;
|
|
16
|
+
"update:modelValue": (args_0: VpnRouteValue | null) => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
|
+
"onUpdate:complete"?: ((args_0: boolean) => any) | undefined;
|
|
19
|
+
"onUpdate:modelValue"?: ((args_0: VpnRouteValue | null) => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({
|
|
3
|
+
type: { type: [Object, null], required: true },
|
|
4
|
+
modelValue: { type: [Object, null], required: true },
|
|
5
|
+
idPrefix: { type: String, required: true },
|
|
6
|
+
error: { type: String, required: false }
|
|
7
|
+
});
|
|
8
|
+
const emit = defineEmits(["update:modelValue", "update:complete"]);
|
|
9
|
+
const t = useIntegrationsT();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<section
|
|
14
|
+
v-if="type?.network"
|
|
15
|
+
:aria-labelledby="`${idPrefix}-heading`"
|
|
16
|
+
class="space-y-2 rounded-lg border border-black/[0.08] p-3"
|
|
17
|
+
>
|
|
18
|
+
<h3 :id="`${idPrefix}-heading`" class="text-sm font-semibold">{{ t("integration.vpn.section.title") }}</h3>
|
|
19
|
+
<IntegrationsVpnRoutePicker
|
|
20
|
+
:model-value="modelValue"
|
|
21
|
+
:kind="type.network.kind"
|
|
22
|
+
:id-prefix="idPrefix"
|
|
23
|
+
:error="error"
|
|
24
|
+
@update:model-value="(v) => emit('update:modelValue', v)"
|
|
25
|
+
@update:complete="(c) => emit('update:complete', c)"
|
|
26
|
+
/>
|
|
27
|
+
</section>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Network section of a credential form: how the credential reaches its system —
|
|
3
|
+
* over the public internet, or through a route of one of the tenant's VPN
|
|
4
|
+
* connections (specs/credential-network.md). Drawn only for a type that declares
|
|
5
|
+
* which of its fields a route stands in for; every other type has nothing to choose.
|
|
6
|
+
*/
|
|
7
|
+
import type { CredentialType, VpnRouteValue } from '../../types/integrations.js';
|
|
8
|
+
type __VLS_Props = {
|
|
9
|
+
type: CredentialType | null;
|
|
10
|
+
modelValue: VpnRouteValue | null;
|
|
11
|
+
idPrefix: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
};
|
|
14
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
|
+
"update:complete": (args_0: boolean) => any;
|
|
16
|
+
"update:modelValue": (args_0: VpnRouteValue | null) => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
|
+
"onUpdate:complete"?: ((args_0: boolean) => any) | undefined;
|
|
19
|
+
"onUpdate:modelValue"?: ((args_0: VpnRouteValue | null) => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
export default _default;
|
|
@@ -20,6 +20,7 @@ import { credentialState } from "../../utils/credentialState";
|
|
|
20
20
|
import { defaultsFor } from "../../utils/configDefaults";
|
|
21
21
|
import { applyingFields, settingApplies, staleKeys } from "../../utils/settingCondition";
|
|
22
22
|
import { coerceSettingValue } from "../../utils/settingValue";
|
|
23
|
+
import { credentialVpnBadge, isVpnRouteValue } from "../../utils/vpnRoute";
|
|
23
24
|
const props = defineProps({
|
|
24
25
|
field: { type: Object, required: true },
|
|
25
26
|
config: { type: Object, required: true },
|
|
@@ -110,6 +111,14 @@ const { credentials, loaded: credentialsLoaded, load: loadCredentials, refresh:
|
|
|
110
111
|
onMounted(() => {
|
|
111
112
|
void loadCredentials();
|
|
112
113
|
});
|
|
114
|
+
const { connections: vpnConnections, loaded: vpnLoaded, load: loadVpn } = useVpnConnections();
|
|
115
|
+
watch(
|
|
116
|
+
() => props.field.type === "credentials-ref" && credsForField(props.field).some((c) => c.network),
|
|
117
|
+
(needed) => {
|
|
118
|
+
if (needed) void loadVpn();
|
|
119
|
+
},
|
|
120
|
+
{ immediate: true }
|
|
121
|
+
);
|
|
113
122
|
function onCredentialPickerOpen(open) {
|
|
114
123
|
if (open) void refreshCredentials();
|
|
115
124
|
}
|
|
@@ -132,14 +141,15 @@ function credsForField(field) {
|
|
|
132
141
|
const bound = computed(() => {
|
|
133
142
|
const id = value.value;
|
|
134
143
|
const c = typeof id === "string" && id ? credentials.value.find((x) => x.id === id) : void 0;
|
|
135
|
-
return c ? { credential: c, state: credentialState(c) } : null;
|
|
144
|
+
return c ? { credential: c, state: credentialState(c), vpn: credentialVpnBadge(c.network, vpnConnections.value, vpnLoaded.value) } : null;
|
|
136
145
|
});
|
|
137
146
|
const credOptions = computed(
|
|
138
|
-
() => credsForField(props.field).map((c) => ({ credential: c, state: credentialState(c) }))
|
|
147
|
+
() => credsForField(props.field).map((c) => ({ credential: c, state: credentialState(c), vpn: credentialVpnBadge(c.network, vpnConnections.value, vpnLoaded.value) }))
|
|
139
148
|
);
|
|
140
149
|
const UNKNOWN_CREDENTIAL = computed(() => t("integration.nodeConfig.credential.unknown"));
|
|
141
150
|
const credTriggerLabel = computed(() => {
|
|
142
151
|
const label = controlName.value;
|
|
152
|
+
if (bound.value?.vpn) return t("integration.nodeConfig.credential.trigger.boundVpn", { label, name: bound.value.credential.name, state: bound.value.state.label, vpn: bound.value.vpn.text });
|
|
143
153
|
if (bound.value) return t("integration.nodeConfig.credential.trigger.bound", { label, name: bound.value.credential.name, state: bound.value.state.label });
|
|
144
154
|
if (value.value && credentialsLoaded.value) return t("integration.nodeConfig.credential.trigger.unknown", { label, unknown: UNKNOWN_CREDENTIAL.value });
|
|
145
155
|
return label;
|
|
@@ -394,8 +404,21 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
394
404
|
inside string values). Wins over expressionAllowed — an object value
|
|
395
405
|
in a text input would render as [object Object]. -->
|
|
396
406
|
<!-- queries parameter: filter builder -->
|
|
407
|
+
<!-- vpn-route — public internet, or a route of one of the tenant's VPN
|
|
408
|
+
connections, of the kind the node asks for (specs/credential-network.md
|
|
409
|
+
AC-8). Ahead of every other branch: its value is an object, which the JSON
|
|
410
|
+
and expression branches below would otherwise claim. -->
|
|
411
|
+
<IntegrationsVpnRoutePicker
|
|
412
|
+
v-if="field.type === 'vpn-route'"
|
|
413
|
+
:model-value="isVpnRouteValue(value) ? { connection: value.connection, route: value.route } : null"
|
|
414
|
+
:kind="field.routeKind ?? 'http'"
|
|
415
|
+
:id-prefix="`vpn-${field.key}-${rowIndex ?? 0}`"
|
|
416
|
+
:label="controlName"
|
|
417
|
+
@update:model-value="(v) => emit('set', field.key, v)"
|
|
418
|
+
/>
|
|
419
|
+
|
|
397
420
|
<IntegrationsQueryBuilder
|
|
398
|
-
v-if="isQueryField"
|
|
421
|
+
v-else-if="isQueryField"
|
|
399
422
|
:model-value="value"
|
|
400
423
|
@update:model-value="(v) => write(v)"
|
|
401
424
|
/>
|
|
@@ -651,6 +674,13 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
651
674
|
>
|
|
652
675
|
{{ bound.state.label }}
|
|
653
676
|
</span>
|
|
677
|
+
<span
|
|
678
|
+
v-if="bound.vpn"
|
|
679
|
+
class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium"
|
|
680
|
+
:class="bound.vpn.tone"
|
|
681
|
+
>
|
|
682
|
+
{{ bound.vpn.text }}
|
|
683
|
+
</span>
|
|
654
684
|
</span>
|
|
655
685
|
<!-- A stored id the tenant's credentials no longer contain: the
|
|
656
686
|
credential was deleted, and the field would otherwise look untouched
|
|
@@ -677,6 +707,13 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
677
707
|
:class="o.state.toneClass"
|
|
678
708
|
:title="o.state.message ?? void 0"
|
|
679
709
|
>{{ o.state.label }}</span>
|
|
710
|
+
<!-- Reached through a VPN route: the connection, and its state where it
|
|
711
|
+
is not carrying traffic (specs/credential-network.md AC-6). -->
|
|
712
|
+
<span
|
|
713
|
+
v-if="o.vpn"
|
|
714
|
+
class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium"
|
|
715
|
+
:class="o.vpn.tone"
|
|
716
|
+
>{{ o.vpn.text }}</span>
|
|
680
717
|
</span>
|
|
681
718
|
</SelectItem>
|
|
682
719
|
<div v-if="!credOptions.length" class="px-2 py-1.5 text-sm text-muted-foreground">
|
|
@@ -699,6 +736,12 @@ function setArrayItemField(index, fieldKey, v) {
|
|
|
699
736
|
<p v-else-if="bound?.state.key === 'failed' && bound.state.message" class="text-sm break-words text-rose-600">
|
|
700
737
|
{{ t("integration.nodeConfig.credential.state.failed", { message: bound.state.message }) }}
|
|
701
738
|
</p>
|
|
739
|
+
<p v-if="bound?.vpn?.broken && bound.credential.network" class="text-sm break-words text-rose-600">
|
|
740
|
+
{{ t("integration.vpn.reference.missing", { connection: bound.credential.network.connection, route: bound.credential.network.route }) }}
|
|
741
|
+
</p>
|
|
742
|
+
<p v-else-if="bound?.vpn && !bound.vpn.active" class="text-sm text-amber-600">
|
|
743
|
+
{{ t("integration.nodeConfig.credential.vpnInactive") }}
|
|
744
|
+
</p>
|
|
702
745
|
<IntegrationsCredentialCreateDialog
|
|
703
746
|
v-model:open="credCreateOpen"
|
|
704
747
|
:credential-type="field.credentialType"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { VpnRouteKind, VpnRouteValue } from '../../types/integrations.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
modelValue: VpnRouteValue | null;
|
|
4
|
+
/** The kind of route on offer — the credential type's declared kind, or the setting's `routeKind`. */
|
|
5
|
+
kind: VpnRouteKind;
|
|
6
|
+
/** Prefix for the ids of the controls, unique on the page. */
|
|
7
|
+
idPrefix: string;
|
|
8
|
+
/** The setting this picker belongs to, prefixed to every control's name; absent in the credential forms. */
|
|
9
|
+
label?: string;
|
|
10
|
+
/** Why the last save refused the choice, if it did. */
|
|
11
|
+
error?: string;
|
|
12
|
+
};
|
|
13
|
+
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
14
|
+
"update:complete": (args_0: boolean) => any;
|
|
15
|
+
"update:modelValue": (args_0: VpnRouteValue | null) => any;
|
|
16
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
17
|
+
"onUpdate:complete"?: ((args_0: boolean) => any) | undefined;
|
|
18
|
+
"onUpdate:modelValue"?: ((args_0: VpnRouteValue | null) => any) | undefined;
|
|
19
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
|
+
export default _default;
|