diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 9a80aa6a3..269eef243 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -2074,6 +2074,10 @@ onUnmounted(() => { setConnectionDialogOpen(false); openDriverStorePage(); " + @open-tunnel-profile-settings=" + setConnectionDialogOpen(false); + openSettings('tunnels'); + " @open-lineage-target="openLineageTarget" @open-database-search-target="openDatabaseSearchTarget" @open-diagram-target="openDiagramTarget" diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 5328cbcc3..a1f65da48 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -18,6 +18,8 @@ import type { InfluxDbExternalConfig, InfluxDbVersion } from "@/types/influxdb"; import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq"; import type { NacosAdminConfig, NacosAuthConfig } from "@/types/nacos"; import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stores/connectionStore"; +import { useTunnelProfileStore } from "@/stores/tunnelProfileStore"; +import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSummary } from "@/lib/connection/tunnelProfiles"; import { REDIS_SCAN_PAGE_SIZE_DEFAULT, REDIS_SCAN_PAGE_SIZE_MIN, REDIS_SCAN_PAGE_SIZE_MAX, REDIS_SCAN_PAGE_SIZE_OPTIONS } from "@/lib/redis/redisKeyPattern"; import { useSettingsStore } from "@/stores/settingsStore"; import { useToast } from "@/composables/useToast"; @@ -114,9 +116,11 @@ const emit = defineEmits<{ connectSucceeded: [name: string]; connectFailed: [message: string]; openDriverStore: []; + openTunnelProfileSettings: []; }>(); const store = useConnectionStore(); +const tunnelProfileStore = useTunnelProfileStore(); const isTesting = ref(false); const isSaving = ref(false); const testResult = ref<{ ok: boolean; message: string } | null>(null); @@ -251,6 +255,7 @@ function normalizeSshTunnel(hop: Partial): SshTunnelConfig { use_ssh_agent: !!hop.use_ssh_agent, ssh_agent_sock_path: hop.ssh_agent_sock_path || "", auth_method: hop.auth_method || inferSshAuthMethod(hop), + profile_id: hop.profile_id || undefined, }; } @@ -288,6 +293,7 @@ function normalizeProxyTunnel(layer: Partial): ProxyTunnelCon port: Number(layer.port) || 1080, username: layer.username || "", password: layer.password || "", + profile_id: layer.profile_id || undefined, }; } @@ -299,6 +305,7 @@ function normalizeHttpTunnel(layer: Partial): HttpTunnelConfig url: layer.url || "", token: layer.token || "", connect_timeout_secs: Number(layer.connect_timeout_secs) || 10, + profile_id: layer.profile_id || undefined, }; } @@ -1534,6 +1541,32 @@ const selectedSshLayer = computed(() => (selectedTransportLayer.value?.type === const selectedProxyLayer = computed(() => (selectedTransportLayer.value?.type === "proxy" ? selectedTransportLayer.value : null)); const selectedHttpTunnelLayer = computed(() => (selectedTransportLayer.value?.type === "http_tunnel" ? selectedTransportLayer.value : null)); +const tunnelProfiles = computed(() => tunnelProfileStore.profiles); +const selectedLayerProfileId = computed(() => selectedTransportLayer.value?.profile_id || ""); +const selectedLayerProfile = computed(() => tunnelProfileStore.profileById(selectedLayerProfileId.value)); + +function tunnelProfileOptionLabel(profile: (typeof tunnelProfiles.value)[number]): string { + const summary = tunnelProfileSummary(profile); + if (!profile.name?.trim()) return summary || profile.id; + return summary ? `${profile.name} (${summary})` : profile.name; +} + +function applyTunnelProfileSelection(value: unknown) { + const selected = selectedTransportLayer.value; + if (!selected) return; + if (!value || value === "custom") { + if (!selected.profile_id) return; + const detached = detachTunnelProfileLayer(selected, tunnelProfileStore.profileById(selected.profile_id)); + form.value.transport_layers = transportLayers.value.map((layer) => (layer.id === selected.id ? detached : layer)); + } else { + const profile = tunnelProfileStore.profileById(String(value)); + if (!profile) return; + const stub = tunnelProfileReferenceLayer(profile, selected); + form.value.transport_layers = transportLayers.value.map((layer) => (layer.id === selected.id ? stub : layer)); + } + resetTestState(); +} + 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 }); @@ -1541,6 +1574,11 @@ function transportLayerDefaultName(layer: TransportLayerConfig, index: number): } function transportLayerDisplayName(layer: TransportLayerConfig, index: number): string { + if (layer.profile_id) { + const profile = tunnelProfileStore.profileById(layer.profile_id); + if (profile) return profile.name?.trim() || tunnelProfileSummary(profile) || transportLayerDefaultName(layer, index); + return layer.name?.trim() || t("connection.tunnelProfileMissingName"); + } const target = layer.type === "http_tunnel" ? layer.url?.trim() : layer.host?.trim(); return layer.name?.trim() || target || transportLayerDefaultName(layer, index); } @@ -3306,6 +3344,9 @@ function validateTransportLayers(config: LegacyConnectionConfig) { const layers = config.transport_layers || []; layers.forEach((layer, index) => { if (layer.enabled === false) return; + // Profile-referencing layers are stubs: the shared profile supplies the + // whole configuration at connect time, so there is nothing to validate. + if (layer.profile_id) return; const label = layer.name?.trim() || transportLayerDefaultName(layer, index); if (layer.type === "http_tunnel") { if (index !== 0) throw new Error(t("connection.httpTunnelInvalidOrder", { hop: label })); @@ -3713,6 +3754,7 @@ function onJdbcDriverSelect(id: any) { } onMounted(async () => { + void tunnelProfileStore.init(); unlistenAgentInstallProgress = await api.listenAgentInstallProgress(handleAgentInstallProgress); }); @@ -5372,7 +5414,35 @@ function openExternalUrl(url: string) { -
+
+ +
+ + +
+
+
+ +
+ + {{ t("connection.tunnelProfileMissing") }} +
+
+
-