diff --git a/frontend/src/pages/DatabaseSchema.tsx b/frontend/src/pages/DatabaseSchema.tsx index 2dc0ab9..3e6c326 100644 --- a/frontend/src/pages/DatabaseSchema.tsx +++ b/frontend/src/pages/DatabaseSchema.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect } from 'react'; +import { useState, useMemo, useEffect, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { ReactFlow, @@ -11,17 +11,30 @@ import { useEdgesState, MarkerType, Position, + Handle, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import Dagre from '@dagrejs/dagre'; -import { Database as DatabaseIcon, Loader2, Key, Link, RefreshCw } from 'lucide-react'; +import { Database as DatabaseIcon, Loader2, Key, Link, RefreshCw, Search, X } from 'lucide-react'; import { databasesApi, schemaApi, TableInfo, SchemaData } from '@/services/api'; import { Database } from '@/types'; // Custom node for table function TableNode({ data }: { data: TableInfo & Record }) { return ( -
+
+ {/* Connection handles */} + + + {/* Table header */}
@@ -187,6 +200,8 @@ function getLayoutedElements(schema: SchemaData): { nodes: Node[]; edges: Edge[] export default function DatabaseSchema() { const queryClient = useQueryClient(); const [selectedDbId, setSelectedDbId] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedTables, setSelectedTables] = useState>(new Set()); const { data: databases = [], isLoading: isLoadingDatabases } = useQuery({ queryKey: ['databases'], @@ -214,20 +229,87 @@ export default function DatabaseSchema() { const schema = schemaResponse?.data; + // Filter tables based on search + const filteredTableList = useMemo(() => { + if (!schema || !searchQuery.trim()) return []; + const query = searchQuery.toLowerCase(); + return schema.tables + .filter(t => t.name.toLowerCase().includes(query)) + .slice(0, 50); // Limit suggestions + }, [schema, searchQuery]); + + // Get related tables (tables connected via FK) + const getRelatedTables = useCallback((tableNames: Set, allTables: TableInfo[]): Set => { + const related = new Set(); + tableNames.forEach(name => related.add(name)); + + allTables.forEach(table => { + const tableKey = `${table.schema}.${table.name}`; + if (tableNames.has(table.name) || tableNames.has(tableKey)) { + // Add tables that this table references + table.foreign_keys.forEach(fk => { + related.add(fk.references_table); + }); + } + // Add tables that reference selected tables + table.foreign_keys.forEach(fk => { + if (tableNames.has(fk.references_table)) { + related.add(table.name); + } + }); + }); + + return related; + }, []); + + // Filter schema for selected tables + related tables + const filteredSchema = useMemo((): SchemaData | null => { + if (!schema) return null; + if (selectedTables.size === 0) return null; // Don't render anything if nothing selected + + const relatedTables = getRelatedTables(selectedTables, schema.tables); + const filteredTables = schema.tables.filter(t => + relatedTables.has(t.name) || relatedTables.has(`${t.schema}.${t.name}`) + ); + + return { + tables: filteredTables, + updated_at: schema.updated_at, + }; + }, [schema, selectedTables, getRelatedTables]); + const { nodes: layoutedNodes, edges: layoutedEdges } = useMemo(() => { - if (!schema) return { nodes: [], edges: [] }; - return getLayoutedElements(schema); - }, [schema]); + if (!filteredSchema || filteredSchema.tables.length === 0) return { nodes: [], edges: [] }; + return getLayoutedElements(filteredSchema); + }, [filteredSchema]); + + // Add table to selection + const addTable = (tableName: string) => { + setSelectedTables(prev => new Set([...prev, tableName])); + setSearchQuery(''); + }; + + // Remove table from selection + const removeTable = (tableName: string) => { + setSelectedTables(prev => { + const next = new Set(prev); + next.delete(tableName); + return next; + }); + }; + + // Clear all selected tables + const clearSelection = () => { + setSelectedTables(new Set()); + }; const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); // Update nodes when schema changes useEffect(() => { - if (layoutedNodes.length > 0) { - setNodes(layoutedNodes); - setEdges(layoutedEdges); - } + setNodes(layoutedNodes); + setEdges(layoutedEdges); }, [layoutedNodes, layoutedEdges, setNodes, setEdges]); if (isLoadingDatabases) { @@ -246,7 +328,11 @@ export default function DatabaseSchema() {
+ {selectedDbId && schema && ( +
+ + setSearchQuery(e.target.value)} + placeholder="Поиск таблиц..." + className="w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" + /> + {filteredTableList.length > 0 && ( +
+ {filteredTableList.map(table => ( + + ))} +
+ )} +
+ )} + {selectedDbId && (
+ {/* Selected tables chips */} + {selectedTables.size > 0 && ( +
+ Выбрано: + {[...selectedTables].map(tableName => ( + + {tableName} + + + ))} + + {filteredSchema && ( + + Показано: {filteredSchema.tables.length} таблиц (включая связанные) + + )} +
+ )} + {/* Schema visualization */}
{!selectedDbId && ( @@ -294,6 +443,16 @@ export default function DatabaseSchema() {
)} + {selectedDbId && !isLoadingSchema && schema && selectedTables.size === 0 && ( +
+
+ +

Начните вводить название таблицы в поиске

+

Выберите таблицы для отображения на схеме

+
+
+ )} + {selectedDbId && !isLoadingSchema && schema && nodes.length > 0 && ( )} + + {selectedDbId && !isLoadingSchema && schema && selectedTables.size > 0 && nodes.length === 0 && ( +
+ Таблицы не найдены +
+ )}
);