Skip to content

Commit 3cbb136

Browse files
feat: add multi-database driver support to UI
- Update connection modal with database type selector - Add driver selection dropdown (PostgreSQL, Redshift) - Auto-adjust default ports based on selected driver - Update connection logic to use driver factory - Extend connection configuration to include SSL option - Refactor frontend to dynamically use appropriate driver
1 parent 457cb28 commit 3cbb136

File tree

4 files changed

+148
-40
lines changed

4 files changed

+148
-40
lines changed

src-tauri/src/drivers/pgsql.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::{utils::reflective_get, AppState};
1313
#[tauri::command(rename_all = "snake_case")]
1414
pub async fn pgsql_connector(
1515
project_id: &str,
16-
key: Option<[&str; 5]>,
16+
key: Option<[&str; 6]>,
1717
app: AppHandle,
1818
) -> Result<ProjectConnectionStatus> {
1919
let app_state = app.state::<AppState>();
@@ -25,13 +25,14 @@ pub async fn pgsql_connector(
2525
return Ok(ProjectConnectionStatus::Connected);
2626
}
2727

28-
let (user, password, database, host, port_str) = match key {
28+
let (user, password, database, host, port_str, use_ssl) = match key {
2929
Some(key) => (
3030
key[0].to_string(),
3131
key[1].to_string(),
3232
key[2].to_string(),
3333
key[3].to_string(),
3434
key[4].to_string(),
35+
key[5] == "true",
3536
),
3637
None => {
3738
let projects_db = app_state.project_db.lock().await;
@@ -47,6 +48,7 @@ pub async fn pgsql_connector(
4748
project_details[3].clone(),
4849
project_details[4].clone(),
4950
project_details[5].clone(),
51+
project_details.get(6).map(|s| s == "true").unwrap_or(false),
5052
)
5153
}
5254
};

src/App.tsx

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useState, useCallback, useEffect } from "react";
22
import type * as Monaco from "monaco-editor";
33
import { useKeyPressEvent } from "react-use";
4-
import { Database, Play, Save, Settings, ChevronRight, ChevronDown, Server, Table, Plus, X, CheckCircle2, Clock } from "lucide-react";
4+
import { Database, Play, Save, Settings, ChevronRight, ChevronDown, Server, Table, Plus, X, CheckCircle2, Clock, Moon, Sun } from "lucide-react";
55
import Editor from "@monaco-editor/react";
66
import { Button } from "@/components/ui/button";
77
import { ResizeHandle } from "@/components/resize-handle";
@@ -13,15 +13,11 @@ import {
1313
getProjects,
1414
insertProject,
1515
insertQuery,
16-
pgsqlConnector,
17-
pgsqlLoadColumns,
18-
pgsqlLoadSchemas,
19-
pgsqlLoadTables,
20-
pgsqlRunQuery,
2116
ProjectConnectionStatus,
2217
ProjectMap,
2318
TableInfo,
2419
} from "@/tauri";
20+
import { DriverFactory, type DriverType } from "@/lib/database-driver";
2521

2622
type Tab = {
2723
id: number;
@@ -216,6 +212,7 @@ export default function App() {
216212
const [connectionModalOpen, setConnectionModalOpen] = useState(false);
217213
const [viewMode, setViewMode] = useState<"grid" | "record">("grid");
218214
const [selectedRow, setSelectedRow] = useState<number>(0);
215+
const [theme, setTheme] = useState<"light" | "dark">("light");
219216

220217
const [projects, setProjects] = useState<ProjectMap>({});
221218
const [status, setStatus] = useState<Record<string, ProjectConnectionStatus>>({});
@@ -243,6 +240,14 @@ export default function App() {
243240
useEffect(() => { columnsRef.current = columns; }, [columns]);
244241
useEffect(() => { activeProjectRef.current = activeProject; }, [activeProject]);
245242

243+
// Helper to get driver for a project
244+
const getProjectDriver = useCallback((projectId: string) => {
245+
const d = projectsRef.current[projectId];
246+
if (!d) return null;
247+
const driverType = d[0] as DriverType;
248+
return DriverFactory.getDriver(driverType);
249+
}, []);
250+
246251
function registerContextAwareCompletions(monaco: typeof Monaco) {
247252
type TableRef = { schema?: string; table: string };
248253
function stripQuotes(s: string) { return s.replaceAll('"', ""); }
@@ -261,12 +266,15 @@ export default function App() {
261266
async function resolveTableRef(projectId: string, ref: TableRef): Promise<{ schema: string; table: string } | null> {
262267
if (ref.schema) return { schema: ref.schema, table: ref.table };
263268
const projSchemas = schemasRef.current[projectId] || [];
269+
const driver = getProjectDriver(projectId);
270+
if (!driver) return { schema: "public", table: ref.table };
271+
264272
for (const schema of projSchemas) {
265273
const key = `${projectId}::${schema}`;
266274
let t = tablesRef.current[key];
267275
if (!t) {
268276
try {
269-
t = await pgsqlLoadTables(projectId, schema);
277+
t = await driver.loadTables(projectId, schema);
270278
tablesRef.current[key] = t;
271279
setTables((prev) => ({ ...prev, [key]: t! }));
272280
} catch {}
@@ -280,6 +288,8 @@ export default function App() {
280288
provideCompletionItems: async (model, position) => {
281289
const projectId = activeProjectRef.current;
282290
if (!projectId) return { suggestions: [] };
291+
const driver = getProjectDriver(projectId);
292+
if (!driver) return { suggestions: [] };
283293
const textUntilPosition = model.getValueInRange({
284294
startLineNumber: 1, startColumn: 1,
285295
endLineNumber: position.lineNumber, endColumn: position.column,
@@ -315,7 +325,7 @@ export default function App() {
315325
let cols = columnsRef.current[colKey];
316326
if (!cols) {
317327
try {
318-
cols = await pgsqlLoadColumns(projectId, resolved.schema, resolved.table);
328+
cols = await driver.loadColumns(projectId, resolved.schema, resolved.table);
319329
columnsRef.current[colKey] = cols;
320330
setColumns((prev) => ({ ...prev, [colKey]: cols! }));
321331
} catch {}
@@ -330,7 +340,7 @@ export default function App() {
330340
let t = tablesRef.current[key];
331341
if (!t) {
332342
try {
333-
t = await pgsqlLoadTables(projectId, schema);
343+
t = await driver.loadTables(projectId, schema);
334344
tablesRef.current[key] = t;
335345
setTables((prev) => ({ ...prev, [key]: t! }));
336346
} catch {}
@@ -347,7 +357,7 @@ export default function App() {
347357
let cols = columnsRef.current[colKey];
348358
if (!cols) {
349359
try {
350-
cols = await pgsqlLoadColumns(projectId, left, right);
360+
cols = await driver.loadColumns(projectId, left, right);
351361
columnsRef.current[colKey] = cols;
352362
setColumns((prev) => ({ ...prev, [colKey]: cols! }));
353363
} catch {}
@@ -364,7 +374,7 @@ export default function App() {
364374
let t = tablesRef.current[key];
365375
if (!t) {
366376
try {
367-
t = await pgsqlLoadTables(projectId, schema);
377+
t = await driver.loadTables(projectId, schema);
368378
tablesRef.current[key] = t;
369379
setTables((prev) => ({ ...prev, [key]: t! }));
370380
} catch {}
@@ -415,12 +425,15 @@ export default function App() {
415425
const d = projects[project_id];
416426
if (!d) return;
417427
setStatus((s) => ({ ...s, [project_id]: ProjectConnectionStatus.Connecting }));
418-
const key: [string, string, string, string, string] = [d[1], d[2], d[3], d[4], d[5]];
428+
const driverType = d[0] as DriverType;
429+
const useSsl = d[6] === "true"; // 7th element is SSL flag
430+
const key: [string, string, string, string, string, string] = [d[1], d[2], d[3], d[4], d[5], useSsl ? "true" : "false"];
419431
try {
420-
const st = await pgsqlConnector(project_id, key);
432+
const driver = DriverFactory.getDriver(driverType);
433+
const st = await driver.connect(project_id, key);
421434
setStatus((s) => ({ ...s, [project_id]: st }));
422435
if (st === ProjectConnectionStatus.Connected) {
423-
const sc = await pgsqlLoadSchemas(project_id);
436+
const sc = await driver.loadSchemas(project_id);
424437
setSchemas((prev) => ({ ...prev, [project_id]: sc }));
425438
}
426439
} catch {
@@ -436,9 +449,13 @@ export default function App() {
436449
const onLoadTables = useCallback(async (project_id: string, schema: string) => {
437450
const key = `${project_id}::${schema}`;
438451
if (tables[key]) return;
439-
const rows = await pgsqlLoadTables(project_id, schema);
452+
const d = projects[project_id];
453+
if (!d) return;
454+
const driverType = d[0] as DriverType;
455+
const driver = DriverFactory.getDriver(driverType);
456+
const rows = await driver.loadTables(project_id, schema);
440457
setTables((t) => ({ ...t, [key]: rows }));
441-
}, [tables]);
458+
}, [tables, projects]);
442459

443460
const onOpenDefaultTableQuery = useCallback((project_id: string, schema: string, table: string) => {
444461
const sql = `SELECT * FROM "${schema}"."${table}" LIMIT 100;`;
@@ -448,14 +465,18 @@ export default function App() {
448465

449466
const runQuery = useCallback(async () => {
450467
if (!activeProject || !activeTab) return;
451-
const [cols, rows, time] = await pgsqlRunQuery(activeProject, activeTab.editorValue);
468+
const d = projects[activeProject];
469+
if (!d) return;
470+
const driverType = d[0] as DriverType;
471+
const driver = DriverFactory.getDriver(driverType);
472+
const [cols, rows, time] = await driver.runQuery(activeProject, activeTab.editorValue);
452473
setTabs((ts) => {
453474
const copy = ts.slice();
454475
copy[selectedTab] = { ...copy[selectedTab], result: { columns: cols, rows, time } };
455476
return copy;
456477
});
457478
setSelectedRow(0);
458-
}, [activeProject, activeTab, selectedTab]);
479+
}, [activeProject, activeTab, selectedTab, projects]);
459480

460481
const saveQuery = useCallback(async (title: string) => {
461482
if (!activeProject) return;
@@ -478,19 +499,31 @@ export default function App() {
478499
};
479500

480501
const handleSaveConnection = useCallback(async (connection: ConnectionConfig) => {
481-
const details = ["PGSQL", connection.username, connection.password, connection.database, connection.host, connection.port];
502+
const details = [connection.driver, connection.username, connection.password, connection.database, connection.host, connection.port, connection.ssl ? "true" : "false"];
482503
await insertProject(connection.name, details);
483504
await reloadProjects();
484505
}, [reloadProjects]);
485506

507+
const toggleTheme = () => {
508+
setTheme(prev => prev === "light" ? "dark" : "light");
509+
};
510+
511+
useEffect(() => {
512+
if (theme === "dark") {
513+
document.documentElement.classList.add("dark");
514+
} else {
515+
document.documentElement.classList.remove("dark");
516+
}
517+
}, [theme]);
518+
486519
return (
487520
<div className="flex h-screen flex-col bg-background text-foreground">
488521
{/* Top Bar */}
489522
<div className="flex h-12 items-center justify-between border-b border-border bg-card px-4">
490523
<div className="flex items-center gap-3">
491524
<div className="flex items-center gap-2">
492-
<Database className="h-5 w-5 text-white" />
493-
<span className="font-mono text-sm font-semibold text-white">PostgresGUI</span>
525+
<Database className="h-5 w-5" />
526+
<span className="font-mono text-sm font-semibold">PostgresGUI</span>
494527
</div>
495528
{activeProject && activeProjectDetails ? (
496529
<>
@@ -503,9 +536,9 @@ export default function App() {
503536
status[activeProject] === ProjectConnectionStatus.Failed && "bg-destructive",
504537
!status[activeProject] && "bg-destructive"
505538
)} />
506-
<span className="font-mono text-white">{activeProject}</span>
539+
<span className="font-mono">{activeProject}</span>
507540
<span className="text-muted-foreground/50"></span>
508-
<span className="text-white">{activeProjectDetails[4]}:{activeProjectDetails[5]}</span>
541+
<span>{activeProjectDetails[4]}:{activeProjectDetails[5]}</span>
509542
</div>
510543
</>
511544
) : null}
@@ -522,8 +555,8 @@ export default function App() {
522555
}}
523556
disabled={!activeProject}
524557
>
525-
<Save className="h-4 w-4 text-white" />
526-
<span className="text-xs text-white">Save</span>
558+
<Save className="h-4 w-4" />
559+
<span className="text-xs">Save</span>
527560
</Button>
528561
<Button
529562
variant="default"
@@ -532,12 +565,12 @@ export default function App() {
532565
onClick={() => void runQuery()}
533566
disabled={!activeProject}
534567
>
535-
<Play className="h-4 w-4 text-white" />
536-
<span className="text-xs text-white">Execute (⌘+Enter)</span>
568+
<Play className="h-4 w-4" />
569+
<span className="text-xs">Execute (⌘+Enter)</span>
537570
</Button>
538571
<div className="h-4 w-px bg-border" />
539-
<Button variant="ghost" size="icon" className="h-8 w-8">
540-
<Settings className="h-4 w-4 text-white" />
572+
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={toggleTheme}>
573+
{theme === "light" ? <Moon className="h-4 w-4" /> : <Sun className="h-4 w-4" />}
541574
</Button>
542575
</div>
543576
</div>
@@ -578,7 +611,7 @@ export default function App() {
578611
: "bg-card text-muted-foreground hover:bg-accent hover:text-accent-foreground"
579612
)}
580613
>
581-
<button onClick={() => setSelectedTab(idx)} className="font-mono text-xs text-white">
614+
<button onClick={() => setSelectedTab(idx)} className="font-mono text-xs">
582615
Query {idx + 1}
583616
</button>
584617
{tabs.length > 1 && (
@@ -592,26 +625,26 @@ export default function App() {
592625
}}
593626
className="opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100"
594627
>
595-
<X className="h-3 w-3 text-white" />
628+
<X className="h-3 w-3" />
596629
</button>
597630
)}
598631
</div>
599632
))}
600633
</div>
601634
<Button variant="ghost" size="icon" onClick={() => setTabs((ts) => [...ts, { id: ts.length + 1, editorValue: "" }])} className="h-9 w-9 shrink-0">
602-
<Plus className="h-4 w-4 text-white" />
635+
<Plus className="h-4 w-4" />
603636
</Button>
604637
</div>
605638
{/* SQL Editor */}
606639
<div className="relative flex-1 overflow-hidden bg-[var(--color-editor-bg)]" suppressHydrationWarning>
607-
<div className="absolute inset-0 overflow-auto bg-[#1e1e1e]">
640+
<div className="absolute inset-0 overflow-auto bg-editor-bg">
608641
<Editor
609642
height="100%"
610643
defaultLanguage="pgsql"
611644
language="pgsql"
612-
theme="vs-dark"
645+
theme={theme === "light" ? "vs" : "vs-dark"}
613646
loading={
614-
<div className="flex h-full w-full items-center justify-center bg-[#1e1e1e]">
647+
<div className="flex h-full w-full items-center justify-center bg-editor-bg">
615648
<span className="text-muted-foreground text-sm">Loading editor...</span>
616649
</div>
617650
}
@@ -624,7 +657,6 @@ export default function App() {
624657
lineNumbers: "on",
625658
quickSuggestions: { other: true, comments: false, strings: true },
626659
suggestOnTriggerCharacters: true,
627-
theme: "vs-dark",
628660
}}
629661
value={activeTab?.editorValue}
630662
onChange={(v) => {

0 commit comments

Comments
 (0)