From 47b040fadfebc31ab8182850d499dd871a49a112 Mon Sep 17 00:00:00 2001 From: "A.Shakhmatov" Date: Wed, 11 Feb 2026 22:19:38 +0300 Subject: [PATCH] feat: add DSN input mode to connection dialog Add Fields/DSN toggle in New Connection dialog. DSN mode accepts a PostgreSQL connection string (postgresql://user:pass@host:port/db?sslmode=...) and auto-parses it into individual fields. Switching between modes syncs the data bidirectionally. Co-Authored-By: Claude Opus 4.6 --- .../connections/ConnectionDialog.tsx | 265 +++++++++++++----- 1 file changed, 196 insertions(+), 69 deletions(-) diff --git a/src/components/connections/ConnectionDialog.tsx b/src/components/connections/ConnectionDialog.tsx index 220971c..19c0f59 100644 --- a/src/components/connections/ConnectionDialog.tsx +++ b/src/components/connections/ConnectionDialog.tsx @@ -21,6 +21,46 @@ import type { ConnectionConfig } from "@/types"; import { ENVIRONMENTS } from "@/lib/environment"; import { Loader2, X } from "lucide-react"; +type InputMode = "fields" | "dsn"; + +function parseDsn(dsn: string): Partial | null { + try { + const trimmed = dsn.trim(); + // Support both postgres:// and postgresql:// + const match = trimmed.match( + /^(?:postgres(?:ql)?):\/\/(?:([^:@]*?)(?::([^@]*?))?@)?([^/:?]+)(?::(\d+))?(?:\/([^?]*))?(?:\?(.*))?$/ + ); + if (!match) return null; + + const [, user, password, host, port, database, queryStr] = match; + + const result: Partial = {}; + if (host) result.host = decodeURIComponent(host); + if (port) result.port = parseInt(port, 10); + if (user) result.user = decodeURIComponent(user); + if (password !== undefined) result.password = decodeURIComponent(password); + if (database) result.database = decodeURIComponent(database); + + if (queryStr) { + const params = new URLSearchParams(queryStr); + const ssl = params.get("sslmode"); + if (ssl) result.ssl_mode = ssl; + } + + return result; + } catch { + return null; + } +} + +function buildDsn(config: ConnectionConfig): string { + const ssl = config.ssl_mode ?? "prefer"; + const user = encodeURIComponent(config.user); + const pass = config.password ? `:${encodeURIComponent(config.password)}` : ""; + const auth = config.user ? `${user}${pass}@` : ""; + return `postgresql://${auth}${config.host}:${config.port}/${encodeURIComponent(config.database)}?sslmode=${ssl}`; +} + const CONNECTION_COLORS = [ { name: "Red", value: "#ef4444" }, { name: "Orange", value: "#f97316" }, @@ -53,14 +93,19 @@ const emptyConfig: ConnectionConfig = { export function ConnectionDialog({ open, onOpenChange, connection }: Props) { const [form, setForm] = useState(emptyConfig); + const [mode, setMode] = useState("fields"); + const [dsn, setDsn] = useState(""); + const [dsnError, setDsnError] = useState(null); const saveMutation = useSaveConnection(); const testMutation = useTestConnection(); useEffect(() => { if (open) { - setForm( - connection ?? { ...emptyConfig, id: crypto.randomUUID() } - ); + const config = connection ?? { ...emptyConfig, id: crypto.randomUUID() }; + setForm(config); + setMode("fields"); + setDsn(connection ? buildDsn(config) : ""); + setDsnError(null); } }, [open, connection]); @@ -116,76 +161,158 @@ export function ConnectionDialog({ open, onOpenChange, connection }: Props) { placeholder="My Database" /> +
- update("host", e.target.value)} - /> -
-
- - update("port", parseInt(e.target.value) || 5432)} - /> -
-
- - update("user", e.target.value)} - /> -
-
- - update("password", e.target.value)} - /> -
-
- - update("database", e.target.value)} - /> -
-
- - +
+ + +
+ + {mode === "dsn" ? ( +
+ +
+ { + const value = e.target.value; + setDsn(value); + if (value.trim()) { + const parsed = parseDsn(value); + if (parsed) { + setForm((f) => ({ ...f, ...parsed })); + setDsnError(null); + } else { + setDsnError("Invalid DSN format"); + } + } else { + setDsnError(null); + } + }} + placeholder="postgresql://user:password@host:5432/database?sslmode=prefer" + /> + {dsnError && ( +

{dsnError}

+ )} +

+ postgresql://user:password@host:port/database?sslmode=prefer +

+
+
+ ) : ( + <> +
+ + update("host", e.target.value)} + /> +
+
+ + update("port", parseInt(e.target.value) || 5432)} + /> +
+
+ + update("user", e.target.value)} + /> +
+
+ + update("password", e.target.value)} + /> +
+
+ + update("database", e.target.value)} + /> +
+
+ + +
+ + )}