From 582ef594b194a06a9e7048463ba4b9bc277cd4e3 Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 30 Jan 2026 22:52:16 +0530 Subject: [PATCH 1/4] feat: Implemented schema based fetching --- bridge/src/connectors/mariadb.ts | 17 ++++++++++++++++ bridge/src/connectors/mysql.ts | 17 ++++++++++++++++ bridge/src/connectors/postgres.ts | 20 ++++++++++++++++++ bridge/src/handlers/databaseHandlers.ts | 20 +++++++++++++++++- bridge/src/jsonRpcHandler.ts | 3 +++ bridge/src/services/queryExecutor.ts | 11 ++++++++++ src/hooks/useDatabaseDetails.ts | 27 ++++++++++++++++++++++--- src/hooks/useDbQueries.ts | 20 ++++++++++++++---- src/pages/DatabaseDetails.tsx | 26 ++++++++++++++++++++++-- src/services/bridgeApi.ts | 18 +++++++++++++++-- 10 files changed, 167 insertions(+), 12 deletions(-) diff --git a/bridge/src/connectors/mariadb.ts b/bridge/src/connectors/mariadb.ts index 5f0c07d..896de96 100644 --- a/bridge/src/connectors/mariadb.ts +++ b/bridge/src/connectors/mariadb.ts @@ -1765,3 +1765,20 @@ export async function searchTable( await pool.end(); } } + +/** + * listSchemaNames: Retrieves just the names of schemas (databases). + * Lightweight version for schema selector. + */ +export async function listSchemaNames(cfg: MariaDBConfig): Promise { + const pool = mysql.createPool(createPoolConfig(cfg)); + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.query(LIST_SCHEMAS); + return (rows as any[]).map((r: any) => r.name); + } finally { + connection.release(); + await pool.end(); + } +} diff --git a/bridge/src/connectors/mysql.ts b/bridge/src/connectors/mysql.ts index 0bc4116..3500830 100644 --- a/bridge/src/connectors/mysql.ts +++ b/bridge/src/connectors/mysql.ts @@ -1740,3 +1740,20 @@ export async function searchTable( await pool.end(); } } + +/** + * listSchemaNames: Retrieves just the names of schemas (databases). + * Lightweight version for schema selector. + */ +export async function listSchemaNames(cfg: MySQLConfig): Promise { + const pool = mysql.createPool(createPoolConfig(cfg)); + const connection = await pool.getConnection(); + + try { + const [rows] = await connection.query(LIST_SCHEMAS); + return (rows as any[]).map((r: any) => r.name); + } finally { + connection.release(); + await pool.end(); + } +} diff --git a/bridge/src/connectors/postgres.ts b/bridge/src/connectors/postgres.ts index e576be3..58654c1 100644 --- a/bridge/src/connectors/postgres.ts +++ b/bridge/src/connectors/postgres.ts @@ -1869,3 +1869,23 @@ export async function searchTable( } catch (_) { } } } + +/** + * listSchemaNames: Retrieves just the names of schemas. + * Lightweight version of listSchemas. + */ +export async function listSchemaNames(connection: PGConfig): Promise { + // Check cache first (re-use schemas cache if available, or a new cache if needed) + // For now, simpler to just query as it's very fast + const client = createClient(connection); + + try { + await client.connect(); + const res = await client.query(PG_LIST_SCHEMAS); + return res.rows.map((r: any) => r.name); + } finally { + try { + await client.end(); + } catch (e) { } + } +} diff --git a/bridge/src/handlers/databaseHandlers.ts b/bridge/src/handlers/databaseHandlers.ts index 3b80a01..4a9c622 100644 --- a/bridge/src/handlers/databaseHandlers.ts +++ b/bridge/src/handlers/databaseHandlers.ts @@ -85,7 +85,7 @@ export class DatabaseHandlers { } const { conn, dbType } = await this.dbService.getDatabaseConnection(dbId); - const tables = await this.queryExecutor.listTables(conn, dbType); + const tables = await this.queryExecutor.listTables(conn, dbType, params.schema); this.rpc.sendResponse(id, { ok: true, data: tables }); } catch (e: any) { this.logger?.error({ e }, "db.listTables failed"); @@ -93,6 +93,24 @@ export class DatabaseHandlers { } } + async handleListSchemas(params: any, id: number | string) { + try { + const { id: dbId } = params || {}; + if (!dbId) { + return this.rpc.sendError(id, { + code: "BAD_REQUEST", + message: "Missing id", + }); + } + const { conn, dbType } = await this.dbService.getDatabaseConnection(dbId); + const schemas = await this.queryExecutor.listSchemaNames(conn, dbType); + this.rpc.sendResponse(id, { ok: true, data: schemas }); + } catch (e: any) { + this.logger?.error({ e }, "db.listSchemas failed"); + this.rpc.sendError(id, { code: "IO_ERROR", message: String(e) }); + } + } + async handleGetSchema(params: any, id: number | string) { try { const { id: dbId, schema } = params || {}; diff --git a/bridge/src/jsonRpcHandler.ts b/bridge/src/jsonRpcHandler.ts index ba92907..5c45512 100644 --- a/bridge/src/jsonRpcHandler.ts +++ b/bridge/src/jsonRpcHandler.ts @@ -131,6 +131,9 @@ export function registerDbHandlers( rpcRegister("db.getSchema", (p, id) => databaseHandlers.handleGetSchema(p, id) ); + rpcRegister("db.listSchemas", (p, id) => + databaseHandlers.handleListSchemas(p, id) + ); // ========================================== // MIGRATION HANDLERS diff --git a/bridge/src/services/queryExecutor.ts b/bridge/src/services/queryExecutor.ts index 8083453..06baeea 100644 --- a/bridge/src/services/queryExecutor.ts +++ b/bridge/src/services/queryExecutor.ts @@ -320,4 +320,15 @@ export class QueryExecutor { }; } } + + async listSchemaNames(conn: DatabaseConfig, dbType: DBType): Promise { + if (dbType === DBType.POSTGRES) { + return this.postgres.listSchemaNames(conn); + } else if (dbType === DBType.MARIADB) { + return this.mariadb.listSchemaNames(conn); + } else if (dbType === DBType.MYSQL) { + return this.mysql.listSchemaNames(conn); + } + return ["public"]; + } } diff --git a/src/hooks/useDatabaseDetails.ts b/src/hooks/useDatabaseDetails.ts index 1f2c277..bf4ac88 100644 --- a/src/hooks/useDatabaseDetails.ts +++ b/src/hooks/useDatabaseDetails.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { bridgeApi } from "@/services/bridgeApi"; -import { useDatabase, useTables, useTableData, usePrefetch, useInvalidateCache } from "@/hooks/useDbQueries"; +import { useDatabase, useTables, useTableData, usePrefetch, useInvalidateCache, useSchemaNames } from "@/hooks/useDbQueries"; import { QueryProgress, SelectedTable, TableInfo, TableRow } from "@/types/database"; interface UseDatabaseDetailsOptions { @@ -34,6 +34,9 @@ interface UseDatabaseDetailsReturn { handlePageChange: (page: number) => Promise; handlePageSizeChange: (size: number) => Promise; refetchTableData: () => void; + schemas: string[]; + selectedSchema: string; + setSelectedSchema: (schema: string) => void; } export function useDatabaseDetails({ @@ -43,16 +46,31 @@ export function useDatabaseDetails({ const { data: dbDetails } = useDatabase(dbId); const databaseName = dbDetails?.name || "Database"; + const { data: schemas = [] } = useSchemaNames(dbId); + const [selectedSchema, setSelectedSchema] = useState("public"); + + // Auto-select first schema if current one is invalid + useEffect(() => { + if (schemas.length > 0 && !schemas.includes(selectedSchema)) { + // Prefer public if available, otherwise first schema + if (schemas.includes("public")) { + setSelectedSchema("public"); + } else { + setSelectedSchema(schemas[0]); + } + } + }, [schemas, selectedSchema]); + const { data: tablesData = [], isLoading: loadingTables, refetch: refetchTables, isRefetching: isRefetchingTables - } = useTables(dbId); + } = useTables(dbId, selectedSchema); // Transform tables data const tables: TableInfo[] = tablesData.map((item: any) => ({ - schema: item.schema || "public", + schema: item.schema || selectedSchema || "public", name: item.name || "unknown", type: item.type || "table", })); @@ -311,5 +329,8 @@ export function useDatabaseDetails({ handlePageChange, handlePageSizeChange, refetchTableData, + schemas, + selectedSchema, + setSelectedSchema }; } diff --git a/src/hooks/useDbQueries.ts b/src/hooks/useDbQueries.ts index 501c5b9..dc597ab 100644 --- a/src/hooks/useDbQueries.ts +++ b/src/hooks/useDbQueries.ts @@ -12,7 +12,7 @@ export const queryKeys = { database: (id: string) => ["databases", id] as const, // Tables - tables: (dbId: string) => ["tables", dbId] as const, + tables: (dbId: string, schema?: string) => ["tables", dbId, schema || "all"] as const, tableData: (dbId: string, schema: string, table: string, page: number, pageSize: number) => ["tableData", dbId, schema, table, page, pageSize] as const, @@ -93,11 +93,11 @@ export function useMigrations(dbId: string | undefined) { * - Returns cached data instantly if available * - Background refetch if stale */ -export function useTables(dbId: string | undefined) { +export function useTables(dbId: string | undefined, schema?: string) { return useQuery({ - queryKey: queryKeys.tables(dbId!), + queryKey: queryKeys.tables(dbId!, schema), queryFn: async () => { - const result = await bridgeApi.listTables(dbId!); + const result = await bridgeApi.listTables(dbId!, schema); return result.map((item: any): TableInfo => ({ schema: item.schema || "public", name: item.name || "unknown", @@ -110,6 +110,18 @@ export function useTables(dbId: string | undefined) { }); } +/** + * Fetch schema names for a database + */ +export function useSchemaNames(dbId: string | undefined) { + return useQuery({ + queryKey: ["schemaNames", dbId] as const, + queryFn: () => bridgeApi.listSchemas(dbId!), + enabled: !!dbId, + staleTime: STALE_TIMES.schemas, + }); +} + /** * Fetch paginated table data * - Each page is cached separately diff --git a/src/pages/DatabaseDetails.tsx b/src/pages/DatabaseDetails.tsx index 084deb8..a17c215 100644 --- a/src/pages/DatabaseDetails.tsx +++ b/src/pages/DatabaseDetails.tsx @@ -70,6 +70,9 @@ const DatabaseDetail = () => { handlePageChange, handlePageSizeChange, refetchTableData, + schemas, + selectedSchema, + setSelectedSchema, } = useDatabaseDetails({ dbId, bridgeReady: bridgeReady ?? false, @@ -159,9 +162,28 @@ const DatabaseDetail = () => { )}
-

{databaseName || 'Database'}

+
+

{databaseName || 'Database'}

+ {schemas.length > 0 && ( + + + + + + {schemas.map(s => ( + setSelectedSchema(s)}> + {s} + + ))} + + + )} +

