import React, { useState, useEffect } from "react"; import { getSchemaInfo, getSchemaPreview } from "../api"; import DataTable from "./DataTable"; const getParticipationExplanation = (rel) => { const from = rel.from_table || ""; const to = rel.to_table || ""; const fromPart = rel.from_participation || "total"; const toPart = rel.to_participation || "partial"; const singular = (name) => { if (name.endsWith("ies")) return name.slice(0, -3) + "y"; if (name.endsWith("s")) return name.slice(0, -1); return name; }; const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : ""; const fromExp = fromPart === "total" ? `every ${singular(from)} must have a ${singular(to)}` : `not all ${from} have ${to}`; const toExp = toPart === "total" ? `every ${singular(to)} must have ${from}` : `not all ${to} have ${from}`; return { fromText: <>{cap(singular(from))} participation: {fromPart} ({fromExp}), toText: <>{cap(singular(to))} participation: {toPart} ({toExp}) }; }; export default function SchemaPreviewPanel({ dataset, onStartQuerying, addToast }) { const [schemaInfo, setSchemaInfo] = useState(null); const [previews, setPreviews] = useState([]); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState(""); const [hoveredRel, setHoveredRel] = useState(null); const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); useEffect(() => { const loadSchemaDetails = async () => { setLoading(true); try { console.log("[SchemaPreviewPanel] Fetching details for:", dataset.db_name); const [infoRes, previewRes] = await Promise.all([ getSchemaInfo(dataset.db_name), getSchemaPreview(dataset.db_name) ]); setSchemaInfo(infoRes); setPreviews(previewRes.previews || []); if (infoRes.tables && infoRes.tables.length > 0) { setActiveTab(infoRes.tables[0].name); } setLoading(false); } catch (err) { console.error("[SchemaPreviewPanel] Failed to load schema info:", err); addToast("Failed to load schema details: " + err.message, "error"); setLoading(false); } }; if (dataset) { loadSchemaDetails(); } }, [dataset]); if (loading) { return (
Loading relational schema and ER diagram...
); } if (!schemaInfo) { return (
Failed to load schema details.
); } // Math positions for ERD SVG const numTables = schemaInfo.tables.length; const positions = {}; let svgWidth = 700; let svgHeight = 380; // Custom gorgeous layout for the ecommerce sample database if (dataset.db_name === "ecommerce") { positions["customers"] = { x: 40, y: 30 }; positions["products"] = { x: 40, y: 230 }; positions["orders"] = { x: 260, y: 130 }; positions["order_items"] = { x: 480, y: 30 }; positions["reviews"] = { x: 480, y: 230 }; } else { // Generic column-wise grid positioning const colCount = numTables >= 6 ? 2 : 3; const numRows = Math.ceil(numTables / colCount); // Set explicit dimensions instead of generic 700x380 svgWidth = Math.max(700, 40 + colCount * 220); svgHeight = Math.max(380, 30 + numRows * 170); schemaInfo.tables.forEach((tbl, idx) => { const col = idx % colCount; const row = Math.floor(idx / colCount); positions[tbl.name] = { x: 40 + col * 220, y: 30 + row * 170 }; }); } const getLineCoords = (rel) => { const p1 = positions[rel.from_table]; const p2 = positions[rel.to_table]; if (!p1 || !p2) return { x1: 0, y1: 0, x2: 0, y2: 0, isSelfLoop: false }; const width = 170; const height = 110; let x1, y1, x2, y2; let isSelfLoop = false; if (rel.from_table === rel.to_table) { isSelfLoop = true; // Coordinates for self-loop drawing x1 = p1.x + width - 20; // Start near the top right y1 = p1.y; x2 = p1.x + width; // End near the middle right y2 = p1.y + 40; return { x1, y1, x2, y2, isSelfLoop, width, height, p1 }; } // Check relative alignment if (p1.x + width < p2.x) { // p1 is left of p2 x1 = p1.x + width; y1 = p1.y + height / 2; x2 = p2.x; y2 = p2.y + height / 2; } else if (p2.x + width < p1.x) { // p1 is right of p2 x1 = p1.x; y1 = p1.y + height / 2; x2 = p2.x + width; y2 = p2.y + height / 2; } else { // Vertically aligned x1 = p1.x + width / 2; x2 = p2.x + width / 2; if (p1.y < p2.y) { y1 = p1.y + height; y2 = p2.y; } else { y1 = p1.y; y2 = p2.y + height; } } return { x1, y1, x2, y2, isSelfLoop: false }; }; const activePreview = previews.find(p => p.table_name === activeTab); return (
{/* Header Info Banner */}

