diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 76cb6db73..4903f148f 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -136,6 +136,7 @@ import ProcedureExecutionDialog from "@/components/objects/ProcedureExecutionDia import { useExportTracker, type ExportTask } from "@/composables/useExportTracker"; import { isTauriRuntime } from "@/lib/tauriRuntime"; import { copyToClipboard } from "@/lib/clipboard"; +import { hasEnabledTransportLayers } from "@/lib/connectionTransport"; import { formatShortcut } from "@/lib/shortcutRegistry"; import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import ConnectionErrorIndicator from "@/components/connection/ConnectionErrorIndicator.vue"; @@ -929,6 +930,20 @@ async function copyName() { } } +async function copyFinalProxyPort() { + const connectionId = props.node.connectionId; + const config = connectionId ? connectionStore.getConfig(connectionId) : undefined; + if (!config || !hasEnabledTransportLayers(config)) return; + + try { + const port = await api.connectionFinalProxyPort(config); + await copyToClipboard(String(port)); + toast(t("contextMenu.finalProxyPortCopied", { port }), 2000); + } catch (e: any) { + toast(t("grid.copyFailed", { message: translateBackendError(t, e?.message || String(e)) }), 5000); + } +} + async function copySelectedNames() { const selectedNodes = selectedTreeNodesInVisibleOrder(); const nodes = @@ -2475,6 +2490,10 @@ const canConfigureVisibleDatabases = computed(() => { if (props.node.type !== "connection" || !props.node.connectionId) return false; return connectionStore.getConfig(props.node.connectionId)?.db_type !== "elasticsearch"; }); +const canCopyFinalProxyPort = computed(() => { + if (props.node.type !== "connection" || !props.node.connectionId) return false; + return hasEnabledTransportLayers(connectionStore.getConfig(props.node.connectionId)); +}); function connectionIconType(connectionId?: string) { const config = connectionId ? connectionStore.getConfig(connectionId) : undefined; @@ -2845,6 +2864,9 @@ function treeItemMenuItems(): ContextMenuItem[] { items.push({ label: t("contextMenu.closeConnection"), action: disconnectConnection, icon: Unplug }); } items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare }); + if (canCopyFinalProxyPort.value) { + items.push({ label: t("contextMenu.copyFinalProxyPort"), action: copyFinalProxyPort, icon: Network }); + } if (canOpenSqlFileExecution.value) { items.push({ label: t("sqlFile.title"), action: openSqlFileExecution, icon: FileCode }); } diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index f56b4b379..6c24206c9 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -954,6 +954,8 @@ export default { closeOtherTabs: "Close Other Tabs", closeAllTabs: "Close All Tabs", copyName: "Copy Name", + copyFinalProxyPort: "Copy Final Proxy Port", + finalProxyPortCopied: "Final proxy port {port} copied", setDefaultDatabase: "Set as Default Database", clearDefaultDatabase: "Clear Default Database", exportDatabase: "Export Database", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 3af625553..e57b4bbe2 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -850,6 +850,8 @@ export default { closeOtherTabs: "Cerrar otras pestañas", closeAllTabs: "Cerrar todas las pestañas", copyName: "Copiar nombre", + copyFinalProxyPort: "Copiar puerto proxy final", + finalProxyPortCopied: "Puerto proxy final {port} copiado", setDefaultDatabase: "Establecer como base de datos predeterminada", clearDefaultDatabase: "Limpiar base de datos predeterminada", exportDatabase: "Exportar base de datos", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 6523c684c..d02faca00 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -973,6 +973,8 @@ export default { closeOtherTabs: "Chiudi Altre Schede", closeAllTabs: "Chiudi Tutte le Schede", copyName: "Copia Nome", + copyFinalProxyPort: "Copia porta proxy finale", + finalProxyPortCopied: "Porta proxy finale {port} copiata", setDefaultDatabase: "Imposta come Database Predefinito", clearDefaultDatabase: "Cancella Database Predefinito", exportDatabase: "Esporta Database", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 0dde849b2..fffd3a805 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -968,6 +968,8 @@ export default { closeTab: "Fechar Aba", closeOtherTabs: "Fechar Outras Abas", closeAllTabs: "Fechar Todas as Abas", + copyFinalProxyPort: "Copiar porta final do proxy", + finalProxyPortCopied: "Porta final do proxy {port} copiada", copyName: "Copiar Nome", setDefaultDatabase: "Definir como Banco de Dados Padrão", clearDefaultDatabase: "Limpar Banco de Dados Padrão", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 4e4c4a997..89578f1e5 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -934,6 +934,8 @@ export default { closeOtherTabs: "关闭其他标签页", closeAllTabs: "关闭全部标签页", copyName: "复制名称", + copyFinalProxyPort: "复制最终代理端口", + finalProxyPortCopied: "最终代理端口 {port} 已复制", setDefaultDatabase: "设为默认数据库", clearDefaultDatabase: "取消默认数据库", exportDatabase: "导出数据库", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 0a1cc09a9..d00d082fb 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -914,6 +914,8 @@ export default { closeOtherTabs: "關閉其他分頁", closeAllTabs: "關閉全部分頁", copyName: "複製名稱", + copyFinalProxyPort: "複製最終代理連接埠", + finalProxyPortCopied: "最終代理連接埠 {port} 已複製", setDefaultDatabase: "設為預設資料庫", clearDefaultDatabase: "取消預設資料庫", exportDatabase: "匯出資料庫", diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts index 5d7066d14..63bcde895 100644 --- a/apps/desktop/src/lib/api.ts +++ b/apps/desktop/src/lib/api.ts @@ -33,6 +33,7 @@ function forward(name: K): Backend[K] { // Connection export const testConnection = forward("testConnection"); export const connectDb = forward("connectDb"); +export const connectionFinalProxyPort = forward("connectionFinalProxyPort"); export const disconnectDb = forward("disconnectDb"); export const closeDatabaseConnection = forward("closeDatabaseConnection"); export const refreshConnections = forward("refreshConnections"); diff --git a/apps/desktop/src/lib/connectionTransport.ts b/apps/desktop/src/lib/connectionTransport.ts new file mode 100644 index 000000000..5520d3008 --- /dev/null +++ b/apps/desktop/src/lib/connectionTransport.ts @@ -0,0 +1,5 @@ +import type { ConnectionConfig } from "@/types/database"; + +export function hasEnabledTransportLayers(connection?: Pick | null): boolean { + return !!connection?.transport_layers?.some((layer) => layer.enabled !== false); +} diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts index 17bc1e5b7..fc525ae33 100644 --- a/apps/desktop/src/lib/http.ts +++ b/apps/desktop/src/lib/http.ts @@ -151,6 +151,10 @@ export async function connectDb(config: ConnectionConfig): Promise { return post("/api/connection/connect", { config }); } +export async function connectionFinalProxyPort(config: ConnectionConfig): Promise { + return post("/api/connection/final-proxy-port", { config }); +} + export async function disconnectDb(connectionId: string): Promise { return post("/api/connection/disconnect", { connectionId }); } diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts index 3a3fe3d51..a26e0ec07 100644 --- a/apps/desktop/src/lib/tauri.ts +++ b/apps/desktop/src/lib/tauri.ts @@ -414,6 +414,10 @@ export async function connectDb(config: ConnectionConfig): Promise { return invoke("connect_db", { config }); } +export async function connectionFinalProxyPort(config: ConnectionConfig): Promise { + return invoke("connection_final_proxy_port", { config }); +} + export async function disconnectDb(connectionId: string): Promise { return invoke("disconnect_db", { connectionId }); } diff --git a/crates/dbx-web/src/main.rs b/crates/dbx-web/src/main.rs index b59781acf..e47cb7c2b 100644 --- a/crates/dbx-web/src/main.rs +++ b/crates/dbx-web/src/main.rs @@ -94,6 +94,7 @@ async fn main() { // Connection .route("/connection/test", post(routes::connection::test_connection)) .route("/connection/connect", post(routes::connection::connect_db)) + .route("/connection/final-proxy-port", post(routes::connection::connection_final_proxy_port)) .route("/connection/disconnect", post(routes::connection::disconnect_db)) .route("/connection/close-database", post(routes::connection::close_database_connection)) .route("/connection/save", post(routes::connection::save_connections)) diff --git a/crates/dbx-web/src/routes/connection.rs b/crates/dbx-web/src/routes/connection.rs index 52e8477a8..66945466c 100644 --- a/crates/dbx-web/src/routes/connection.rs +++ b/crates/dbx-web/src/routes/connection.rs @@ -78,6 +78,24 @@ pub async fn connect_db( Ok(Json(connection_id)) } +pub async fn connection_final_proxy_port( + State(state): State>, + Json(body): Json, +) -> Result, AppError> { + let runtime_config = body.config.canonicalized(); + if !runtime_config.has_effective_transport_layers() { + return Err(AppError("Connection has no configured transport layers".to_string())); + } + + let app = &state.app; + let connection_id = runtime_config.id.clone(); + let db_config = dbx_core::connection::metadata_connection_config(&runtime_config); + app.configs.write().await.insert(connection_id.clone(), runtime_config); + + let (_, port) = app.connection_host_port(&connection_id, &db_config).await.map_err(AppError)?; + Ok(Json(port)) +} + pub async fn disconnect_db( State(state): State>, Json(body): Json, @@ -93,8 +111,7 @@ pub async fn disconnect_db( } drop(connections); - app.tunnels.stop_tunnel(&body.connection_id).await; - app.proxy_tunnels.stop_tunnel(&body.connection_id).await; + app.reset_connection_transport(&body.connection_id).await; Ok(Json(())) } diff --git a/packages/app-tests/connectionTransport.test.ts b/packages/app-tests/connectionTransport.test.ts new file mode 100644 index 000000000..6d7f95bcd --- /dev/null +++ b/packages/app-tests/connectionTransport.test.ts @@ -0,0 +1,20 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { hasEnabledTransportLayers } from "../../apps/desktop/src/lib/connectionTransport.ts"; + +test("hasEnabledTransportLayers matches effective transport layer visibility", () => { + assert.equal(hasEnabledTransportLayers(undefined), false); + assert.equal(hasEnabledTransportLayers({ transport_layers: [] }), false); + assert.equal( + hasEnabledTransportLayers({ + transport_layers: [{ type: "proxy", id: "proxy", enabled: false, host: "127.0.0.1", port: 1080 }], + }), + false, + ); + assert.equal( + hasEnabledTransportLayers({ + transport_layers: [{ type: "proxy", id: "proxy", host: "127.0.0.1", port: 1080 }], + }), + true, + ); +}); diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index f714149f1..e33d5ff36 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -573,6 +573,24 @@ pub async fn connect_db(state: State<'_, Arc>, config: ConnectionConfi Ok(id) } +#[tauri::command] +pub async fn connection_final_proxy_port( + state: State<'_, Arc>, + config: ConnectionConfig, +) -> Result { + let runtime_config = config.canonicalized(); + if !runtime_config.has_effective_transport_layers() { + return Err("Connection has no configured transport layers".to_string()); + } + + let connection_id = runtime_config.id.clone(); + let db_config = metadata_connection_config(&runtime_config); + state.configs.write().await.insert(connection_id.clone(), runtime_config); + + let (_, port) = state.connection_host_port(&connection_id, &db_config).await?; + Ok(port) +} + #[tauri::command] pub async fn disconnect_db(state: State<'_, Arc>, connection_id: String) -> Result<(), String> { let mut conns = state.connections.write().await; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 962ce8dcc..a32a1a0c9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -363,6 +363,7 @@ pub fn run() { commands::cloud_sync::webdav_sync_download, commands::connection::test_connection, commands::connection::connect_db, + commands::connection::connection_final_proxy_port, commands::connection::disconnect_db, commands::connection::close_database_connection, commands::connection::refresh_connections,