@revenexx/studio-integrations 3.1.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 +22 -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/RunDetailPanel.vue +3 -2
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue +28 -24
- 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/components/Integrations/WorkflowRunReplayPanel.vue +14 -2
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +15 -9
- package/dist/runtime/composables/useIntegrationsCredentials.js +9 -4
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +2 -0
- package/dist/runtime/composables/useIntegrationsTemplates.js +15 -9
- package/dist/runtime/composables/useVpnConnections.d.ts +19 -0
- package/dist/runtime/composables/useVpnConnections.js +28 -0
- package/dist/runtime/i18n/messages/de.json +39 -0
- package/dist/runtime/i18n/messages/en.json +39 -0
- package/dist/runtime/pages/integrations/credentials.vue +67 -15
- package/dist/runtime/pages/integrations/templates/[slug].vue +21 -22
- package/dist/runtime/types/integrations.d.ts +74 -2
- package/dist/runtime/utils/configDefaults.d.ts +5 -2
- package/dist/runtime/utils/stepError.d.ts +13 -0
- package/dist/runtime/utils/stepError.js +24 -0
- package/dist/runtime/utils/vpnRoute.d.ts +69 -0
- package/dist/runtime/utils/vpnRoute.js +79 -0
- package/package.json +1 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Label,
|
|
6
|
+
RadioGroup,
|
|
7
|
+
RadioGroupItem,
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue
|
|
13
|
+
} from "@revenexx/studio";
|
|
14
|
+
import {
|
|
15
|
+
connectionLabel,
|
|
16
|
+
connectionStatusLabel,
|
|
17
|
+
connectionStatusTone,
|
|
18
|
+
isHttpsTarget,
|
|
19
|
+
resolveVpnReference,
|
|
20
|
+
routesOfKind
|
|
21
|
+
} from "../../utils/vpnRoute";
|
|
22
|
+
import { servedPaths } from "../../utils/servedPaths";
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
modelValue: { type: [Object, null], required: true },
|
|
25
|
+
kind: { type: String, required: true },
|
|
26
|
+
idPrefix: { type: String, required: true },
|
|
27
|
+
label: { type: String, required: false },
|
|
28
|
+
error: { type: String, required: false }
|
|
29
|
+
});
|
|
30
|
+
const emit = defineEmits(["update:modelValue", "update:complete"]);
|
|
31
|
+
const t = useIntegrationsT();
|
|
32
|
+
const { connections, loaded, loading, error: loadError, load } = useVpnConnections();
|
|
33
|
+
onMounted(() => {
|
|
34
|
+
void load(true);
|
|
35
|
+
});
|
|
36
|
+
const viaVpn = ref(!!props.modelValue);
|
|
37
|
+
const pickedConnection = ref(props.modelValue?.connection ?? "");
|
|
38
|
+
watch(() => props.modelValue ? `${props.modelValue.connection}\0${props.modelValue.route}` : "", () => {
|
|
39
|
+
const v = props.modelValue;
|
|
40
|
+
if (v) {
|
|
41
|
+
viaVpn.value = true;
|
|
42
|
+
pickedConnection.value = v.connection;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const current = computed(() => props.modelValue?.connection === pickedConnection.value ? props.modelValue : null);
|
|
46
|
+
const complete = computed(() => !viaVpn.value || !!current.value);
|
|
47
|
+
watch(complete, (c) => emit("update:complete", c), { immediate: true });
|
|
48
|
+
onUnmounted(() => emit("update:complete", true));
|
|
49
|
+
function name(key) {
|
|
50
|
+
const base = t(key);
|
|
51
|
+
return props.label ? t("integration.vpn.picker.ariaFor", { label: props.label, control: base }) : base;
|
|
52
|
+
}
|
|
53
|
+
function setMode(mode) {
|
|
54
|
+
if (mode === "vpn") {
|
|
55
|
+
viaVpn.value = true;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
viaVpn.value = false;
|
|
59
|
+
pickedConnection.value = "";
|
|
60
|
+
emit("update:modelValue", null);
|
|
61
|
+
}
|
|
62
|
+
const connection = computed(() => connections.value.find((c) => c.name === pickedConnection.value));
|
|
63
|
+
const routes = computed(() => routesOfKind(connection.value, props.kind));
|
|
64
|
+
function pickConnection(v) {
|
|
65
|
+
const next = String(v ?? "");
|
|
66
|
+
if (next === pickedConnection.value) return;
|
|
67
|
+
pickedConnection.value = next;
|
|
68
|
+
}
|
|
69
|
+
function pickRoute(v) {
|
|
70
|
+
const route = String(v ?? "");
|
|
71
|
+
if (!route || !pickedConnection.value) return;
|
|
72
|
+
emit("update:modelValue", { connection: pickedConnection.value, route });
|
|
73
|
+
}
|
|
74
|
+
const reference = computed(() => current.value && loaded.value ? resolveVpnReference(connections.value, current.value) : null);
|
|
75
|
+
const chosenRoute = computed(() => reference.value?.state === "ok" ? reference.value.route : void 0);
|
|
76
|
+
const noConnections = computed(() => loaded.value && !connections.value.length);
|
|
77
|
+
const pending = computed(() => {
|
|
78
|
+
if (complete.value) return "";
|
|
79
|
+
const stored = props.modelValue;
|
|
80
|
+
if (!stored) return t("integration.vpn.picker.pending.public");
|
|
81
|
+
const via = connections.value.find((c) => c.name === stored.connection);
|
|
82
|
+
return t("integration.vpn.picker.pending.route", { route: stored.route, connection: via ? connectionLabel(via) : stored.connection });
|
|
83
|
+
});
|
|
84
|
+
const VPN_PAGE = "/integrations/vpn";
|
|
85
|
+
const router = useRouter();
|
|
86
|
+
function vpnPageServed() {
|
|
87
|
+
return servedPaths(router.getRoutes().map((r) => r.path)).has(VPN_PAGE);
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<div class="space-y-2">
|
|
93
|
+
<RadioGroup
|
|
94
|
+
:model-value="viaVpn ? 'vpn' : 'public'"
|
|
95
|
+
:aria-label="name('integration.vpn.picker.group')"
|
|
96
|
+
class="gap-1.5"
|
|
97
|
+
@update:model-value="(v) => setMode(String(v))"
|
|
98
|
+
>
|
|
99
|
+
<div class="flex items-center gap-2">
|
|
100
|
+
<RadioGroupItem :id="`${idPrefix}-public`" value="public" />
|
|
101
|
+
<Label :for="`${idPrefix}-public`" class="font-normal">{{ t("integration.vpn.picker.public") }}</Label>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="flex items-center gap-2">
|
|
104
|
+
<!-- Not offered where there is nothing to pick from — unless a stored route
|
|
105
|
+
already points through one, which has to stay readable as such. -->
|
|
106
|
+
<RadioGroupItem :id="`${idPrefix}-vpn`" value="vpn" :disabled="noConnections && !modelValue" />
|
|
107
|
+
<Label :for="`${idPrefix}-vpn`" class="font-normal">{{ t("integration.vpn.picker.vpn") }}</Label>
|
|
108
|
+
</div>
|
|
109
|
+
</RadioGroup>
|
|
110
|
+
|
|
111
|
+
<p v-if="noConnections" class="text-sm text-muted-foreground">
|
|
112
|
+
{{ t("integration.vpn.empty.text") }}
|
|
113
|
+
<NuxtLink v-if="vpnPageServed()" :to="VPN_PAGE" class="font-medium text-violet-600 hover:underline">{{ t("integration.vpn.empty.link") }}</NuxtLink>
|
|
114
|
+
<template v-else>{{ t("integration.vpn.empty.elsewhere") }}</template>
|
|
115
|
+
</p>
|
|
116
|
+
|
|
117
|
+
<template v-if="viaVpn">
|
|
118
|
+
<p v-if="loading && !loaded" class="text-sm text-muted-foreground">{{ t("integration.vpn.loading") }}</p>
|
|
119
|
+
<div v-else-if="loadError" role="alert" class="flex items-center gap-2 text-sm text-rose-700">
|
|
120
|
+
<span>{{ loadError }}</span>
|
|
121
|
+
<Button variant="outline" size="sm" class="h-7" @click="load(true)">{{ t("integration.common.action.retry") }}</Button>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<template v-if="loaded">
|
|
125
|
+
<div class="space-y-1.5">
|
|
126
|
+
<Label :for="`${idPrefix}-connection`">{{ t("integration.vpn.picker.connection") }}</Label>
|
|
127
|
+
<Select :model-value="pickedConnection" @update:model-value="pickConnection">
|
|
128
|
+
<SelectTrigger :id="`${idPrefix}-connection`" :aria-label="name('integration.vpn.picker.connection')" class="bg-white">
|
|
129
|
+
<SelectValue :placeholder="t('integration.vpn.picker.connection.placeholder')">
|
|
130
|
+
<span v-if="connection" class="flex min-w-0 items-center gap-1.5">
|
|
131
|
+
<span class="min-w-0 truncate">{{ connectionLabel(connection) }}</span>
|
|
132
|
+
<span class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium" :class="connectionStatusTone(String(connection.status))">
|
|
133
|
+
{{ connectionStatusLabel(String(connection.status)) }}
|
|
134
|
+
</span>
|
|
135
|
+
</span>
|
|
136
|
+
<span v-else-if="pickedConnection" class="text-rose-600">{{ pickedConnection }}</span>
|
|
137
|
+
<span v-else class="text-muted-foreground">{{ t("integration.vpn.picker.connection.placeholder") }}</span>
|
|
138
|
+
</SelectValue>
|
|
139
|
+
</SelectTrigger>
|
|
140
|
+
<SelectContent>
|
|
141
|
+
<SelectItem v-for="c in connections" :key="c.name" :value="c.name">
|
|
142
|
+
<span class="flex items-center gap-1.5">
|
|
143
|
+
<span class="min-w-0 truncate">{{ connectionLabel(c) }}</span>
|
|
144
|
+
<span class="shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium" :class="connectionStatusTone(String(c.status))">
|
|
145
|
+
{{ connectionStatusLabel(String(c.status)) }}
|
|
146
|
+
</span>
|
|
147
|
+
</span>
|
|
148
|
+
</SelectItem>
|
|
149
|
+
</SelectContent>
|
|
150
|
+
</Select>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div v-if="pickedConnection" class="space-y-1.5">
|
|
154
|
+
<Label :for="`${idPrefix}-route`">{{ t("integration.vpn.picker.route") }}</Label>
|
|
155
|
+
<Select :model-value="current?.route ?? ''" @update:model-value="pickRoute">
|
|
156
|
+
<SelectTrigger :id="`${idPrefix}-route`" :aria-label="name('integration.vpn.picker.route')" class="bg-white">
|
|
157
|
+
<SelectValue :placeholder="t('integration.vpn.picker.route.placeholder')">
|
|
158
|
+
<span v-if="chosenRoute">{{ chosenRoute.name }} — {{ chosenRoute.host }}:{{ chosenRoute.port }}</span>
|
|
159
|
+
<span v-else-if="current?.route" class="text-rose-600">{{ current.route }}</span>
|
|
160
|
+
<span v-else class="text-muted-foreground">{{ t("integration.vpn.picker.route.placeholder") }}</span>
|
|
161
|
+
</SelectValue>
|
|
162
|
+
</SelectTrigger>
|
|
163
|
+
<SelectContent>
|
|
164
|
+
<SelectItem v-for="r in routes" :key="r.name" :value="r.name">
|
|
165
|
+
{{ r.name }} — {{ r.host }}:{{ r.port }}
|
|
166
|
+
</SelectItem>
|
|
167
|
+
<div v-if="!routes.length" class="px-2 py-1.5 text-sm text-muted-foreground">
|
|
168
|
+
{{ t(`integration.vpn.picker.route.empty.${kind}`) }}
|
|
169
|
+
</div>
|
|
170
|
+
</SelectContent>
|
|
171
|
+
</Select>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<!-- A reference the tenant's connections no longer resolve. Said in words,
|
|
175
|
+
because the choice would otherwise look merely unfinished while every
|
|
176
|
+
run through it fails. -->
|
|
177
|
+
<p v-if="pending" class="text-sm text-amber-700">
|
|
178
|
+
{{ pending }}
|
|
179
|
+
</p>
|
|
180
|
+
<p v-if="reference?.state === 'missing' && current" class="text-sm text-rose-700">
|
|
181
|
+
{{ t("integration.vpn.reference.missing", { connection: current.connection, route: current.route }) }}
|
|
182
|
+
</p>
|
|
183
|
+
<p v-else-if="connection && connection.status !== 'active'" class="text-sm text-amber-700">
|
|
184
|
+
{{ t("integration.vpn.reference.inactive", { connection: connectionLabel(connection), status: connectionStatusLabel(String(connection.status)) }) }}
|
|
185
|
+
</p>
|
|
186
|
+
<p v-if="isHttpsTarget(chosenRoute)" class="text-sm text-muted-foreground">
|
|
187
|
+
{{ t("integration.vpn.httpsNote") }}
|
|
188
|
+
</p>
|
|
189
|
+
</template>
|
|
190
|
+
</template>
|
|
191
|
+
|
|
192
|
+
<p v-if="error" class="text-sm text-rose-700">{{ error }}</p>
|
|
193
|
+
</div>
|
|
194
|
+
</template>
|
|
@@ -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;
|
|
@@ -31,6 +31,7 @@ import { formatDuration } from "../../utils/formatDuration";
|
|
|
31
31
|
import { SAFETY_POLL_MS } from "../../utils/pollBudget";
|
|
32
32
|
import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
|
|
33
33
|
import { executedNodeIds, isSkippedStep, skipReasonMessage, stepStatusLabel } from "../../utils/skippedStep";
|
|
34
|
+
import { frameLabel, stepError } from "../../utils/stepError";
|
|
34
35
|
import { errorReason } from "../../utils/apiError";
|
|
35
36
|
const props = defineProps({
|
|
36
37
|
workflow: { type: Object, required: true },
|
|
@@ -108,6 +109,7 @@ const playing = ref(false);
|
|
|
108
109
|
let playTimer = null;
|
|
109
110
|
const safeCursor = computed(() => steps.value.length === 0 ? 0 : Math.min(cursor.value, steps.value.length - 1));
|
|
110
111
|
const activeStep = computed(() => steps.value[safeCursor.value]);
|
|
112
|
+
const activeError = computed(() => stepError(activeStep.value?.error));
|
|
111
113
|
function seek(i) {
|
|
112
114
|
playing.value = false;
|
|
113
115
|
cursor.value = Math.max(0, Math.min(i, steps.value.length - 1));
|
|
@@ -529,9 +531,19 @@ watch(() => props.initialRunId, (id) => {
|
|
|
529
531
|
<code class="rounded bg-black/[0.05] px-1.5 py-0.5 font-mono">{{ activeStep.branch }}</code>
|
|
530
532
|
</div>
|
|
531
533
|
|
|
532
|
-
<div v-if="
|
|
534
|
+
<div v-if="activeError" class="text-sm" data-testid="replay-step-error">
|
|
533
535
|
<div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.error") }}</div>
|
|
534
|
-
<
|
|
536
|
+
<div class="max-h-40 overflow-auto rounded-md border border-rose-500/20 bg-rose-500/5 p-2 text-sm text-rose-700 break-words whitespace-pre-wrap">{{ activeError.message }}</div>
|
|
537
|
+
<div v-if="activeError.type" class="mt-1 font-mono text-xs text-muted-foreground">{{ activeError.type }}</div>
|
|
538
|
+
<!-- Where the node's own code failed (PO-447): for whoever maintains the
|
|
539
|
+
node, so folded rather than beside the reason (IN-12). Keyed on the step so
|
|
540
|
+
it folds again on the next failed one. -->
|
|
541
|
+
<details v-if="activeError.frames?.length" :key="safeCursor" class="mt-1">
|
|
542
|
+
<summary class="cursor-pointer text-xs text-muted-foreground">{{ t("integration.replay.debug.field.frames") }}</summary>
|
|
543
|
+
<ol class="mt-1 space-y-0.5 font-mono text-xs">
|
|
544
|
+
<li v-for="(frame, fi) in activeError.frames" :key="fi">{{ frameLabel(frame) }}</li>
|
|
545
|
+
</ol>
|
|
546
|
+
</details>
|
|
535
547
|
</div>
|
|
536
548
|
|
|
537
549
|
<div v-if="activeStep.input !== void 0 && activeStep.input !== null" class="text-sm">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Credential } from '../types/integrations.js';
|
|
1
|
+
import type { Credential, CredentialNetwork } from '../types/integrations.js';
|
|
2
2
|
import { IntegrationsApiError } from '../utils/apiError.js';
|
|
3
3
|
/**
|
|
4
4
|
* Typed, per-tenant credential instances, backed by the Integrations service
|
|
@@ -6,15 +6,19 @@ import { IntegrationsApiError } from '../utils/apiError.js';
|
|
|
6
6
|
* carries `public_config` (non-secret fields) + `status` only.
|
|
7
7
|
*
|
|
8
8
|
* GET /credentials[?type=slug] → list
|
|
9
|
-
* POST /credentials → create { credential_type_slug, name, config }
|
|
9
|
+
* POST /credentials → create { credential_type_slug, name, config, network? }
|
|
10
10
|
* GET /credentials/{id} → show (masked)
|
|
11
|
-
* PATCH /credentials/{id} → update { name?, config? } (blank secret = keep,
|
|
12
|
-
* unless the same edit changes any other value
|
|
13
|
-
* then 422 at config.<secret>, PO-765)
|
|
11
|
+
* PATCH /credentials/{id} → update { name?, config?, network? } (blank secret = keep,
|
|
12
|
+
* unless the same edit changes any other value
|
|
13
|
+
* or the network: then 422 at config.<secret>, PO-765)
|
|
14
14
|
* DELETE /credentials/{id} → remove
|
|
15
|
-
* POST /credentials/test → inline test { credential_type_slug, config }
|
|
16
|
-
* POST /credentials/{id}/test → test { config? } → { ok, message, recorded, last_test_at, last_test_ok }
|
|
15
|
+
* POST /credentials/test → inline test { credential_type_slug, config, network? }
|
|
16
|
+
* POST /credentials/{id}/test → test { config?, network? } → { ok, message, recorded, last_test_at, last_test_ok }
|
|
17
17
|
* POST /credentials/{id}/oauth/authorize-url → { authorize_url }
|
|
18
|
+
*
|
|
19
|
+
* `network` is `{ kind: 'vpn', connection, route }` or `null` (public internet), and
|
|
20
|
+
* is sent only for a type that declares what a VPN route replaces (PO-829); left out,
|
|
21
|
+
* the service keeps what is stored.
|
|
18
22
|
*/
|
|
19
23
|
/**
|
|
20
24
|
* `message` is what the credential type said about the test, whichever way it went —
|
|
@@ -65,13 +69,15 @@ export declare function useIntegrationsCredentials(): {
|
|
|
65
69
|
credential_type_slug: string;
|
|
66
70
|
name: string;
|
|
67
71
|
config: Record<string, unknown>;
|
|
72
|
+
network?: CredentialNetwork | null;
|
|
68
73
|
}) => Promise<Credential>;
|
|
69
74
|
update: (id: string, input: {
|
|
70
75
|
name?: string;
|
|
71
76
|
config?: Record<string, unknown>;
|
|
77
|
+
network?: CredentialNetwork | null;
|
|
72
78
|
}) => Promise<Credential>;
|
|
73
79
|
remove: (id: string) => Promise<void>;
|
|
74
|
-
testInline: (credentialTypeSlug: string, config: Record<string, unknown
|
|
75
|
-
test: (id: string, config?: Record<string, unknown
|
|
80
|
+
testInline: (credentialTypeSlug: string, config: Record<string, unknown>, network?: CredentialNetwork | null) => Promise<CredentialTestResult>;
|
|
81
|
+
test: (id: string, config?: Record<string, unknown>, network?: CredentialNetwork | null) => Promise<CredentialTestResult>;
|
|
76
82
|
oauthAuthorizeUrl: (id: string) => Promise<string>;
|
|
77
83
|
};
|
|
@@ -49,19 +49,24 @@ export function useIntegrationsCredentials() {
|
|
|
49
49
|
await api.del(`/credentials/${encodeURIComponent(id)}`);
|
|
50
50
|
credentials.value = credentials.value.filter((c) => c.id !== id);
|
|
51
51
|
}
|
|
52
|
-
async function testInline(credentialTypeSlug, config) {
|
|
52
|
+
async function testInline(credentialTypeSlug, config, network) {
|
|
53
53
|
try {
|
|
54
|
-
const
|
|
54
|
+
const body = { credential_type_slug: credentialTypeSlug, config };
|
|
55
|
+
if (network !== void 0) body.network = network;
|
|
56
|
+
const res = await api.post("/credentials/test", body);
|
|
55
57
|
return res?.ok === false ? res : { ok: true, message: res?.message };
|
|
56
58
|
} catch (e) {
|
|
57
59
|
return { ok: false, error: errorReason(e, t("integration.credentials.error.testFailed")) };
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
|
-
async function test(id, config) {
|
|
62
|
+
async function test(id, config, network) {
|
|
61
63
|
try {
|
|
64
|
+
const body = {};
|
|
65
|
+
if (config) body.config = config;
|
|
66
|
+
if (network !== void 0) body.network = network;
|
|
62
67
|
const res = await api.post(
|
|
63
68
|
`/credentials/${encodeURIComponent(id)}/test`,
|
|
64
|
-
|
|
69
|
+
Object.keys(body).length ? body : void 0
|
|
65
70
|
);
|
|
66
71
|
if (!config && res?.ok === false && res.recorded === false) return { ...res, untested: true };
|
|
67
72
|
return res?.ok === false ? res : { ok: true, message: res?.message, recorded: res?.recorded };
|
|
@@ -44,7 +44,7 @@ export declare function useIntegrationsRuns(): {
|
|
|
44
44
|
latestLoading: any;
|
|
45
45
|
loadLatestPerWorkflow: (force?: boolean) => Promise<WorkflowRun[]>;
|
|
46
46
|
lastRunFor: (workflowId: number) => WorkflowRun | null;
|
|
47
|
-
trigger: (workflowId: number | string, payload?: Record<string, unknown>, mode?: "
|
|
47
|
+
trigger: (workflowId: number | string, payload?: Record<string, unknown>, mode?: "async" | "sync") => Promise<unknown>;
|
|
48
48
|
cancelRun: (workflowId: number | string, runId: number | string, reason?: string) => Promise<unknown>;
|
|
49
49
|
terminateRun: (workflowId: number | string, runId: number | string, reason?: string) => Promise<unknown>;
|
|
50
50
|
retryRun: (workflowId: number | string, runId: number | string) => Promise<unknown>;
|
|
@@ -12,6 +12,8 @@ export declare function useIntegrationsTemplates(): {
|
|
|
12
12
|
fetchDetail: (slug: string) => Promise<WorkflowTemplate | null>;
|
|
13
13
|
ensureDetails: (slugs: string[], _concurrency?: number) => Promise<void>;
|
|
14
14
|
requirements: (slug: string) => Promise<TemplateRequirements | null>;
|
|
15
|
+
checkRequirements: (slug: string) => Promise<TemplateRequirements>;
|
|
16
|
+
uncheckedReason: (err: unknown) => string;
|
|
15
17
|
instantiate: (slug: string, opts?: {
|
|
16
18
|
name?: string;
|
|
17
19
|
credentialBindings?: Record<string, string | Record<string, string>>;
|
|
@@ -165,16 +165,22 @@ export function useIntegrationsTemplates() {
|
|
|
165
165
|
if (!wanted.length) return;
|
|
166
166
|
remember(await loader.many(wanted));
|
|
167
167
|
}
|
|
168
|
+
async function checkRequirements(slug) {
|
|
169
|
+
const raw = unwrapRecord(await api.get(`/templates/${slug}/requirements`));
|
|
170
|
+
return {
|
|
171
|
+
...raw,
|
|
172
|
+
credentialTypes: (raw.credentialTypes ?? []).map((c) => ({
|
|
173
|
+
...c,
|
|
174
|
+
name: c.name ? resolveLocalized(c.name) : c.name
|
|
175
|
+
}))
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function uncheckedReason(err) {
|
|
179
|
+
return errorReason(err, t("integration.templates.detail.setup.uncheckedFallback"));
|
|
180
|
+
}
|
|
168
181
|
async function requirements(slug) {
|
|
169
182
|
try {
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
...raw,
|
|
173
|
-
credentialTypes: (raw.credentialTypes ?? []).map((c) => ({
|
|
174
|
-
...c,
|
|
175
|
-
name: c.name ? resolveLocalized(c.name) : c.name
|
|
176
|
-
}))
|
|
177
|
-
};
|
|
183
|
+
return await checkRequirements(slug);
|
|
178
184
|
} catch {
|
|
179
185
|
return null;
|
|
180
186
|
}
|
|
@@ -194,5 +200,5 @@ export function useIntegrationsTemplates() {
|
|
|
194
200
|
}
|
|
195
201
|
const total = computed(() => templates.value.length);
|
|
196
202
|
const startingPoints = computed(() => templates.value.slice(0, 4));
|
|
197
|
-
return { templates, loaded, loading, error, total, startingPoints, load, list, get, fetchDetail, ensureDetails, requirements, instantiate, install };
|
|
203
|
+
return { templates, loaded, loading, error, total, startingPoints, load, list, get, fetchDetail, ensureDetails, requirements, checkRequirements, uncheckedReason, instantiate, install };
|
|
198
204
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { VpnConnection } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* The tenant's VPN connections and their routes, as the Integrations service
|
|
4
|
+
* reports them (`GET /api/v1/vpn-connections`, PO-825). Each carries the status
|
|
5
|
+
* Console derives for it; only an `active` one carries traffic.
|
|
6
|
+
*
|
|
7
|
+
* Read by the credential forms' Network section, the credential picker's VPN badge
|
|
8
|
+
* and the `vpn-route` node setting — so it is one shared list, loaded once per page
|
|
9
|
+
* and re-read on request. A tenant without any answers an empty list; a Console
|
|
10
|
+
* that cannot be reached answers 502, which lands in `error` rather than reading as
|
|
11
|
+
* "no connections".
|
|
12
|
+
*/
|
|
13
|
+
export declare function useVpnConnections(): {
|
|
14
|
+
connections: any;
|
|
15
|
+
loading: any;
|
|
16
|
+
error: any;
|
|
17
|
+
loaded: any;
|
|
18
|
+
load: (force?: boolean) => Promise<VpnConnection[]>;
|
|
19
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
2
|
+
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
3
|
+
import { errorReason } from "../utils/apiError.js";
|
|
4
|
+
import { unwrapList } from "../utils/apiEnvelope.js";
|
|
5
|
+
import { useLoadGate } from "./useLoadGate.js";
|
|
6
|
+
export function useVpnConnections() {
|
|
7
|
+
const connections = useState("integrations.vpnConnections", () => []);
|
|
8
|
+
const loading = useState("integrations.vpnConnections.loading", () => false);
|
|
9
|
+
const error = useState("integrations.vpnConnections.error", () => null);
|
|
10
|
+
const loaded = useState("integrations.vpnConnections.loaded", () => false);
|
|
11
|
+
const gate = useLoadGate("integrations.vpnConnections", { loading, loaded });
|
|
12
|
+
const api = useIntegrationsApi();
|
|
13
|
+
const t = useIntegrationsT();
|
|
14
|
+
async function load(force = false) {
|
|
15
|
+
if (!await gate.claim(force)) return connections.value;
|
|
16
|
+
error.value = null;
|
|
17
|
+
try {
|
|
18
|
+
connections.value = unwrapList(await api.get("/vpn-connections"));
|
|
19
|
+
loaded.value = true;
|
|
20
|
+
} catch (e) {
|
|
21
|
+
error.value = errorReason(e, t("integration.vpn.error.load"));
|
|
22
|
+
} finally {
|
|
23
|
+
gate.release();
|
|
24
|
+
}
|
|
25
|
+
return connections.value;
|
|
26
|
+
}
|
|
27
|
+
return { connections, loading, error, loaded, load };
|
|
28
|
+
}
|
|
@@ -574,8 +574,10 @@
|
|
|
574
574
|
"integration.nodeConfig.credential.state.failed": "Letzter Test fehlgeschlagen: {message}",
|
|
575
575
|
"integration.nodeConfig.credential.state.needsReauth": "Diese Zugangsdaten müssen neu verbunden werden.",
|
|
576
576
|
"integration.nodeConfig.credential.trigger.bound": "{label}: {name}, {state}",
|
|
577
|
+
"integration.nodeConfig.credential.trigger.boundVpn": "{label}: {name}, {state}, {vpn}",
|
|
577
578
|
"integration.nodeConfig.credential.trigger.unknown": "{label}: {unknown}",
|
|
578
579
|
"integration.nodeConfig.credential.unknown": "Unbekannte Zugangsdaten — bitte erneut auswählen",
|
|
580
|
+
"integration.nodeConfig.credential.vpnInactive": "Ausführungen mit diesen Zugangsdaten schlagen fehl, bis ihre VPN-Verbindung wieder aktiv ist.",
|
|
579
581
|
"integration.nodeConfig.dynamicField.currentNotResolved": "aktueller Wert nicht in den geladenen Optionen",
|
|
580
582
|
"integration.nodeConfig.dynamicField.empty": "keine Optionen verfügbar",
|
|
581
583
|
"integration.nodeConfig.dynamicField.emptyFiltered": "keine Optionen passen zum Filter",
|
|
@@ -920,6 +922,7 @@
|
|
|
920
922
|
"integration.replay.debug.field.config": "Konfiguration",
|
|
921
923
|
"integration.replay.debug.field.duration": "Dauer",
|
|
922
924
|
"integration.replay.debug.field.error": "Fehler",
|
|
925
|
+
"integration.replay.debug.field.frames": "Stelle im Code des Nodes",
|
|
923
926
|
"integration.replay.debug.field.input": "Eingabe",
|
|
924
927
|
"integration.replay.debug.field.output": "Ausgabe",
|
|
925
928
|
"integration.replay.debug.field.started": "Gestartet",
|
|
@@ -1172,6 +1175,7 @@
|
|
|
1172
1175
|
"integration.templates.detail.install.state.loading": "Es wird geprüft, was diese Vorlage benötigt…",
|
|
1173
1176
|
"integration.templates.detail.install.submit": "Installieren",
|
|
1174
1177
|
"integration.templates.detail.install.submitting": "Wird eingerichtet…",
|
|
1178
|
+
"integration.templates.detail.install.stillInstallable": "Sie können sie trotzdem installieren und die Zugangsdaten im Editor verbinden.",
|
|
1175
1179
|
"integration.templates.detail.publisher.kind.community": "Community",
|
|
1176
1180
|
"integration.templates.detail.publisher.kind.partner": "Partner",
|
|
1177
1181
|
"integration.templates.detail.publisher.kind.revenexx": "Offiziell",
|
|
@@ -1189,6 +1193,8 @@
|
|
|
1189
1193
|
"integration.templates.detail.setup.missingCredentials": "{n} Zugangsdatensatz zu verbinden | {n} Zugangsdatensätze zu verbinden",
|
|
1190
1194
|
"integration.templates.detail.setup.missingSecrets": "{n} Secret fehlt | {n} Secrets fehlen",
|
|
1191
1195
|
"integration.templates.detail.setup.ready": "Alles ist bereit für die Installation.",
|
|
1196
|
+
"integration.templates.detail.setup.unchecked": "Es ließ sich nicht prüfen, was diese Vorlage benötigt: {reason}",
|
|
1197
|
+
"integration.templates.detail.setup.uncheckedFallback": "Die Plattform hat nicht geantwortet.",
|
|
1192
1198
|
"integration.templates.detail.state.loading": "Vorlage wird geladen…",
|
|
1193
1199
|
"integration.templates.detail.state.notFound": "Vorlage nicht gefunden.",
|
|
1194
1200
|
"integration.templates.detail.steps.empty": "Für diese Vorlage gibt es noch keine Schritt-für-Schritt-Übersicht.",
|
|
@@ -1227,6 +1233,39 @@
|
|
|
1227
1233
|
"integration.templates.trigger.manual": "Bei Bedarf ausführen",
|
|
1228
1234
|
"integration.templates.trigger.webhook": "Webhook",
|
|
1229
1235
|
"integration.triggers.error.loadFailed": "Trigger konnten nicht geladen werden.",
|
|
1236
|
+
"integration.vpn.badge.aria": "{name}: {badge}",
|
|
1237
|
+
"integration.vpn.badge.missing": "VPN-Route fehlt",
|
|
1238
|
+
"integration.vpn.badge.named": "VPN · {connection}",
|
|
1239
|
+
"integration.vpn.badge.withState": "VPN · {connection} ({status})",
|
|
1240
|
+
"integration.vpn.empty.elsewhere": "VPN-Verbindungen werden im Cockpit eingerichtet.",
|
|
1241
|
+
"integration.vpn.empty.link": "Jetzt einrichten",
|
|
1242
|
+
"integration.vpn.empty.text": "Für diesen Mandanten ist noch keine VPN-Verbindung eingerichtet.",
|
|
1243
|
+
"integration.vpn.error.load": "Die VPN-Verbindungen konnten nicht geladen werden.",
|
|
1244
|
+
"integration.vpn.httpsNote": "Das Ziel wird über HTTPS erreicht; sein Zertifikat wird am Gateway nicht geprüft.",
|
|
1245
|
+
"integration.vpn.loading": "VPN-Verbindungen werden geladen…",
|
|
1246
|
+
"integration.vpn.picker.ariaFor": "{label}: {control}",
|
|
1247
|
+
"integration.vpn.picker.connection": "VPN-Verbindung",
|
|
1248
|
+
"integration.vpn.picker.connection.placeholder": "Verbindung auswählen",
|
|
1249
|
+
"integration.vpn.picker.group": "Netzwerk",
|
|
1250
|
+
"integration.vpn.picker.pending.public": "Noch keine Route gewählt. Bis eine gewählt ist, gilt das öffentliche Internet.",
|
|
1251
|
+
"integration.vpn.picker.pending.route": "Noch keine Route gewählt. Bis eine gewählt ist, gilt {route} über {connection}.",
|
|
1252
|
+
"integration.vpn.picker.public": "Öffentliches Internet",
|
|
1253
|
+
"integration.vpn.picker.route": "Route",
|
|
1254
|
+
"integration.vpn.picker.route.empty.http": "Diese Verbindung hat keine HTTP-Routen.",
|
|
1255
|
+
"integration.vpn.picker.route.empty.tcp": "Diese Verbindung hat keine TCP-Routen.",
|
|
1256
|
+
"integration.vpn.picker.route.placeholder": "Route auswählen",
|
|
1257
|
+
"integration.vpn.picker.vpn": "Über VPN-Verbindung",
|
|
1258
|
+
"integration.vpn.reference.inactive": "{connection} ist nicht aktiv ({status}) — Ausführungen darüber schlagen fehl, bis die Verbindung wieder aktiv ist.",
|
|
1259
|
+
"integration.vpn.reference.missing": "Die VPN-Route {connection}/{route} existiert nicht mehr. Wählen Sie eine andere Route oder das öffentliche Internet.",
|
|
1260
|
+
"integration.vpn.section.title": "Netzwerk",
|
|
1261
|
+
"integration.vpn.status.active": "Aktiv",
|
|
1262
|
+
"integration.vpn.status.disabled": "Deaktiviert",
|
|
1263
|
+
"integration.vpn.status.down": "Getrennt",
|
|
1264
|
+
"integration.vpn.status.failed": "Fehlgeschlagen",
|
|
1265
|
+
"integration.vpn.status.provisioning": "Wird eingerichtet",
|
|
1266
|
+
"integration.vpn.status.standby": "Bereitschaft",
|
|
1267
|
+
"integration.vpn.status.unknown": "Unbekannt",
|
|
1268
|
+
"integration.vpn.target": "{target} über {connection}",
|
|
1230
1269
|
"integration.workflowFile.action.export": "Als Datei exportieren",
|
|
1231
1270
|
"integration.workflowFile.action.import": "Aus Datei importieren…",
|
|
1232
1271
|
"integration.workflowFile.count.both": "{nodes}, {triggers}",
|
|
@@ -574,8 +574,10 @@
|
|
|
574
574
|
"integration.nodeConfig.credential.state.failed": "Last test failed: {message}",
|
|
575
575
|
"integration.nodeConfig.credential.state.needsReauth": "This credential needs reconnect.",
|
|
576
576
|
"integration.nodeConfig.credential.trigger.bound": "{label}: {name}, {state}",
|
|
577
|
+
"integration.nodeConfig.credential.trigger.boundVpn": "{label}: {name}, {state}, {vpn}",
|
|
577
578
|
"integration.nodeConfig.credential.trigger.unknown": "{label}: {unknown}",
|
|
578
579
|
"integration.nodeConfig.credential.unknown": "Unknown credential — pick again",
|
|
580
|
+
"integration.nodeConfig.credential.vpnInactive": "Runs that use this credential fail until its VPN connection is active again.",
|
|
579
581
|
"integration.nodeConfig.dynamicField.currentNotResolved": "current value not in resolved options",
|
|
580
582
|
"integration.nodeConfig.dynamicField.empty": "no options available",
|
|
581
583
|
"integration.nodeConfig.dynamicField.emptyFiltered": "no options match the filter",
|
|
@@ -920,6 +922,7 @@
|
|
|
920
922
|
"integration.replay.debug.field.config": "Config",
|
|
921
923
|
"integration.replay.debug.field.duration": "Duration",
|
|
922
924
|
"integration.replay.debug.field.error": "Error",
|
|
925
|
+
"integration.replay.debug.field.frames": "Where in the node's code",
|
|
923
926
|
"integration.replay.debug.field.input": "Input",
|
|
924
927
|
"integration.replay.debug.field.output": "Output",
|
|
925
928
|
"integration.replay.debug.field.started": "Started",
|
|
@@ -1172,6 +1175,7 @@
|
|
|
1172
1175
|
"integration.templates.detail.install.state.loading": "Checking what this template needs…",
|
|
1173
1176
|
"integration.templates.detail.install.submit": "Install",
|
|
1174
1177
|
"integration.templates.detail.install.submitting": "Setting up…",
|
|
1178
|
+
"integration.templates.detail.install.stillInstallable": "You can still install it and connect its credentials in the editor.",
|
|
1175
1179
|
"integration.templates.detail.publisher.kind.community": "Community",
|
|
1176
1180
|
"integration.templates.detail.publisher.kind.partner": "Partner",
|
|
1177
1181
|
"integration.templates.detail.publisher.kind.revenexx": "Official",
|
|
@@ -1189,6 +1193,8 @@
|
|
|
1189
1193
|
"integration.templates.detail.setup.missingCredentials": "{n} credential to connect | {n} credentials to connect",
|
|
1190
1194
|
"integration.templates.detail.setup.missingSecrets": "{n} secret missing | {n} secrets missing",
|
|
1191
1195
|
"integration.templates.detail.setup.ready": "Everything is ready to install.",
|
|
1196
|
+
"integration.templates.detail.setup.unchecked": "Could not check what this template needs: {reason}",
|
|
1197
|
+
"integration.templates.detail.setup.uncheckedFallback": "The platform did not answer.",
|
|
1192
1198
|
"integration.templates.detail.state.loading": "Loading template…",
|
|
1193
1199
|
"integration.templates.detail.state.notFound": "Template not found.",
|
|
1194
1200
|
"integration.templates.detail.steps.empty": "No step-by-step outline for this template yet.",
|
|
@@ -1227,6 +1233,39 @@
|
|
|
1227
1233
|
"integration.templates.trigger.manual": "Run on demand",
|
|
1228
1234
|
"integration.templates.trigger.webhook": "Webhook",
|
|
1229
1235
|
"integration.triggers.error.loadFailed": "Could not load triggers.",
|
|
1236
|
+
"integration.vpn.badge.aria": "{name}: {badge}",
|
|
1237
|
+
"integration.vpn.badge.missing": "VPN route missing",
|
|
1238
|
+
"integration.vpn.badge.named": "VPN · {connection}",
|
|
1239
|
+
"integration.vpn.badge.withState": "VPN · {connection} ({status})",
|
|
1240
|
+
"integration.vpn.empty.elsewhere": "VPN connections are set up in the cockpit.",
|
|
1241
|
+
"integration.vpn.empty.link": "Set one up",
|
|
1242
|
+
"integration.vpn.empty.text": "No VPN connection is set up for this tenant yet.",
|
|
1243
|
+
"integration.vpn.error.load": "The VPN connections could not be loaded.",
|
|
1244
|
+
"integration.vpn.httpsNote": "The target is reached over HTTPS, and its certificate is not verified at the gateway.",
|
|
1245
|
+
"integration.vpn.loading": "Loading VPN connections…",
|
|
1246
|
+
"integration.vpn.picker.ariaFor": "{label}: {control}",
|
|
1247
|
+
"integration.vpn.picker.connection": "VPN connection",
|
|
1248
|
+
"integration.vpn.picker.connection.placeholder": "Pick a connection",
|
|
1249
|
+
"integration.vpn.picker.group": "Network",
|
|
1250
|
+
"integration.vpn.picker.pending.public": "No route chosen yet. Until one is, the public internet is used.",
|
|
1251
|
+
"integration.vpn.picker.pending.route": "No route chosen yet. Until one is, {route} via {connection} is used.",
|
|
1252
|
+
"integration.vpn.picker.public": "Public internet",
|
|
1253
|
+
"integration.vpn.picker.route": "Route",
|
|
1254
|
+
"integration.vpn.picker.route.empty.http": "This connection has no HTTP routes.",
|
|
1255
|
+
"integration.vpn.picker.route.empty.tcp": "This connection has no TCP routes.",
|
|
1256
|
+
"integration.vpn.picker.route.placeholder": "Pick a route",
|
|
1257
|
+
"integration.vpn.picker.vpn": "Through a VPN connection",
|
|
1258
|
+
"integration.vpn.reference.inactive": "{connection} is not active ({status}) — runs through it fail until it is active again.",
|
|
1259
|
+
"integration.vpn.reference.missing": "The VPN route {connection}/{route} no longer exists. Choose another route or the public internet.",
|
|
1260
|
+
"integration.vpn.section.title": "Network",
|
|
1261
|
+
"integration.vpn.status.active": "Active",
|
|
1262
|
+
"integration.vpn.status.disabled": "Disabled",
|
|
1263
|
+
"integration.vpn.status.down": "Down",
|
|
1264
|
+
"integration.vpn.status.failed": "Failed",
|
|
1265
|
+
"integration.vpn.status.provisioning": "Provisioning",
|
|
1266
|
+
"integration.vpn.status.standby": "Standby",
|
|
1267
|
+
"integration.vpn.status.unknown": "Unknown",
|
|
1268
|
+
"integration.vpn.target": "{target} via {connection}",
|
|
1230
1269
|
"integration.workflowFile.action.export": "Export as file",
|
|
1231
1270
|
"integration.workflowFile.action.import": "Import from file…",
|
|
1232
1271
|
"integration.workflowFile.count.both": "{nodes}, {triggers}",
|