feat(connection): add HTTP tunnel
This commit is contained in:
1 parent
91c77e53c4
commit
b80f3e8088
21 files changed
+1515
-123
No files matched your search
@@ -13,7 +13,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { ConnectionConfig, DatabaseType, JdbcDriverInfo, JdbcMavenBundleInfo, ProxyTunnelConfig, SshTunnelConfig, TransportLayerConfig } from "@/types/database";
|
||||
import type { ConnectionConfig, DatabaseType, HttpTunnelConfig, JdbcDriverInfo, JdbcMavenBundleInfo, ProxyTunnelConfig, SshTunnelConfig, TransportLayerConfig } from "@/types/database";
|
||||
import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq";
|
||||
import type { NacosAdminConfig, NacosAuthConfig } from "@/types/nacos";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
@@ -234,6 +234,17 @@ function defaultProxyTunnel(): ProxyTunnelConfig {
|
||||
};
|
||||
}
|
||||
|
||||
function defaultHttpTunnel(): HttpTunnelConfig {
|
||||
return {
|
||||
id: uuid(),
|
||||
name: "",
|
||||
enabled: true,
|
||||
url: "",
|
||||
token: "",
|
||||
connect_timeout_secs: 10,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeProxyTunnel(layer: Partial<ProxyTunnelConfig>): ProxyTunnelConfig {
|
||||
return {
|
||||
id: layer.id || uuid(),
|
||||
@@ -247,10 +258,24 @@ function normalizeProxyTunnel(layer: Partial<ProxyTunnelConfig>): ProxyTunnelCon
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHttpTunnel(layer: Partial<HttpTunnelConfig>): HttpTunnelConfig {
|
||||
return {
|
||||
id: layer.id || uuid(),
|
||||
name: layer.name || "",
|
||||
enabled: layer.enabled !== false,
|
||||
url: layer.url || "",
|
||||
token: layer.token || "",
|
||||
connect_timeout_secs: Number(layer.connect_timeout_secs) || 10,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTransportLayer(layer: Partial<TransportLayerConfig>): TransportLayerConfig {
|
||||
if (layer.type === "proxy") {
|
||||
return { type: "proxy", ...normalizeProxyTunnel(layer) };
|
||||
}
|
||||
if (layer.type === "http_tunnel") {
|
||||
return { type: "http_tunnel", ...normalizeHttpTunnel(layer) };
|
||||
}
|
||||
return { type: "ssh", ...normalizeSshTunnel(layer as Partial<SshTunnelConfig>) };
|
||||
}
|
||||
|
||||
@@ -1334,16 +1359,22 @@ const selectedTransportLayer = computed(() => {
|
||||
});
|
||||
const selectedSshLayer = computed(() => (selectedTransportLayer.value?.type === "ssh" ? selectedTransportLayer.value : null));
|
||||
const selectedProxyLayer = computed(() => (selectedTransportLayer.value?.type === "proxy" ? selectedTransportLayer.value : null));
|
||||
const selectedHttpTunnelLayer = computed(() => (selectedTransportLayer.value?.type === "http_tunnel" ? selectedTransportLayer.value : null));
|
||||
|
||||
function transportLayerDefaultName(layer: TransportLayerConfig, index: number): string {
|
||||
if (layer.type === "proxy") return `Proxy ${index + 1}`;
|
||||
if (layer.type === "http_tunnel") return t("connection.httpTunnelDefaultName", { index: index + 1 });
|
||||
return t("connection.sshHopDefaultName", { index: index + 1 });
|
||||
}
|
||||
|
||||
function transportLayerDisplayName(layer: TransportLayerConfig, index: number): string {
|
||||
const target = layer.type === "http_tunnel" ? layer.url?.trim() : layer.host?.trim();
|
||||
return layer.name?.trim() || target || transportLayerDefaultName(layer, index);
|
||||
}
|
||||
|
||||
const transportPathSegments = computed(() => {
|
||||
const layers = transportLayers.value.filter((layer) => layer.enabled !== false);
|
||||
return [
|
||||
"DBX",
|
||||
...layers.map((layer, index) => {
|
||||
const fallback = layer.type === "proxy" ? `Proxy ${index + 1}` : `SSH ${index + 1}`;
|
||||
return layer.name?.trim() || layer.host?.trim() || fallback;
|
||||
}),
|
||||
form.value.host || "Database",
|
||||
];
|
||||
return ["DBX", ...layers.map(transportLayerDisplayName), form.value.host || "Database"];
|
||||
});
|
||||
|
||||
function defaultDatabaseForProfile() {
|
||||
@@ -2838,6 +2869,14 @@ function addProxyTunnel() {
|
||||
resetTestState();
|
||||
}
|
||||
|
||||
function addHttpTunnel() {
|
||||
const next: TransportLayerConfig = { type: "http_tunnel", ...defaultHttpTunnel() };
|
||||
next.name = t("connection.httpTunnelDefaultName", { index: 1 });
|
||||
form.value.transport_layers = [next, ...transportLayers.value];
|
||||
selectedTransportLayerId.value = next.id;
|
||||
resetTestState();
|
||||
}
|
||||
|
||||
function duplicateTransportLayer(layer: TransportLayerConfig) {
|
||||
const next = normalizeTransportLayer({ ...layer, id: uuid(), name: layer.name ? `${layer.name} copy` : "" });
|
||||
form.value.transport_layers = [...transportLayers.value, next];
|
||||
@@ -2875,10 +2914,11 @@ function dropTransportLayer(targetId: string) {
|
||||
resetTestState();
|
||||
}
|
||||
|
||||
function changeSelectedTransportLayerType(type: "ssh" | "proxy") {
|
||||
function changeSelectedTransportLayerType(type: "ssh" | "proxy" | "http_tunnel") {
|
||||
const selected = selectedTransportLayer.value;
|
||||
if (!selected || selected.type === type) return;
|
||||
const replacement: TransportLayerConfig = type === "proxy" ? { type: "proxy", ...defaultProxyTunnel(), id: selected.id, name: selected.name } : { type: "ssh", ...defaultSshTunnel(), id: selected.id, name: selected.name };
|
||||
const replacement: TransportLayerConfig =
|
||||
type === "proxy" ? { type: "proxy", ...defaultProxyTunnel(), id: selected.id, name: selected.name } : type === "http_tunnel" ? { type: "http_tunnel", ...defaultHttpTunnel(), id: selected.id, name: selected.name } : { type: "ssh", ...defaultSshTunnel(), id: selected.id, name: selected.name };
|
||||
form.value.transport_layers = transportLayers.value.map((layer) => (layer.id === selected.id ? replacement : layer));
|
||||
resetTestState();
|
||||
}
|
||||
@@ -2894,7 +2934,16 @@ function validateTransportLayers(config: LegacyConnectionConfig) {
|
||||
const layers = config.transport_layers || [];
|
||||
layers.forEach((layer, index) => {
|
||||
if (layer.enabled === false) return;
|
||||
const label = layer.name?.trim() || t("connection.sshHopDefaultName", { index: index + 1 });
|
||||
const label = layer.name?.trim() || transportLayerDefaultName(layer, index);
|
||||
if (layer.type === "http_tunnel") {
|
||||
if (index !== 0) throw new Error(t("connection.httpTunnelInvalidOrder", { hop: label }));
|
||||
if (!layer.url?.trim()) throw new Error(t("connection.httpTunnelInvalidUrl", { hop: label }));
|
||||
const timeout = Number(layer.connect_timeout_secs);
|
||||
if (!Number.isFinite(timeout) || timeout < 1 || timeout > 300) {
|
||||
throw new Error(t("connection.httpTunnelInvalidTimeout", { hop: label }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!layer.host?.trim()) throw new Error(t("connection.sshHopInvalidHost", { hop: label }));
|
||||
const port = Number(layer.port);
|
||||
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
||||
@@ -4589,17 +4638,17 @@ function openExternalUrl(url: string) {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent v-if="canUseTransportLayers" value="transport" class="m-0">
|
||||
<div class="connection-form-body grid gap-4 py-4 pr-2 max-h-[65vh] overflow-y-auto">
|
||||
<div class="connection-label-wide-grid grid grid-cols-4 items-start gap-4">
|
||||
<div class="connection-form-body grid gap-4 py-4 pr-2 max-h-[65vh] overflow-y-auto overflow-x-hidden">
|
||||
<div class="connection-label-wide-grid grid min-w-0 grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelSmallPaddedClass">{{ t("connection.sshHops") }}</Label>
|
||||
<div class="col-span-3 grid gap-3">
|
||||
<div class="flex flex-wrap items-center gap-1 text-[11px] text-muted-foreground">
|
||||
<div class="col-span-3 grid min-w-0 gap-3">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-1 text-[11px] text-muted-foreground">
|
||||
<template v-for="(segment, index) in transportPathSegments" :key="`${segment}-${index}`">
|
||||
<span class="rounded border bg-muted/40 px-2 py-1">{{ segment }}</span>
|
||||
<span class="inline-block max-w-full truncate rounded border bg-muted/40 px-2 py-1">{{ segment }}</span>
|
||||
<ChevronRight v-if="index < transportPathSegments.length - 1" class="h-3 w-3" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<div class="grid min-w-0 gap-2">
|
||||
<button
|
||||
v-for="(hop, index) in transportLayers"
|
||||
:key="hop.id"
|
||||
@@ -4616,7 +4665,7 @@ function openExternalUrl(url: string) {
|
||||
<span class="w-5 shrink-0 text-muted-foreground">{{ index + 1 }}</span>
|
||||
<input v-model="hop.enabled" type="checkbox" class="mr-0" @click.stop />
|
||||
<span class="min-w-0 flex-1 truncate">
|
||||
{{ hop.name || hop.host || (hop.type === "proxy" ? `Proxy ${index + 1}` : t("connection.sshHopDefaultName", { index: index + 1 })) }}
|
||||
{{ transportLayerDisplayName(hop, index) }}
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
@@ -4636,7 +4685,7 @@ function openExternalUrl(url: string) {
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<Button type="button" variant="outline" size="sm" @click="addSshTunnel">
|
||||
<Plus class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("connection.sshHopAdd") }}
|
||||
@@ -4645,6 +4694,10 @@ function openExternalUrl(url: string) {
|
||||
<Plus class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("connection.proxy") }}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" @click="addHttpTunnel">
|
||||
<Plus class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("connection.httpTunnelAdd") }}
|
||||
</Button>
|
||||
<Button v-if="selectedTransportLayer" type="button" variant="outline" size="sm" @click="duplicateTransportLayer(selectedTransportLayer)">
|
||||
<Copy class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("connection.sshHopDuplicate") }}
|
||||
@@ -4671,6 +4724,7 @@ function openExternalUrl(url: string) {
|
||||
<SelectContent>
|
||||
<SelectItem value="ssh">SSH</SelectItem>
|
||||
<SelectItem value="proxy">Proxy</SelectItem>
|
||||
<SelectItem value="http_tunnel">{{ t("connection.httpTunnel") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -4756,6 +4810,20 @@ function openExternalUrl(url: string) {
|
||||
<PasswordInput v-model="selectedProxyLayer.password" class="col-span-3" :placeholder="t('connection.proxyPasswordPlaceholder')" :disabled="selectedProxyLayer.enabled === false" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="selectedHttpTunnelLayer">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.httpTunnelUrl") }}</Label>
|
||||
<Input v-model="selectedHttpTunnelLayer.url" class="col-span-3" placeholder="https://dbx.example.com/dbx_tunnel.php" :disabled="selectedHttpTunnelLayer.enabled === false" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.httpTunnelToken") }}</Label>
|
||||
<PasswordInput v-model="selectedHttpTunnelLayer.token" class="col-span-3" :placeholder="t('connection.httpTunnelTokenPlaceholder')" :disabled="selectedHttpTunnelLayer.enabled === false" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.httpTunnelConnectTimeout") }}</Label>
|
||||
<Input v-model.number="selectedHttpTunnelLayer.connect_timeout_secs" type="number" min="1" max="300" step="1" class="col-span-3" :disabled="selectedHttpTunnelLayer.enabled === false" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -4960,8 +5028,4 @@ function openExternalUrl(url: string) {
|
||||
width: min(100%, 36rem);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.connection-dialog-content .grid.grid-cols-4.connection-label-wide-grid {
|
||||
grid-template-columns: 7.75rem repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user