- {tables.length} tables + {tables.length} tables in {selectedSchema}

diff --git a/src/services/bridgeApi.ts b/src/services/bridgeApi.ts index d01cb9a..a0eefe1 100644 --- a/src/services/bridgeApi.ts +++ b/src/services/bridgeApi.ts @@ -289,14 +289,14 @@ class BridgeApiService { /** * List all tables in a database */ - async listTables(id: string): Promise { + async listTables(id: string, schema?: string): Promise { // Changed return type to any[] to match typical result shape [{schema, name, type}] try { if (!id) { throw new Error("Database ID is required"); } - const result = await bridgeRequest("db.listTables", { id }); + const result = await bridgeRequest("db.listTables", { id, schema }); return result?.data || []; } catch (error: any) { console.error("Failed to list tables:", error); @@ -304,6 +304,20 @@ class BridgeApiService { } } + async listSchemas(id: string): Promise { + try { + if (!id) { + throw new Error("Database ID is required"); + } + + const result = await bridgeRequest("db.listSchemas", { id }); + return result?.data || []; + } catch (error: any) { + console.error("Failed to list schemas:", error); + throw new Error(`Failed to list schemas: ${error.message}`); + } + } + /** * Alias for getDatabaseStats - used by useDbQueries hook */ From 4ac13949310e6c866898e788855fef9e786c0d52 Mon Sep 17 00:00:00 2001 From: Yash Date: Sat, 31 Jan 2026 00:03:28 +0530 Subject: [PATCH 2/4] feat: implemented the table creation based on schemas --- bridge/src/utils/migrationGenerator.ts | 19 +++++-------- package.json | 1 + pnpm-lock.yaml | 11 +++++--- src/components/database/MigrationsPanel.tsx | 27 ++++++++++++++++++- .../database/TablesExplorerPanel.tsx | 4 ++- src/pages/DatabaseDetails.tsx | 1 + 6 files changed, 45 insertions(+), 18 deletions(-) diff --git a/bridge/src/utils/migrationGenerator.ts b/bridge/src/utils/migrationGenerator.ts index 3b09692..37e1bd2 100644 --- a/bridge/src/utils/migrationGenerator.ts +++ b/bridge/src/utils/migrationGenerator.ts @@ -58,10 +58,9 @@ export function generateCreateTableMigration(params: { const allDefs = [...[columnDefs], ...fkDefs].filter(Boolean).join(",\n"); - // For MySQL/MariaDB, don't use schema prefix (database is the schema) - const tableRef = (dbType === "mysql" || dbType === "mariadb") - ? quoteIdent(tableName, dbType) - : `${quoteIdent(schemaName, dbType)}.${quoteIdent(tableName, dbType)}`; + // For MySQL/MariaDB, use database.table format (schemas are databases) + // For Postgres, use schema.table format + const tableRef = `${quoteIdent(schemaName, dbType)}.${quoteIdent(tableName, dbType)}`; // Generate UP SQL const upSQL = `CREATE TABLE ${tableRef} ( @@ -94,10 +93,8 @@ export function generateAlterTableMigration(params: { const name = `alter_${tableName}_table`; const filename = `${version}_${name}.sql`; - // For MySQL/MariaDB, don't use schema prefix - const fullTableName = (dbType === "mysql" || dbType === "mariadb") - ? quoteIdent(tableName, dbType) - : `${quoteIdent(schemaName, dbType)}.${quoteIdent(tableName, dbType)}`; + // For all database types, use schema/database prefix + const fullTableName = `${quoteIdent(schemaName, dbType)}.${quoteIdent(tableName, dbType)}`; // Build UP SQL const upStatements: string[] = []; @@ -251,10 +248,8 @@ export function generateDropTableMigration(params: { const name = `drop_${tableName}_table`; const filename = `${version}_${name}.sql`; - // For MySQL/MariaDB, don't use schema prefix - const fullTableName = (dbType === "mysql" || dbType === "mariadb") - ? quoteIdent(tableName, dbType) - : `${quoteIdent(schemaName, dbType)}.${quoteIdent(tableName, dbType)}`; + // For all database types, use schema/database prefix + const fullTableName = `${quoteIdent(schemaName, dbType)}.${quoteIdent(tableName, dbType)}`; let upSQL = ""; if (mode === "CASCADE") { diff --git a/package.json b/package.json index 666dd72..8315075 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", + "baseline-browser-mapping": "^2.9.19", "tw-animate-css": "^1.4.0", "typescript": "~5.8.3", "vite": "^7.0.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4bbe8d8..7ed4353 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,6 +147,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.6.0 version: 4.7.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) + baseline-browser-mapping: + specifier: ^2.9.19 + version: 2.9.19 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -1452,8 +1455,8 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} - baseline-browser-mapping@2.8.29: - resolution: {integrity: sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==} + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true browserslist@4.28.0: @@ -3342,11 +3345,11 @@ snapshots: dependencies: tslib: 2.8.1 - baseline-browser-mapping@2.8.29: {} + baseline-browser-mapping@2.9.19: {} browserslist@4.28.0: dependencies: - baseline-browser-mapping: 2.8.29 + baseline-browser-mapping: 2.9.19 caniuse-lite: 1.0.30001756 electron-to-chromium: 1.5.258 node-releases: 2.0.27 diff --git a/src/components/database/MigrationsPanel.tsx b/src/components/database/MigrationsPanel.tsx index ba0b408..d994b7a 100644 --- a/src/components/database/MigrationsPanel.tsx +++ b/src/components/database/MigrationsPanel.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { CheckCircle2, Clock, AlertCircle, Database, Play, Undo2, Trash2, Eye } from "lucide-react"; +import { CheckCircle2, Clock, AlertCircle, Database, Play, Undo2, Trash2, Eye, RefreshCw } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -22,6 +22,22 @@ export default function MigrationsPanel({ migrations, baselined, dbId }: Migrati const [selectedMigration, setSelectedMigration] = useState<{ version: string; name: string } | null>(null); const [showSQLDialog, setShowSQLDialog] = useState(false); const [sqlContent, setSqlContent] = useState<{ up: string; down: string } | null>(null); + const [isRefreshing, setIsRefreshing] = useState(false); + + const handleRefresh = async () => { + setIsRefreshing(true); + try { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["migrations", dbId] }), + queryClient.invalidateQueries({ queryKey: ["tables", dbId] }), + queryClient.invalidateQueries({ queryKey: ["schema", dbId] }), + queryClient.invalidateQueries({ queryKey: ["schemaNames", dbId] }), + ]); + toast.success("Refreshed successfully"); + } finally { + setIsRefreshing(false); + } + }; // Merge and sort migrations const appliedVersions = new Set(applied.map((m) => m.version)); @@ -115,6 +131,15 @@ export default function MigrationsPanel({ migrations, baselined, dbId }: Migrati {baselined ? "Baselined" : "Not Baselined"} +

Schema version control and migration status diff --git a/src/components/database/TablesExplorerPanel.tsx b/src/components/database/TablesExplorerPanel.tsx index 0faa00f..4b81067 100644 --- a/src/components/database/TablesExplorerPanel.tsx +++ b/src/components/database/TablesExplorerPanel.tsx @@ -11,6 +11,7 @@ interface TablesExplorerPanelProps { dbId: string; tables: TableInfo[]; selectedTable: SelectedTable | null; + selectedSchema: string; onSelectTable: (tableName: string, schemaName: string) => void; loading?: boolean; } @@ -19,6 +20,7 @@ export default function TablesExplorerPanel({ dbId, tables, selectedTable, + selectedSchema, onSelectTable, loading = false, }: TablesExplorerPanelProps) { @@ -160,7 +162,7 @@ export default function TablesExplorerPanel({ dbId={dbId} open={createTableOpen} onOpenChange={setCreateTableOpen} - schemaName={selectedTable?.schema || ''} + schemaName={selectedSchema} /> diff --git a/src/pages/DatabaseDetails.tsx b/src/pages/DatabaseDetails.tsx index a17c215..429be47 100644 --- a/src/pages/DatabaseDetails.tsx +++ b/src/pages/DatabaseDetails.tsx @@ -254,6 +254,7 @@ const DatabaseDetail = () => { dbId={dbId || ''} tables={tables} selectedTable={selectedTable} + selectedSchema={selectedSchema} onSelectTable={handleTableSelect} loading={loadingTables} /> From 64795373ebeb2b906a0edfc8a99ce834394b422b Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 3 Feb 2026 18:20:00 +0530 Subject: [PATCH 3/4] feat: add schema filtering dropdown to ER diagram component --- .../er-diagram/ERDiagramContent.tsx | 79 +++++++++++++++++-- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/src/components/er-diagram/ERDiagramContent.tsx b/src/components/er-diagram/ERDiagramContent.tsx index 342481f..5261db7 100644 --- a/src/components/er-diagram/ERDiagramContent.tsx +++ b/src/components/er-diagram/ERDiagramContent.tsx @@ -1,5 +1,5 @@ import { toPng, toSvg } from "html-to-image"; -import { ArrowLeft, Database, Download, Filter, LayoutGrid, Search, X } from "lucide-react"; +import { ArrowLeft, ChevronDown, Database, Download, Filter, LayoutGrid, Layers, Search, X } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { @@ -26,6 +26,13 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; interface Column extends ColumnDetails { fkRef?: string; // e.g., "public.roles.id" @@ -61,6 +68,7 @@ const ERDiagramContent: React.FC = ({ nodeTypes }) => { const [searchQuery, setSearchQuery] = useState(""); const [selectedNodeId, setSelectedNodeId] = useState(null); const [hoveredEdge, setHoveredEdge] = useState(null); + const [selectedSchema, setSelectedSchema] = useState("__all__"); // Use React Query for schema data (cached!) const { @@ -75,14 +83,38 @@ const ERDiagramContent: React.FC = ({ nodeTypes }) => { ? "Schema data found, but no tables to render." : null; - // Transform schema to ER nodes/edges when data changes + // Get available schema names for the dropdown + const availableSchemas = useMemo(() => { + if (!schemaData?.schemas) return []; + return schemaData.schemas + .filter(s => s.tables?.length > 0) + .map(s => s.name); + }, [schemaData]); + + // Filter schema data based on selected schema + const filteredSchemaData = useMemo((): DatabaseSchemaDetails | null => { + if (!schemaData) return null; + if (selectedSchema === "__all__") return schemaData; + + return { + ...schemaData, + schemas: schemaData.schemas.filter(s => s.name === selectedSchema) + }; + }, [schemaData, selectedSchema]); + + // Transform schema to ER nodes/edges when data or filter changes useEffect(() => { - if (schemaData && schemaData.schemas?.some(s => s.tables?.length)) { - const { nodes: newNodes, edges: newEdges } = transformSchemaToER(schemaData); + if (filteredSchemaData && filteredSchemaData.schemas?.some(s => s.tables?.length)) { + const { nodes: newNodes, edges: newEdges } = transformSchemaToER(filteredSchemaData); setNodes(newNodes as typeof nodes); setEdges(newEdges); + // Fit view after layout change + setTimeout(() => reactFlowInstance?.fitView({ padding: 0.2, duration: 300 }), 100); + } else { + setNodes([]); + setEdges([]); } - }, [schemaData, setNodes, setEdges]); + }, [filteredSchemaData, setNodes, setEdges, reactFlowInstance]); // Filter nodes based on search query const filteredNodes = useMemo(() => { @@ -180,13 +212,13 @@ const ERDiagramContent: React.FC = ({ nodeTypes }) => { // Re-layout with dagre const reLayout = useCallback(() => { - if (schemaData) { - const { nodes: newNodes, edges: newEdges } = transformSchemaToER(schemaData, true); + if (filteredSchemaData) { + const { nodes: newNodes, edges: newEdges } = transformSchemaToER(filteredSchemaData, true); setNodes(newNodes as typeof nodes); setEdges(newEdges); setTimeout(() => reactFlowInstance?.fitView({ padding: 0.2, duration: 500 }), 100); } - }, [schemaData, setNodes, setEdges, reactFlowInstance]); + }, [filteredSchemaData, setNodes, setEdges, reactFlowInstance]); // Edge hover handlers for tooltip const onEdgeMouseEnter: EdgeMouseHandler = useCallback((_event, edge) => { @@ -278,6 +310,36 @@ const ERDiagramContent: React.FC = ({ nodeTypes }) => { ER Diagram + {/* Schema filter dropdown */} + {availableSchemas.length > 0 && ( + + + + + + setSelectedSchema("__all__")}> + + All Schemas + + {schemaData?.schemas.reduce((acc, s) => acc + (s.tables?.length || 0), 0)} tables + + + {availableSchemas.map(schema => ( + setSelectedSchema(schema)}> + {schema} + + {schemaData?.schemas.find(s => s.name === schema)?.tables?.length || 0} tables + + + ))} + + + )} + {/* Search bar */}

@@ -417,6 +479,7 @@ const ERDiagramContent: React.FC = ({ nodeTypes }) => {
{nodes.length} Tables • {edges.length} Relations + {selectedSchema !== "__all__" && ` • Schema: ${selectedSchema}`} {selectedNodeId && ` • Selected: ${selectedNodeId.split('.')[1]}`} Click table to highlight • Drag to pan • Scroll to zoom From bf59263d548553ba13b81bafa723b8a0c3ce98d7 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 3 Feb 2026 18:54:04 +0530 Subject: [PATCH 4/4] feat: add schema filtering functionality to query builder --- .../query-builder/BuilderSidebar.tsx | 32 ++++++++++++++++++- .../query-builder/QueryBuilderPanel.tsx | 24 ++++++++++++-- src/components/query-builder/types.ts | 4 +++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/components/query-builder/BuilderSidebar.tsx b/src/components/query-builder/BuilderSidebar.tsx index 7c5e094..acd1f0e 100644 --- a/src/components/query-builder/BuilderSidebar.tsx +++ b/src/components/query-builder/BuilderSidebar.tsx @@ -8,6 +8,7 @@ import { PanelLeft, History, Columns3, + Layers, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -36,6 +37,9 @@ export function BuilderSidebar({ nodes, history, availableColumns, + availableSchemas, + selectedSchema, + onSchemaChange, filters, selectedColumns, sortBy, @@ -121,7 +125,33 @@ export function BuilderSidebar({ {tables.length} - + + {/* Schema Filter */} + {availableSchemas.length > 0 && ( +
+ +
+ )}
{tables.map((table) => { const isAdded = nodes.some( diff --git a/src/components/query-builder/QueryBuilderPanel.tsx b/src/components/query-builder/QueryBuilderPanel.tsx index 6237fa9..180542f 100644 --- a/src/components/query-builder/QueryBuilderPanel.tsx +++ b/src/components/query-builder/QueryBuilderPanel.tsx @@ -70,11 +70,26 @@ const QueryBuilderPanel = ({ dbId }: QueryBuilderPanelProps) => { const databaseName = dbDetails?.name || dbId; - // Get all tables from schema + // Schema filter state + const [selectedSchema, setSelectedSchema] = useState("__all__"); + + // Get available schema names + const availableSchemas = useMemo(() => { + if (!schemaData?.schemas) return []; + return schemaData.schemas + .filter(s => s.tables?.length > 0) + .map(s => s.name); + }, [schemaData]); + + // Get tables filtered by selected schema const allTables = useMemo(() => { if (!schemaData) return []; - return schemaData.schemas.flatMap((schema) => schema.tables); - }, [schemaData]); + if (selectedSchema === "__all__") { + return schemaData.schemas.flatMap((schema) => schema.tables); + } + const schema = schemaData.schemas.find(s => s.name === selectedSchema); + return schema?.tables || []; + }, [schemaData, selectedSchema]); // Get available columns from added nodes const availableColumns: ColumnOption[] = useMemo(() => { @@ -365,6 +380,9 @@ const QueryBuilderPanel = ({ dbId }: QueryBuilderPanelProps) => { nodes={nodes} history={history} availableColumns={availableColumns} + availableSchemas={availableSchemas} + selectedSchema={selectedSchema} + onSchemaChange={setSelectedSchema} filters={filters} selectedColumns={selectedColumns} sortBy={sortBy} diff --git a/src/components/query-builder/types.ts b/src/components/query-builder/types.ts index 83e740b..6518df0 100644 --- a/src/components/query-builder/types.ts +++ b/src/components/query-builder/types.ts @@ -52,6 +52,10 @@ export interface BuilderSidebarProps { nodes: Node[]; history: QueryHistoryItem[]; availableColumns: ColumnOption[]; + // Schema filtering + availableSchemas: string[]; + selectedSchema: string; + onSchemaChange: (schema: string) => void; filters: QueryFilter[]; selectedColumns: string[]; sortBy: string;