import { useState, useMemo, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { ReactFlow, Node, Edge, Background, Controls, MiniMap, useNodesState, useEdgesState, MarkerType, Position, } 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 { databasesApi, schemaApi, TableInfo, SchemaData } from '@/services/api'; import { Database } from '@/types'; // Custom node for table function TableNode({ data }: { data: TableInfo & Record }) { return (
{/* Table header */}
{data.name}
{data.comment && (
{data.comment}
)}
{/* Columns */}
{data.columns.map((col) => (
{col.is_primary && } {data.foreign_keys.some(fk => fk.column === col.name) && ( )}
{col.name} {col.type} {!col.nullable && NN}
))} {data.columns.length === 0 && (
No columns
)}
{/* FK summary */} {data.foreign_keys.length > 0 && (
{data.foreign_keys.length} FK
)}
); } const nodeTypes = { table: TableNode, }; // Calculate node height based on columns count function getNodeHeight(table: TableInfo): number { const headerHeight = 44; const columnHeight = 28; const fkBarHeight = table.foreign_keys.length > 0 ? 28 : 0; const maxVisibleColumns = 10; const visibleColumns = Math.min(table.columns.length, maxVisibleColumns); return headerHeight + (visibleColumns * columnHeight) + fkBarHeight + 8; } function getLayoutedElements(schema: SchemaData): { nodes: Node[]; edges: Edge[] } { const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); g.setGraph({ rankdir: 'LR', // Left to Right nodesep: 80, // Horizontal separation between nodes ranksep: 120, // Vertical separation between ranks marginx: 50, marginy: 50, }); // Add nodes schema.tables.forEach((table) => { const nodeId = `${table.schema}.${table.name}`; const width = 280; const height = getNodeHeight(table); g.setNode(nodeId, { width, height }); }); // Add edges schema.tables.forEach((table) => { const sourceId = `${table.schema}.${table.name}`; table.foreign_keys.forEach((fk) => { const targetTable = schema.tables.find(t => t.name === fk.references_table); if (targetTable) { const targetId = `${targetTable.schema}.${targetTable.name}`; g.setEdge(sourceId, targetId); } }); }); // Run layout Dagre.layout(g); // Create nodes with positions const nodes: Node[] = schema.tables.map((table) => { const nodeId = `${table.schema}.${table.name}`; const nodeWithPosition = g.node(nodeId); return { id: nodeId, type: 'table', position: { x: nodeWithPosition.x - nodeWithPosition.width / 2, y: nodeWithPosition.y - nodeWithPosition.height / 2, }, data: { ...table } as TableInfo & Record, sourcePosition: Position.Right, targetPosition: Position.Left, }; }); // Create edges const edges: Edge[] = []; schema.tables.forEach((table) => { const sourceId = `${table.schema}.${table.name}`; table.foreign_keys.forEach((fk) => { const targetTable = schema.tables.find(t => t.name === fk.references_table); if (targetTable) { const targetId = `${targetTable.schema}.${targetTable.name}`; edges.push({ id: `${fk.constraint_name}`, source: sourceId, target: targetId, type: 'smoothstep', animated: true, style: { stroke: '#3b82f6', strokeWidth: 2 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#3b82f6', }, label: `${fk.column} → ${fk.references_column}`, labelStyle: { fontSize: 10, fill: '#666' }, labelBgStyle: { fill: 'white', fillOpacity: 0.9 }, labelBgPadding: [4, 2] as [number, number], }); } }); }); return { nodes, edges }; } export default function DatabaseSchema() { const queryClient = useQueryClient(); const [selectedDbId, setSelectedDbId] = useState(''); const { data: databases = [], isLoading: isLoadingDatabases } = useQuery({ queryKey: ['databases'], queryFn: async () => { const { data } = await databasesApi.getAll(); return data.filter((db: Database) => db.type !== 'aql'); }, }); const { data: schemaResponse, isLoading: isLoadingSchema } = useQuery({ queryKey: ['schema', selectedDbId], queryFn: async () => { const { data } = await schemaApi.getSchema(selectedDbId); return data; }, enabled: !!selectedDbId, }); const refreshMutation = useMutation({ mutationFn: () => schemaApi.refreshSchema(selectedDbId), onSuccess: (response) => { queryClient.setQueryData(['schema', selectedDbId], response.data); }, }); const schema = schemaResponse?.data; const { nodes: layoutedNodes, edges: layoutedEdges } = useMemo(() => { if (!schema) return { nodes: [], edges: [] }; return getLayoutedElements(schema); }, [schema]); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); // Update nodes when schema changes useEffect(() => { if (layoutedNodes.length > 0) { setNodes(layoutedNodes); setEdges(layoutedEdges); } }, [layoutedNodes, layoutedEdges, setNodes, setEdges]); if (isLoadingDatabases) { return (
); } return (
{/* Toolbar */}
{selectedDbId && ( )} {schema && (
{schema.tables.length} таблиц | Обновлено: {new Date(schema.updated_at).toLocaleString()}
)}
{/* Schema visualization */}
{!selectedDbId && (
Выберите базу данных для просмотра схемы
)} {selectedDbId && isLoadingSchema && (
)} {selectedDbId && !isLoadingSchema && schema && nodes.length > 0 && ( )} {selectedDbId && !isLoadingSchema && schema && schema.tables.length === 0 && (
В базе данных нет таблиц
)}
); }