Expose forwarded database port for local testing (#803)
Database connection context menus now show a copy action when the connection has enabled transport layers. The action starts or reuses the ordered transport chain, copies the final local forwarding port, and keeps runtime config cached so later disconnect cleanup can stop the created tunnel. Constraint: Local testing sometimes needs external tools to connect through DBX's generated proxy/tunnel endpoint Rejected: Copy full host:port endpoint | user specifically asked for the port and existing tunnel host is always localhost Rejected: Require an active pool before copying | local test workflows may need the tunnel before opening the DBX connection Confidence: high Scope-risk: moderate Tested: pnpm test -- packages/app-tests/connectionTransport.test.ts (ran full app-tests: 862 passed) Tested: pnpm typecheck Tested: pnpm exec oxlint --vue-plugin apps/desktop/src/components/sidebar/TreeItem.vue apps/desktop/src/lib/connectionTransport.ts apps/desktop/src/lib/api.ts apps/desktop/src/lib/tauri.ts apps/desktop/src/lib/http.ts Tested: cargo check -p dbx-core -p dbx-web Tested: cargo check -p dbx Tested: cargo fmt --check --all Co-authored-by: caisin <caisin@caisins-Mac-mini.local>
This commit is contained in:
parent
2c404e73c2
commit
8c0fe3a6e7
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -934,6 +934,8 @@ export default {
|
|||
closeOtherTabs: "关闭其他标签页",
|
||||
closeAllTabs: "关闭全部标签页",
|
||||
copyName: "复制名称",
|
||||
copyFinalProxyPort: "复制最终代理端口",
|
||||
finalProxyPortCopied: "最终代理端口 {port} 已复制",
|
||||
setDefaultDatabase: "设为默认数据库",
|
||||
clearDefaultDatabase: "取消默认数据库",
|
||||
exportDatabase: "导出数据库",
|
||||
|
|
|
|||
|
|
@ -914,6 +914,8 @@ export default {
|
|||
closeOtherTabs: "關閉其他分頁",
|
||||
closeAllTabs: "關閉全部分頁",
|
||||
copyName: "複製名稱",
|
||||
copyFinalProxyPort: "複製最終代理連接埠",
|
||||
finalProxyPortCopied: "最終代理連接埠 {port} 已複製",
|
||||
setDefaultDatabase: "設為預設資料庫",
|
||||
clearDefaultDatabase: "取消預設資料庫",
|
||||
exportDatabase: "匯出資料庫",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ function forward<K extends keyof Backend>(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");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import type { ConnectionConfig } from "@/types/database";
|
||||
|
||||
export function hasEnabledTransportLayers(connection?: Pick<ConnectionConfig, "transport_layers"> | null): boolean {
|
||||
return !!connection?.transport_layers?.some((layer) => layer.enabled !== false);
|
||||
}
|
||||
|
|
@ -151,6 +151,10 @@ export async function connectDb(config: ConnectionConfig): Promise<string> {
|
|||
return post("/api/connection/connect", { config });
|
||||
}
|
||||
|
||||
export async function connectionFinalProxyPort(config: ConnectionConfig): Promise<number> {
|
||||
return post("/api/connection/final-proxy-port", { config });
|
||||
}
|
||||
|
||||
export async function disconnectDb(connectionId: string): Promise<void> {
|
||||
return post("/api/connection/disconnect", { connectionId });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -414,6 +414,10 @@ export async function connectDb(config: ConnectionConfig): Promise<string> {
|
|||
return invoke("connect_db", { config });
|
||||
}
|
||||
|
||||
export async function connectionFinalProxyPort(config: ConnectionConfig): Promise<number> {
|
||||
return invoke("connection_final_proxy_port", { config });
|
||||
}
|
||||
|
||||
export async function disconnectDb(connectionId: string): Promise<void> {
|
||||
return invoke("disconnect_db", { connectionId });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -78,6 +78,24 @@ pub async fn connect_db(
|
|||
Ok(Json(connection_id))
|
||||
}
|
||||
|
||||
pub async fn connection_final_proxy_port(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(body): Json<ConnectRequest>,
|
||||
) -> Result<Json<u16>, 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<Arc<WebState>>,
|
||||
Json(body): Json<DisconnectRequest>,
|
||||
|
|
@ -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(()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
|
@ -573,6 +573,24 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, config: ConnectionConfi
|
|||
Ok(id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn connection_final_proxy_port(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
config: ConnectionConfig,
|
||||
) -> Result<u16, String> {
|
||||
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<AppState>>, connection_id: String) -> Result<(), String> {
|
||||
let mut conns = state.connections.write().await;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue