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}
)}
{/* 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}
);
})()}
);
}