diff --git a/src-tauri/src/models/connection.rs b/src-tauri/src/models/connection.rs index 00de06c8d7..a80d0cafdd 100644 --- a/src-tauri/src/models/connection.rs +++ b/src-tauri/src/models/connection.rs @@ -6,6 +6,12 @@ pub struct ConnectionConfig { pub id: String, pub name: String, pub db_type: DatabaseType, + #[serde(default)] + pub driver_profile: Option, + #[serde(default)] + pub driver_label: Option, + #[serde(default)] + pub url_params: Option, pub host: String, pub port: u16, pub username: String, @@ -64,6 +70,7 @@ impl ConnectionConfig { .unwrap_or_default(); let username = encode_url_part(&self.username); let password = encode_url_part(&self.password); + let params = self.normalized_url_params(); match self.db_type { DatabaseType::Sqlite | DatabaseType::DuckDb => { @@ -79,14 +86,11 @@ impl ConnectionConfig { format!("{scheme}://{username}:{password}@{host}:{port}/") } } - DatabaseType::Mysql => format!( - "mysql://{}:{}@{host}:{port}{db_part}?ssl-mode=preferred", - username, password - ), - DatabaseType::Postgres => format!( - "postgres://{}:{}@{host}:{port}{db_part}", - username, password - ), + DatabaseType::Mysql => format!("mysql://{}:{}@{host}:{port}{db_part}?{params}", username, password), + DatabaseType::Postgres => { + let suffix = if params.is_empty() { String::new() } else { format!("?{params}") }; + format!("postgres://{}:{}@{host}:{port}{db_part}{suffix}", username, password) + } DatabaseType::ClickHouse => format!( "http://{host}:{port}{db_part}" ), @@ -107,6 +111,23 @@ impl ConnectionConfig { ), } } + + fn normalized_url_params(&self) -> String { + let value = self.url_params.as_deref().unwrap_or("").trim(); + match self.db_type { + DatabaseType::Mysql => { + if value.is_empty() { + "ssl-mode=preferred".to_string() + } else if value.contains("ssl-mode=") { + value.trim_start_matches('?').to_string() + } else { + format!("ssl-mode=preferred&{}", value.trim_start_matches('?')) + } + } + DatabaseType::Postgres => value.trim_start_matches('?').to_string(), + _ => value.trim_start_matches('?').to_string(), + } + } } fn encode_url_part(value: &str) -> String { @@ -122,6 +143,9 @@ mod tests { id: "id".to_string(), name: "name".to_string(), db_type: DatabaseType::Mysql, + driver_profile: None, + driver_label: None, + url_params: None, host: "10.1.2.3".to_string(), port: 2883, username: username.to_string(), @@ -157,4 +181,27 @@ mod tests { "mysql://root:p%40ss%3Aword%231@10.1.2.3:2883/db%2Fname?ssl-mode=preferred" ); } + + #[test] + fn mysql_url_appends_custom_params() { + let mut config = mysql_config("root", "secret", Some("test")); + config.url_params = Some("charset=utf8mb4".to_string()); + + assert_eq!( + config.connection_url(), + "mysql://root:secret@10.1.2.3:2883/test?ssl-mode=preferred&charset=utf8mb4" + ); + } + + #[test] + fn postgres_url_appends_custom_params() { + let mut config = mysql_config("postgres", "secret", Some("test")); + config.db_type = DatabaseType::Postgres; + config.url_params = Some("sslmode=disable".to_string()); + + assert_eq!( + config.connection_url(), + "postgres://postgres:secret@10.1.2.3:2883/test?sslmode=disable" + ); + } } diff --git a/src/App.vue b/src/App.vue index e853f6a7de..3eac20f1f3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -78,7 +78,7 @@ const activeTabContext = computed(() => { const items = [ connection.name, - connection.db_type.toUpperCase(), + connectionDriverLabel(connection), ]; if (tab.tableMeta?.tableName) { @@ -100,7 +100,7 @@ const activeConnectionValue = computed(() => activeConnection.value?.id || ""); const connectionStats = computed(() => { const total = connectionStore.connections.length; const connected = connectionStore.connectedIds.size; - const types = new Set(connectionStore.connections.map((connection) => connection.db_type)).size; + const types = new Set(connectionStore.connections.map((connection) => connection.driver_profile || connection.db_type)).size; return { total, connected, types }; }); @@ -110,6 +110,14 @@ function connectionDisplayName(connectionId: string): string { return connectionStore.getConfig(connectionId)?.name || connectionId; } +function connectionDriverLabel(connection?: ConnectionConfig): string { + return connection?.driver_label || connection?.db_type.toUpperCase() || ""; +} + +function connectionIconType(connection?: ConnectionConfig): string { + return connection?.driver_profile || connection?.db_type || "postgres"; +} + function connectionColor(connectionId: string): string { return connectionStore.getConfig(connectionId)?.color || ""; } @@ -424,6 +432,9 @@ async function setupFileDrop() { id: crypto.randomUUID(), name, db_type: dbType, + driver_profile: dbType, + driver_label: dbType === "duckdb" ? "DuckDB" : "SQLite", + url_params: "", host: path, port: 0, username: "", @@ -599,7 +610,7 @@ async function setupFileDrop() { / - {{ activeConnection?.db_type.toUpperCase() }} + {{ connectionDriverLabel(activeConnection) }} / +
+ + +
+
@@ -326,6 +377,11 @@ watch([() => editingId.value, () => open.value], () => {
+ +
+ + +
diff --git a/src/components/sidebar/TreeItem.vue b/src/components/sidebar/TreeItem.vue index bb756168a6..782358cc91 100644 --- a/src/components/sidebar/TreeItem.vue +++ b/src/components/sidebar/TreeItem.vue @@ -223,6 +223,11 @@ const isConnected = computed(() => props.node.type === "connection" && !!props.node.connectionId && connectionStore.connectedIds.has(props.node.connectionId) ); +function connectionIconType(connectionId?: string) { + const config = connectionId ? connectionStore.getConfig(connectionId) : undefined; + return config?.driver_profile || config?.db_type || "postgres"; +} + const connectionColor = computed(() => { const connectionId = props.node.connectionId; return connectionId ? connectionStore.getConfig(connectionId)?.color || "" : ""; @@ -272,7 +277,7 @@ async function showMore() { - + {{ isGroupLabel(node) ? t(node.label) : node.label }} diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index ca0e3a7738..1cb4381369 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -27,6 +27,9 @@ export default { password: "Password", database: "Database", databasePlaceholder: "Optional", + driverName: "Driver Name", + driverNamePlaceholder: "Vendor or environment name", + urlParams: "URL Params", test: "Test", testing: "Testing...", saveAndConnect: "Save & Connect", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 1ea45a8120..698a50f405 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -27,6 +27,9 @@ export default { password: "密码", database: "数据库", databasePlaceholder: "可选", + driverName: "驱动名称", + driverNamePlaceholder: "厂商或环境名称", + urlParams: "URL 参数", test: "测试", testing: "测试中...", saveAndConnect: "保存并连接", diff --git a/src/stores/connectionStore.ts b/src/stores/connectionStore.ts index 7e27d61196..a18582d2c0 100644 --- a/src/stores/connectionStore.ts +++ b/src/stores/connectionStore.ts @@ -22,8 +22,28 @@ export const useConnectionStore = defineStore("connection", () => { return connections.value.find((c) => c.id === connectionId); } + function normalizeConnection(config: ConnectionConfig): ConnectionConfig { + const labelMap: Record = { + mysql: "MySQL", + postgres: "PostgreSQL", + sqlite: "SQLite", + redis: "Redis", + duckdb: "DuckDB", + clickhouse: "ClickHouse", + sqlserver: "SQL Server", + mongodb: "MongoDB", + oracle: "Oracle", + }; + return { + ...config, + driver_profile: config.driver_profile || config.db_type, + driver_label: config.driver_label || labelMap[config.driver_profile || config.db_type] || config.db_type, + url_params: config.url_params || "", + }; + } + function addConnection(config: ConnectionConfig) { - connections.value.push(config); + connections.value.push(normalizeConnection(config)); persistConnections(); } @@ -37,6 +57,7 @@ export const useConnectionStore = defineStore("connection", () => { } function updateConnection(config: ConnectionConfig) { + config = normalizeConnection(config); const idx = connections.value.findIndex((c) => c.id === config.id); if (idx >= 0) connections.value[idx] = config; const node = findNode(treeNodes.value, config.id); @@ -50,6 +71,7 @@ export const useConnectionStore = defineStore("connection", () => { } async function connect(config: ConnectionConfig) { + config = normalizeConnection(config); const id = await api.connectDb(config); activeConnectionId.value = id; connectedIds.value.add(id); @@ -439,12 +461,13 @@ export const useConnectionStore = defineStore("connection", () => { for (const config of imported) { if (!connections.value.find((c) => c.id === config.id)) { config.id = crypto.randomUUID(); - addConnection(config); + const normalized = normalizeConnection(config); + addConnection(normalized); treeNodes.value.push({ - id: config.id, - label: config.name, + id: normalized.id, + label: normalized.name, type: "connection" as const, - connectionId: config.id, + connectionId: normalized.id, isExpanded: false, children: [], }); @@ -454,7 +477,7 @@ export const useConnectionStore = defineStore("connection", () => { async function initFromDisk() { const saved = await api.loadConnections(); - connections.value = saved; + connections.value = saved.map(normalizeConnection); treeNodes.value = saved.map((config) => ({ id: config.id, label: config.name, diff --git a/src/types/database.ts b/src/types/database.ts index 98c164376a..501d044b16 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -4,6 +4,9 @@ export interface ConnectionConfig { id: string; name: string; db_type: DatabaseType; + driver_profile?: string; + driver_label?: string; + url_params?: string; host: string; port: number; username: string;