{schemaInfo.db_name === "ecommerce" ? "E-Commerce Database" : schemaInfo.db_name.replace("_", " ").toUpperCase()}

📂 {schemaInfo.total_tables} tables 🔗 {schemaInfo.relationships.length} relationships 📊 {schemaInfo.total_rows.toLocaleString()} total rows
{/* SVG ERD Section */}
Entity Relationship Diagram (ERD)
{hoveredRel && ( Relationship: {hoveredRel.from_table}.{hoveredRel.from_column}{hoveredRel.to_table}.{hoveredRel.to_column} )}
{/* Markers Definitions for Arrows */} {/* Relationship Lines */} {schemaInfo.relationships.map((rel, idx) => { const coords = getLineCoords(rel); if (coords.x1 === 0 && coords.y1 === 0 && coords.x2 === 0 && coords.y2 === 0) return null; const isHovered = hoveredRel && hoveredRel.from_table === rel.from_table && hoveredRel.from_column === rel.from_column && hoveredRel.to_table === rel.to_table && hoveredRel.to_column === rel.to_column; const dx = coords.x2 - coords.x1; const dy = coords.y2 - coords.y1; const len = Math.sqrt(dx * dx + dy * dy); const ux = len > 0 ? dx / len : 0; const uy = len > 0 ? dy / len : 0; const px = -uy; const py = ux; const xMid = (coords.x1 + coords.x2) / 2; const yMid = (coords.y1 + coords.y2) / 2; const cardinality = rel.cardinality || "1:N"; const fromPart = rel.from_participation || "total"; const toPart = rel.to_participation || "partial"; return ( setHoveredRel(rel)} onMouseMove={(e) => setMousePos({ x: e.clientX, y: e.clientY })} onMouseLeave={() => setHoveredRel(null)} style={{ cursor: "pointer" }} > {coords.isSelfLoop ? ( <> {/* Thick transparent interactive buffer path for easier hovering */} {/* Arrow at the end of the loop to indicate direction */} ) : ( <> {/* Thick transparent interactive buffer line for easier hovering */} {/* Segment 1: from_table end to midpoint */} {fromPart === "total" ? ( <> ) : ( )} {/* Segment 2: midpoint to to_table end */} {toPart === "total" ? ( <> ) : ( )} {/* Crow's Foot / Tick at from_table end (x1, y1) */} {cardinality === "1:N" || cardinality === "N:M" ? ( /* Crow's Foot */ <> ) : ( /* Single Tick */ )} {/* Crow's Foot / Tick at to_table end (x2, y2) */} {cardinality === "N:M" ? ( /* Crow's Foot */ <> ) : ( /* Single Tick */ )} )} {/* Midpoint label overlay */} {rel.relationship_name && ( {rel.relationship_name} )} ); })} {/* Table Boxes */} {schemaInfo.tables.map((tbl) => { const pos = positions[tbl.name] || { x: 50, y: 50 }; const width = 170; const height = 110; return ( {/* Outer rect */} {/* Header rect */} {/* Header divider */} {/* Header text */} {tbl.name} ({tbl.row_count}) {/* Columns list */} {tbl.columns.slice(0, 5).map((col, cIdx) => { const isPK = col.is_primary_key; const isFK = col.is_foreign_key; return ( {/* Key icon / label prefix */} {isPK ? "🔑" : (isFK ? "🔗" : "")} {/* Column Name */} {col.name.length > 18 ? col.name.slice(0, 16) + ".." : col.name} ); })} {/* Overflow indicator if table has > 5 columns */} {tbl.columns.length > 5 && ( + {tbl.columns.length - 5} more columns )} ); })}
{/* ERD Legend Box */}
ERD Legend
── single tick = One (1) ══ double line = Total participation (mandatory) ──< crow's foot = Many (N) ╌╌ dashed line = Partial participation (optional)
{/* Tabs and Data Preview Section */}

Table Data Previews

{/* Tab Selection Row */}
{schemaInfo.tables.map((tbl) => ( ))}
{/* Preview Data Grid */} {activePreview ? (
Showing first 5 rows of {activePreview.total_rows} total rows
) : (
No preview rows loaded.
)}
{/* Floating Tooltip */} {hoveredRel && (() => { const expl = getParticipationExplanation(hoveredRel); return (
{hoveredRel.from_table}{hoveredRel.to_table}
Cardinality: {hoveredRel.cardinality || "1:N"}
• {expl.fromText}
• {expl.toText}
); })()}
); }