import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import ReactFlow, {
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
Panel,
useReactFlow,
ReactFlowProvider,
Handle,
Position,
BaseEdge,
EdgeLabelRenderer,
getBezierPath,
MarkerType
} from 'reactflow';
import 'reactflow/dist/style.css';
import './ERDiagramCanvas.scss';
import axios from 'axios';
// Import icons from react-icons
import {
FaDatabase,
FaTable,
FaTimes,
FaKey,
FaLink,
FaLayerGroup,
FaColumns,
FaProjectDiagram,
FaChevronDown,
FaChevronRight,
FaServer,
FaCloud,
FaHdd,
FaEdit
} from 'react-icons/fa';
import { CustomDatabaseIcon, CustomDocumentIcon, CustomDimensionIcon } from './CustomIcons';
import { Breadcrumbs, Link, Typography, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material';
import AddTableModal from './AddTableModal';
// import UpdateTableModal from './UpdateTableModal';
// Custom Table Node Component for Data Entity
const ERTableNode = ({ data, id }) => {
const getTableIcon = () => {
switch(data.table_type) {
case 'stage':
return ;
case 'fact':
return ;
case 'dimension':
return ;
default:
return ;
}
};
return (
{getTableIcon()}
{data.name}
{/*
*/}
{data.columns && data.columns.map((column, index) => {
// Determine the column's role in relationships
const isSourceKey = column.is_source_key;
const isForeignKey = column.is_foreign_key;
const isPrimaryKey = column.is_primary_key;
// Create CSS classes for highlighting - ONLY based on API relationship data
let columnClasses = "er-column-item";
if (isSourceKey) columnClasses += " er-source-key";
if (isForeignKey) columnClasses += " er-foreign-key-highlight";
// Debug logging for CSS classes - only for actual relationships
if (isSourceKey || isForeignKey) {
console.log(`🎨 Column ${column.name} classes: ${columnClasses}`);
console.log(`🎨 isSourceKey: ${isSourceKey}, isForeignKey: ${isForeignKey}`);
console.log(`Column data:`, column);
}
return (
-
{isPrimaryKey && }
{isForeignKey && }
{isSourceKey && !isPrimaryKey && }
{column.name}
{column.data_type}
{/* Show relationship info on hover */}
{column.relationship_info && column.relationship_info.length > 0 && (
{column.relationship_info.map((rel, relIndex) => (
{isSourceKey ? `→ ${rel.targetTable}.${rel.targetColumn}` : `← ${rel.sourceTable}.${rel.sourceColumn}`}
))}
)}
);
})}
);
};
// Custom Edge Component for Relationships
const ERRelationshipEdge = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
data,
selected
}) => {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
// Determine relationship type and styling
const relationshipType = data?.relationship_type || '1:N';
const isOneToMany = relationshipType === '1:N';
const isOneToOne = relationshipType === '1:1';
const isManyToMany = relationshipType === 'N:M';
// Calculate arrow position and angle
const dx = targetX - sourceX;
const dy = targetY - sourceY;
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
// Arrow position (closer to target)
const arrowDistance = 20;
const arrowX = targetX - (arrowDistance * Math.cos(Math.atan2(dy, dx)));
const arrowY = targetY - (arrowDistance * Math.sin(Math.atan2(dy, dx)));
// Start arrow position for many-to-many
const startArrowX = sourceX + (arrowDistance * Math.cos(Math.atan2(dy, dx)));
const startArrowY = sourceY + (arrowDistance * Math.sin(Math.atan2(dy, dx)));
return (
<>
{/* Main edge path */}
{/* Custom Arrow at target */}
{/* Custom Arrow at source for many-to-many */}
{isManyToMany && (
)}
{/* Relationship cardinality indicators - positioned directly on the line */}
{/* Source cardinality (near source table) - positioned on the line */}
{isOneToMany || isOneToOne ? '1' : 'N'}
{/* Target cardinality (near target table) - positioned on the line */}
{isOneToOne ? '1' : 'N'}
{/* Main relationship label - centered on the line */}
{relationshipType}
{/* Column mapping tooltip - shown on hover */}
{data?.source_column && data?.target_column && (
{data.source_column} → {data.target_column}
)}
>
);
};
// Database Wrapper Node Component
const ERDatabaseWrapperNode = ({ data, id }) => {
// Auto-calculate dimensions based on schema count and content
const schemaCount = data.schemaCount || 0;
const totalTables = data.totalTables || 0;
// Dynamic sizing for database wrapper - much larger to contain all schemas
const autoWidth = Math.max(1400, data.contentWidth + 200); // Base width + content + padding
const autoHeight = Math.max(1000, data.contentHeight + 200); // Base height + content + padding
return (
{data.name}
({schemaCount} schemas • {totalTables} tables)
);
};
// Schema Group Node Component with Auto-sizing
const ERSchemaGroupNode = ({ data, id }) => {
// Auto-calculate dimensions based on table count and content
const tableCount = data.tableCount || 0;
const tablesPerRow = Math.ceil(Math.sqrt(tableCount));
const rows = Math.ceil(tableCount / tablesPerRow);
// Dynamic sizing based on content
const autoWidth = Math.max(800, tablesPerRow * 320 + 160); // Base width + table width + padding
const autoHeight = Math.max(600, rows * 280 + 200); // Base height + table height + padding
return (
{data.name}
({tableCount} tables)
{tableCount === 0 && (
No tables in this schema
)}
);
};
// Node types for Data Entity
const nodeTypes = {
erTable: ERTableNode,
erSchemaGroup: ERSchemaGroupNode,
erDatabaseWrapper: ERDatabaseWrapperNode,
};
// Edge types for Data Entity
const edgeTypes = {
erRelationship: ERRelationshipEdge,
};
// Mock data for services and data sources
const mockServices = [
{
id: 'plan-plus',
name: 'Plan Plus',
icon: ,
dataSources: [
{ id: 'postgres-main', name: 'PostgreSQL Main', type: 'PostgreSQL', icon: },
{ id: 'mysql-analytics', name: 'MySQL Analytics', type: 'MySQL', icon: },
{ id: 'mongodb-logs', name: 'MongoDB Logs', type: 'MongoDB', icon: }
]
},
{
id: 'project-1',
name: 'Project 1',
icon: ,
dataSources: [
{ id: 'snowflake-dw', name: 'Snowflake DW', type: 'Snowflake', icon: },
{ id: 'redshift-analytics', name: 'Redshift Analytics', type: 'Redshift', icon: }
]
},
{
id: 'project-2',
name: 'Project 2',
icon: ,
dataSources: [
{ id: 'bigquery-main', name: 'BigQuery Main', type: 'BigQuery', icon: },
{ id: 'postgres-backup', name: 'PostgreSQL Backup', type: 'PostgreSQL', icon: }
]
}
];
// Hierarchical Breadcrumb Component
const HierarchicalBreadcrumb = ({
selectedService,
selectedDataSource,
onServiceChange,
onDataSourceChange
}) => {
const [serviceMenuAnchor, setServiceMenuAnchor] = useState(null);
const [dataSourceMenuAnchor, setDataSourceMenuAnchor] = useState(null);
const handleServiceClick = (event) => {
setServiceMenuAnchor(event.currentTarget);
};
const handleDataSourceClick = (event) => {
setDataSourceMenuAnchor(event.currentTarget);
};
const handleServiceSelect = (service) => {
onServiceChange(service);
setServiceMenuAnchor(null);
// Reset data source when service changes
if (service.dataSources.length > 0) {
onDataSourceChange(service.dataSources[0]);
}
};
const handleDataSourceSelect = (dataSource) => {
onDataSourceChange(dataSource);
setDataSourceMenuAnchor(null);
};
const handleCloseMenus = () => {
setServiceMenuAnchor(null);
setDataSourceMenuAnchor(null);
};
return (
<>
e.preventDefault()}
sx={{ fontWeight: 500 }}
>
Qubit
Data Entity
{/* DBTEZ Services Dropdown */}
DBTEZ Services
{/* Selected Service */}
{selectedService && (
{selectedService.icon}
{selectedService.name}
)}
{/* Services Menu */}
{/* Data Sources Menu */}
{selectedService && (
)}
>
);
};
// Main Data Entity Canvas Component
const ERDiagramCanvasContent = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [isLoading, setIsLoading] = useState(true);
const [databases, setDatabases] = useState([]);
const [selectedDatabase, setSelectedDatabase] = useState(null);
const [showAddMenu, setShowAddMenu] = useState(false);
// ReactFlow instance for programmatic control
const { fitView } = useReactFlow();
// Service and Data Source selection state
const [selectedService, setSelectedService] = useState(mockServices[0]); // Default to Plan Plus
const [selectedDataSource, setSelectedDataSource] = useState(mockServices[0].dataSources[0]); // Default to first data source
const [isUsingMockData, setIsUsingMockData] = useState(false);
const [apiError, setApiError] = useState(null);
const [dataSourceMenuAnchor, setDataSourceMenuAnchor] = useState(null);
// Add Table Modal state
const [isAddTableModalOpen, setIsAddTableModalOpen] = useState(false);
const [availableSchemas, setAvailableSchemas] = useState([]);
const [existingTables, setExistingTables] = useState([]);
// Update Table Modal state
const [isUpdateTableModalOpen, setIsUpdateTableModalOpen] = useState(false);
const [selectedTableForUpdate, setSelectedTableForUpdate] = useState(null);
// API Configuration
const API_BASE_URL = 'https://sandbox.kezel.io/api';
const token = "abdhsg";
const orgSlug = "sN05Pjv11qvH";
// API endpoints
const ENDPOINTS = {
DATABASE_LIST: `${API_BASE_URL}/qbt_database_list_get`,
SCHEMA_LIST: `${API_BASE_URL}/qbt_schema_list_get`,
TABLE_LIST: `${API_BASE_URL}/qbt_table_list_get`,
COLUMN_LIST: `${API_BASE_URL}/qbt_column_list_get`,
DATASOURCE_KEY_LIST: `${API_BASE_URL}/qbt_datasource_key_list_get`,
SCHEMA_CREATE: `${API_BASE_URL}/qbt_schema_create`,
SCHEMA_DELETE: `${API_BASE_URL}/qbt_schema_delete`,
SCHEMA_UPDATE: `${API_BASE_URL}/qbt_schema_update`,
TABLE_CREATE: `${API_BASE_URL}/qbt_table_create`,
TABLE_UPDATE: `${API_BASE_URL}/qbt_table_update`,
TABLE_DELETE: `${API_BASE_URL}/qbt_table_delete`,
COLUMN_CREATE: `${API_BASE_URL}/qbt_column_create`,
COLUMN_UPDATE: `${API_BASE_URL}/qbt_column_update`,
COLUMN_DELETE: `${API_BASE_URL}/qbt_column_delete`
};
// Mock database data representing ONE database with multiple schemas (fallback only)
const mockDatabaseData = {
id: 1,
name: "Sample Database",
slug: "sample_database",
description: "Sample Database Structure (Mock Data)",
service: selectedService?.name || "Unknown Service",
dataSource: selectedDataSource?.name || "Unknown DataSource",
schemas: [
{
sch: "FINANCE_MART",
tables: [
{
id: 1,
name: "accounts",
tbl: "accounts", // Add tbl field for consistency
table_type: "dimension",
columns: [
{ name: "account_id", col: "account_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
{ name: "account_number", col: "account_number", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false },
{ name: "account_name", col: "account_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false },
{ name: "account_type", col: "account_type", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false },
{ name: "balance", col: "balance", data_type: "DECIMAL(15,2)", is_primary_key: false, is_foreign_key: false },
{ name: "created_date", col: "created_date", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false }
]
},
{
id: 2,
name: "transactions",
tbl: "transactions", // Add tbl field for consistency
table_type: "fact",
columns: [
{ name: "transaction_id", col: "transaction_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
{ name: "account_id", col: "account_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true },
{ name: "transaction_date", col: "transaction_date", data_type: "DATE", is_primary_key: false, is_foreign_key: false },
{ name: "amount", col: "amount", data_type: "DECIMAL(12,2)", is_primary_key: false, is_foreign_key: false }
]
}
]
},
{
sch: "SALES_MART",
name: "Sales Mart",
tables: [
{
id: 3,
name: "customers",
tbl: "customers", // Add tbl field for consistency
table_type: "dimension",
columns: [
{ name: "customer_id", col: "customer_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
{ name: "customer_name", col: "customer_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false },
{ name: "email", col: "email", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }
]
}
]
}
],
relationships: [
{
id: "relationship-1",
sourceTable: "accounts",
sourceSchema: "FINANCE_MART",
sourceColumn: "account_id",
sourceKeyName: "pk_accounts",
targetTable: "transactions",
targetSchema: "FINANCE_MART",
targetColumn: "account_id",
targetKeyName: "fk_transactions_account",
relationship_type: "1:N"
}
]
};
// API Functions for fetching real data
const fetchDatabases = async () => {
try {
const response = await axios.post(
ENDPOINTS.DATABASE_LIST,
{
token: token,
org: orgSlug,
},
{
headers: {
'Content-Type': 'application/json'
}
}
);
console.log('Database list response:', response.data);
const databases = response.data.items || [];
console.log(`Found ${databases.length} databases:`, databases);
return databases.filter(db => db.con); // Filter out databases without connection slug
} catch (error) {
console.error('Error fetching databases:', error);
throw error;
}
};
const fetchSchemas = async (dbSlug) => {
try {
console.log(`Fetching schemas for database slug: ${dbSlug}`);
const response = await axios.post(
ENDPOINTS.SCHEMA_LIST,
{
token: token,
org: orgSlug,
con: dbSlug
},
{
headers: {
'Content-Type': 'application/json'
}
}
);
console.log(`Schema list for database ${dbSlug}:`, response.data);
let schemas = [];
if (Array.isArray(response.data.items)) {
schemas = response.data.items.map(item => ({
name: item.name,
sch: item.sch, // Use 'sch' as the slug
description: item.description || "",
created_at: item.created_at,
is_validated: item.is_validated,
database: dbSlug
}));
}
console.log(`Number of schemas found for database ${dbSlug}: ${schemas.length}`);
console.log('Schema details:', schemas.map(s => ({ name: s.name, sch: s.sch })));
return schemas;
} catch (error) {
console.error(`Error fetching schemas for database ${dbSlug}:`, error);
throw error;
}
};
const fetchTables = async (dbSlug, schemaSlug) => {
try {
console.log(`Fetching tables for database: ${dbSlug}, schema: ${schemaSlug}`);
const response = await axios.post(
ENDPOINTS.TABLE_LIST,
{
token: token,
org: orgSlug,
con: dbSlug,
sch: schemaSlug
},
{
headers: {
'Content-Type': 'application/json'
}
}
);
console.log(`Table list for schema ${schemaSlug}:`, response.data);
let tables = [];
if (Array.isArray(response.data.items)) {
tables = response.data.items.map(item => ({
id: item.id || `${dbSlug}-${schemaSlug}-${item.name}`,
name: item.name,
tbl: item.tbl, // Table slug
description: item.description || "",
table_type: item.table_type || "dimension", // Default to dimension if not specified
created_at: item.created_at,
database: dbSlug,
schema: schemaSlug
}));
}
console.log(`Number of tables found for schema ${schemaSlug}: ${tables.length}`);
return tables;
} catch (error) {
console.error(`Error fetching tables for schema ${schemaSlug}:`, error);
throw error;
}
};
const fetchColumns = async (dbSlug, schemaSlug, tableSlug) => {
try {
console.log(`Fetching columns for database: ${dbSlug}, schema: ${schemaSlug}, table: ${tableSlug}`);
const response = await axios.post(
ENDPOINTS.COLUMN_LIST,
{
token: token,
org: orgSlug,
con: dbSlug,
sch: schemaSlug,
tbl: tableSlug
},
{
headers: {
'Content-Type': 'application/json'
}
}
);
console.log(`Column list for table ${tableSlug}:`, response.data);
let columns = [];
if (Array.isArray(response.data.items)) {
columns = response.data.items.map((item, index) => ({
name: item.name,
col: item.col, // Column slug
data_type: item.data_type || "VARCHAR(255)",
description: item.description || "",
is_primary_key: index === 0, // Assume first column is primary key
is_foreign_key: item.name.toLowerCase().includes('_id') && index > 0, // Simple heuristic for foreign keys
is_nullable: item.is_nullable || false,
database: dbSlug,
schema: schemaSlug,
table: tableSlug
}));
}
console.log(`Number of columns found for table ${tableSlug}: ${columns.length}`);
return columns;
} catch (error) {
console.error(`Error fetching columns for table ${tableSlug}:`, error);
throw error;
}
};
// Function to fetch relationships/foreign keys from the API
const fetchRelationships = async (dbSlug) => {
try {
console.log(`Fetching relationships for database: ${dbSlug}`);
const response = await axios.post(
ENDPOINTS.DATASOURCE_KEY_LIST,
{
token: token,
org: orgSlug,
con: dbSlug
},
{
headers: {
'Content-Type': 'application/json'
}
}
);
console.log(`Relationships response for database ${dbSlug}:`, response.data);
let relationships = [];
if (response.data.status === 200 && Array.isArray(response.data.items)) {
relationships = response.data.items.map((item, index) => {
// Extract source and destination information
const source = item.source?.[0];
const destination = item.destination?.[0];
if (source && destination) {
return {
id: `relationship-${index}`,
sourceTable: source.table_slug,
sourceSchema: source.schema_slug,
sourceColumn: source.column_slug,
sourceKeyName: source.key_name,
targetTable: destination.table_slug,
targetSchema: destination.schema_slug,
targetColumn: destination.column_slug,
targetKeyName: destination.key_name,
relationship_type: '1:N' // Default relationship type, can be enhanced later
};
}
return null;
}).filter(Boolean); // Remove null entries
}
console.log(`Number of relationships found for database ${dbSlug}: ${relationships.length}`);
console.log('Parsed relationships:', relationships);
return relationships;
} catch (error) {
console.error(`Error fetching relationships for database ${dbSlug}:`, error);
// Return empty array on error to prevent breaking the diagram
return [];
}
};
// Function to fetch complete database structure with schemas, tables, and columns
const fetchCompleteDatabase = async (dbSlug) => {
try {
console.log(`Fetching complete structure for database: ${dbSlug}`);
// Fetch schemas
const schemas = await fetchSchemas(dbSlug);
// Fetch tables and columns for each schema
const schemasWithTables = await Promise.all(
schemas.map(async (schema) => {
try {
const tables = await fetchTables(dbSlug, schema.sch);
// Fetch columns for each table
const tablesWithColumns = await Promise.all(
tables.map(async (table) => {
try {
const columns = await fetchColumns(dbSlug, schema.sch, table.tbl);
return {
...table,
columns: columns
};
} catch (columnError) {
console.warn(`Error fetching columns for table ${table.name}:`, columnError);
return {
...table,
columns: [] // Return table with empty columns if column fetch fails
};
}
})
);
return {
...schema,
tables: tablesWithColumns
};
} catch (tableError) {
console.warn(`Error fetching tables for schema ${schema.sch}:`, tableError);
return {
...schema,
tables: [] // Return schema with empty tables if table fetch fails
};
}
})
);
console.log(`Complete database structure for ${dbSlug}:`, schemasWithTables);
return schemasWithTables;
} catch (error) {
console.error(`Error fetching complete database structure for ${dbSlug}:`, error);
throw error;
}
};
// Fetch databases and generate Data Entity using real API
useEffect(() => {
const fetchERData = async () => {
try {
setIsLoading(true);
console.log(`Fetching ER data for service: ${selectedService?.name}, data source: ${selectedDataSource?.name}`);
try {
// Fetch databases from API
const databases = await fetchDatabases();
console.log('Fetched databases:', databases);
if (databases && databases.length > 0) {
// Select database based on selectedDataSource or use first available
const targetDatabase = databases.find(db =>
selectedDataSource?.name === db.name ||
selectedDataSource?.name === db.con ||
selectedDataSource?.slug === db.con
) || databases[0]; // Fallback to first database if no match
console.log('Selected database for Data Entity:', targetDatabase);
console.log('Available databases:', databases.map(db => ({ name: db.name, con: db.con })));
// Fetch complete structure (schemas, tables, columns) for the selected database
const schemasWithTables = await fetchCompleteDatabase(targetDatabase.con);
// Fetch relationships for the selected database
const relationships = await fetchRelationships(targetDatabase.con);
// Create the complete database object using actual API data
const completeDatabase = {
id: targetDatabase.id,
name: targetDatabase.name,
slug: targetDatabase.con,
description: targetDatabase.description || `Database: ${targetDatabase.name}`,
service: selectedService?.name,
dataSource: selectedDataSource?.name,
schemas: schemasWithTables,
relationships: relationships
};
console.log('Complete database structure:', completeDatabase);
console.log('Schemas in database:', completeDatabase.schemas.map(s => ({
name: s.name,
sch: s.sch,
tableCount: s.tables?.length || 0,
tables: s.tables?.map(t => t.name) || []
})));
setDatabases([completeDatabase]); // Wrap in array for compatibility
setSelectedDatabase(completeDatabase); // Set selected database for modal
generateERDiagram(completeDatabase);
setIsUsingMockData(false);
setApiError(null);
console.log('Successfully fetched and generated Data Entity from API data');
} else {
throw new Error('No databases found from API');
}
} catch (apiError) {
// Handle specific error types
let errorMessage = 'Unknown API error';
if (apiError.code === 'ERR_NETWORK' || apiError.message.includes('CORS')) {
errorMessage = 'CORS error - API not accessible from browser';
console.warn('CORS error detected, using mock data:', apiError.message);
} else if (apiError.response?.status === 403) {
errorMessage = '403 Forbidden - Check API token and permissions';
console.warn('403 Forbidden error, using mock data. Check your API token and permissions.');
} else if (apiError.response?.status === 401) {
errorMessage = '401 Unauthorized - Invalid API token';
console.warn('401 Unauthorized error, using mock data. Check your API token.');
} else {
errorMessage = `API Error: ${apiError.message}`;
console.warn('API error, using mock data:', apiError.message);
}
setApiError(errorMessage);
setIsUsingMockData(true);
// Fallback to mock data
const singleDbData = {
...mockDatabaseData,
service: selectedService?.name,
dataSource: selectedDataSource?.name
};
setDatabases([singleDbData]); // Wrap in array for compatibility
setSelectedDatabase(singleDbData); // Set selected database for modal
generateERDiagram(singleDbData);
console.log('Using mock data for Data Entity due to API error');
}
} catch (error) {
console.error('Unexpected error in fetchERData:', error);
setApiError('Unexpected error occurred');
setIsUsingMockData(true);
// Fallback to mock data
const singleDbData = {
...mockDatabaseData,
service: selectedService?.name,
dataSource: selectedDataSource?.name
};
setDatabases([singleDbData]);
setSelectedDatabase(singleDbData); // Set selected database for modal
generateERDiagram(singleDbData);
} finally {
setIsLoading(false);
}
};
// Only fetch if we have selected service and data source
if (selectedService && selectedDataSource) {
fetchERData();
}
}, [selectedService, selectedDataSource]); // Re-fetch when service or data source changes
// Auto-fit view when nodes are updated
useEffect(() => {
if (nodes.length > 0 && !isLoading) {
const timer = setTimeout(() => {
fitView({
padding: 0.1,
includeHiddenNodes: false,
minZoom: 0.15,
maxZoom: 0.8,
duration: 600
});
}, 300);
return () => clearTimeout(timer);
}
}, [nodes, isLoading, fitView]);
// Regenerate diagram when selectedDatabase changes (including after adding new table)
useEffect(() => {
if (selectedDatabase && selectedDatabase.schemas) {
console.log('Selected database changed, regenerating diagram...');
generateERDiagram(selectedDatabase);
}
}, [selectedDatabase]);
// Auto-alignment layout calculator
const calculateOptimalLayout = (databaseData) => {
const layouts = [];
let totalWidth = 0;
let maxHeight = 0;
databaseData.forEach((db) => {
if (db.schemas && db.schemas.length > 0) {
const dbLayout = { schemas: [], totalWidth: 0, maxHeight: 0 };
db.schemas.forEach((schema) => {
if (schema.tables && schema.tables.length > 0) {
const tableCount = schema.tables.length;
const tablesPerRow = Math.min(4, Math.ceil(Math.sqrt(tableCount))); // Max 4 tables per row
const rows = Math.ceil(tableCount / tablesPerRow);
// Calculate optimal schema dimensions
const schemaWidth = Math.max(800, tablesPerRow * 320 + 160);
const schemaHeight = Math.max(600, rows * 280 + 200);
dbLayout.schemas.push({
schema,
width: schemaWidth,
height: schemaHeight,
tablesPerRow,
rows
});
dbLayout.totalWidth += schemaWidth + 100; // Add spacing
dbLayout.maxHeight = Math.max(dbLayout.maxHeight, schemaHeight);
}
});
layouts.push(dbLayout);
totalWidth = Math.max(totalWidth, dbLayout.totalWidth);
maxHeight += dbLayout.maxHeight + 100; // Add vertical spacing
}
});
return { layouts, totalWidth, maxHeight };
};
// Handle adding a new table
const handleAddTable = async (tableData) => {
try {
console.log('Adding new table:', tableData);
// Find the target schema
const targetSchema = availableSchemas.find(schema => schema.sch === tableData.schema);
if (!targetSchema) {
throw new Error('Target schema not found');
}
// Prepare API payload
const apiPayload = {
token: token,
org: orgSlug,
con: selectedDatabase?.slug || "my_dwh", // Use selected database slug
sch: tableData.schema,
name: tableData.name,
external_name: null,
table_type: tableData.table_type,
description: tableData.description,
columns: tableData.columns.map(col => ({
column_name: col.name,
data_type: col.data_type,
is_nullable: col.is_nullable
})),
keys: tableData.keys.map(key => ({
key_name: key.name,
key_type: key.key_type,
key_columns: [{
column_name: key.column_name,
sequence: key.sequence || 1
}]
}))
};
console.log('API Payload:', apiPayload);
// Make API call to create the table
const response = await axios.post(ENDPOINTS.TABLE_CREATE, apiPayload, {
headers: {
'Content-Type': 'application/json'
}
});
console.log('API Response:', response.data);
if (response.data.status === 200) {
// API call successful, create new table object for local state
const newTable = {
id: response.data.id || `new-table-${Date.now()}`,
name: tableData.name,
tbl: response.data.tbl || tableData.name.toLowerCase(), // Table slug from API or fallback
description: tableData.description,
table_type: tableData.table_type,
columns: tableData.columns,
schema: tableData.schema,
database: selectedDatabase?.slug || 'Unknown',
created_at: new Date().toISOString()
};
console.log('Table created successfully via API:', newTable);
// Update the current database structure immediately
const updatedDatabase = { ...selectedDatabase };
const schemaIndex = updatedDatabase.schemas.findIndex(s => s.sch === tableData.schema);
if (schemaIndex !== -1) {
if (!updatedDatabase.schemas[schemaIndex].tables) {
updatedDatabase.schemas[schemaIndex].tables = [];
}
updatedDatabase.schemas[schemaIndex].tables.push(newTable);
console.log(`Added table "${newTable.name}" to schema "${tableData.schema}"`);
console.log('Updated database structure:', updatedDatabase);
// Update the selected database state
setSelectedDatabase(updatedDatabase);
// Update databases list
setDatabases(prevDatabases => {
return prevDatabases.map(db => {
if (db.id === selectedDatabase?.id) {
return updatedDatabase;
}
return db;
});
});
// Regenerate the Data Entity with the new table immediately
console.log('Scheduling diagram regeneration...');
setTimeout(() => {
console.log('Regenerating diagram with updated database...');
generateERDiagram(updatedDatabase);
}, 100);
}
// Update existing tables list for the modal
setExistingTables(prev => [...prev, newTable]);
// Close the modal immediately after successful creation
setIsAddTableModalOpen(false);
console.log('Table added successfully and modal closed');
// Force a small delay to ensure all state updates are processed and then fit view
setTimeout(() => {
if (fitView) {
fitView({
padding: 0.1,
includeHiddenNodes: false,
minZoom: 0.15,
maxZoom: 0.8,
duration: 800
});
}
}, 500);
} else {
throw new Error(response.data.message || 'Failed to create table');
}
} catch (error) {
console.error('Error adding table:', error);
// Always close the modal on error to prevent it from being stuck
setIsAddTableModalOpen(false);
// Check if it's an API error
if (error.response) {
const errorMessage = error.response.data?.message || `API Error: ${error.response.status}`;
throw new Error(errorMessage);
} else if (error.request) {
throw new Error('Network error: Unable to connect to the API');
} else {
throw new Error(error.message || 'Unknown error occurred');
}
}
};
// Handle update table action
const handleUpdateTable = (tableData) => {
console.log('Opening update modal for table:', tableData);
setSelectedTableForUpdate(tableData);
setIsUpdateTableModalOpen(true);
};
// Handle table update submission
const handleUpdateTableSubmit = async (updatedTableData) => {
try {
console.log('Updating table with data:', updatedTableData);
// The API call is handled in the UpdateTableModal component
// After successful update, we need to refresh the diagram
// Close the modal
setIsUpdateTableModalOpen(false);
setSelectedTableForUpdate(null);
// Refresh the diagram by re-fetching data
// You can implement a more efficient update by modifying the existing nodes
// For now, let's trigger a refresh of the current database
if (selectedDatabase) {
generateERDiagram(selectedDatabase);
}
console.log('Table updated successfully');
} catch (error) {
console.error('Error updating table:', error);
// Error handling is done in the UpdateTableModal component
}
};
// Generate Data Entity with Database Wrapper structure
const generateERDiagram = (database) => {
console.log('🔄 Starting Data Entity generation...');
console.log('Database received:', database?.name);
console.log('Total schemas:', database?.schemas?.length);
console.log('Relationships received:', database?.relationships?.length);
console.log('Relationship details:', database?.relationships);
// Debug each relationship in detail
if (database?.relationships && database.relationships.length > 0) {
database.relationships.forEach((rel, index) => {
console.log(`🔗 Relationship ${index + 1}:`);
console.log(` Source: ${rel.sourceSchema}.${rel.sourceTable}.${rel.sourceColumn}`);
console.log(` Target: ${rel.targetSchema}.${rel.targetTable}.${rel.targetColumn}`);
console.log(` Type: ${rel.relationship_type}`);
});
}
const newNodes = [];
const newEdges = []; // Will be populated with relationship edges
// Process the selected database
console.log('Generating Data Entity for database:', database?.name);
console.log('Total schemas in database:', database?.schemas?.length);
console.log('Schema details:', database?.schemas?.map(s => ({
name: s.name,
sch: s.sch,
tableCount: s.tables?.length || 0,
tableNames: s.tables?.map(t => t.name) || []
})));
if (database && database.schemas && database.schemas.length > 0) {
// Calculate database wrapper dimensions
let totalSchemaWidth = 0;
let maxSchemaHeight = 0;
let totalTables = 0;
// Calculate layout for all schemas
const schemaLayouts = database.schemas.map(schema => {
const tableCount = (schema.tables && schema.tables.length) || 0;
totalTables += tableCount;
if (tableCount > 0) {
const tablesPerRow = Math.min(3, Math.ceil(Math.sqrt(tableCount))); // Max 3 tables per row for better fit
const rows = Math.ceil(tableCount / tablesPerRow);
// Calculate schema dimensions with proper padding for tables
const tableSpacingCalc = 330; // Horizontal spacing between tables
const tableRowSpacingCalc = 320; // Vertical spacing between table rows
// Calculate required width and height based on table layout
const tableStartX = 90; // Starting X position within schema
const tableStartY = 120; // Starting Y position within schema
const requiredWidth = tableStartX + (tablesPerRow * tableSpacingCalc) + 100; // Extra margin
const requiredHeight = tableStartY + (rows * tableRowSpacingCalc) + 100; // Extra margin
const schemaWidth = Math.max(1000, requiredWidth);
const schemaHeight = Math.max(850, requiredHeight);
console.log(`Schema "${schema.name || schema.sch}" layout: ${tableCount} tables, ${tablesPerRow} per row, ${rows} rows`);
console.log(`Required dimensions: ${requiredWidth}x${requiredHeight}, Final: ${schemaWidth}x${schemaHeight}`);
totalSchemaWidth += schemaWidth + 150; // Add spacing between schemas
maxSchemaHeight = Math.max(maxSchemaHeight, schemaHeight);
return {
schema,
width: schemaWidth,
height: schemaHeight,
tablesPerRow,
rows,
tableCount
};
} else {
// Handle schemas with no tables - show empty schema container
const schemaWidth = 900; // Minimum width for empty schema
const schemaHeight = 400; // Minimum height for empty schema
totalSchemaWidth += schemaWidth + 150; // Add spacing between schemas
maxSchemaHeight = Math.max(maxSchemaHeight, schemaHeight);
return {
schema,
width: schemaWidth,
height: schemaHeight,
tablesPerRow: 0,
rows: 0,
tableCount: 0
};
}
});
console.log('Schema layouts created:', schemaLayouts.length);
console.log('Schema layout details:', schemaLayouts.map(sl => ({
name: sl.schema.name || sl.schema.sch,
sch: sl.schema.sch,
tableCount: sl.tableCount,
width: sl.width,
height: sl.height
})));
// Create Database Wrapper Node
const databaseWrapperId = `database-${database.slug}`;
const databaseWrapperWidth = Math.max(2000, totalSchemaWidth + 300);
const databaseWrapperHeight = Math.max(1400, maxSchemaHeight + 400);
newNodes.push({
id: databaseWrapperId,
type: 'erDatabaseWrapper',
position: { x: 50, y: 50 },
data: {
name: database.name,
schemaCount: database.schemas.length,
totalTables: totalTables,
width: databaseWrapperWidth,
height: databaseWrapperHeight,
contentWidth: totalSchemaWidth,
contentHeight: maxSchemaHeight
},
draggable: true,
selectable: false,
style: {
width: databaseWrapperWidth,
height: databaseWrapperHeight,
zIndex: -2
}
});
// Create Schema Group Nodes within Database Wrapper
let schemaXOffset = 180; // Start position within database wrapper
const schemaYOffset = 180; // Y position within database wrapper
schemaLayouts.forEach((schemaLayout, schemaIndex) => {
const schemaGroupId = `schema-${database.slug}-${schemaLayout.schema.sch}`;
// Create schema group node
newNodes.push({
id: schemaGroupId,
type: 'erSchemaGroup',
position: { x: schemaXOffset, y: schemaYOffset },
data: {
name: schemaLayout.schema.name || schemaLayout.schema.sch, // Use schema name, fallback to sch
sch: schemaLayout.schema.sch, // Keep sch for identification
tableCount: schemaLayout.tableCount,
width: schemaLayout.width,
height: schemaLayout.height
},
draggable: true,
selectable: false,
parentNode: databaseWrapperId,
extent: 'parent',
style: {
width: schemaLayout.width,
height: schemaLayout.height,
zIndex: -1
}
});
// Create table nodes within schema (only if tables exist)
if (schemaLayout.schema.tables && schemaLayout.schema.tables.length > 0) {
const tableSpacing = 330; // Horizontal spacing between tables
const tableRowSpacing = 320; // Vertical spacing between table rows
const tableStartX = 90; // Starting X position within schema (accounting for schema padding)
const tableStartY = 120; // Starting Y position within schema (accounting for schema padding and label)
let currentRow = 0;
let currentCol = 0;
schemaLayout.schema.tables.forEach((table, tableIndex) => {
const tableId = `table-${table.id || `${database.slug}-${schemaLayout.schema.sch}-${table.name}`}`;
// Debug table structure
if (tableIndex === 0) {
console.log(`📋 Sample table structure:`, table);
console.log(`📋 Sample column structure:`, table.columns?.[0]);
}
// Calculate table position within schema (relative to schema, not absolute)
const tableX = tableStartX + (currentCol * tableSpacing);
const tableY = tableStartY + (currentRow * tableRowSpacing);
// Debug table positioning
if (tableIndex === 0) {
console.log(`Schema "${schemaLayout.schema.name || schemaLayout.schema.sch}" dimensions: ${schemaLayout.width}x${schemaLayout.height}`);
console.log(`Tables per row: ${schemaLayout.tablesPerRow}, Total rows: ${schemaLayout.rows}`);
console.log(`Table count: ${schemaLayout.tableCount}`);
}
// Ensure table position is within schema bounds
const maxTableX = schemaLayout.width - 280; // Table width + some margin
const maxTableY = schemaLayout.height - 300; // Table height + some margin
if (tableX > maxTableX || tableY > maxTableY) {
console.warn(`Table "${table.name}" position (${tableX}, ${tableY}) exceeds schema bounds (${schemaLayout.width}x${schemaLayout.height})`);
}
// Add primary key and foreign key indicators to columns based on relationships
const enhancedColumns = (table.columns || []).map((col, colIndex) => {
// Get table identifier - use table_slug from API or fallback to name
const tableSlug = table.tbl || table.table_slug || table.name;
// Get schema identifier
const schemaSlug = schemaLayout.schema.sch || schemaLayout.schema.schema_slug || schemaLayout.schema.name;
// Get column identifier - use column_slug from the column data
const columnSlug = col.col || col.column_slug || col.name;
// Check if this column is a foreign key (target/destination of a relationship)
const isForeignKey = database.relationships?.some(rel => {
const matchesTable = rel.targetTable === tableSlug;
const matchesSchema = rel.targetSchema === schemaSlug;
const matchesColumn = rel.targetColumn === columnSlug;
return matchesTable && matchesSchema && matchesColumn;
}) || false;
// Check if this column is a source key (source of a relationship)
const isSourceKey = database.relationships?.some(rel => {
const matchesTable = rel.sourceTable === tableSlug;
const matchesSchema = rel.sourceSchema === schemaSlug;
const matchesColumn = rel.sourceColumn === columnSlug;
return matchesTable && matchesSchema && matchesColumn;
}) || false;
// Debug logging for relationship matching
if (database.relationships && database.relationships.length > 0) {
// Only log for the first few columns to avoid spam
if (colIndex < 3) {
console.log(`🔍 Checking column: table=${tableSlug}, schema=${schemaSlug}, column=${columnSlug}`);
console.log(`🔍 Available relationships:`, database.relationships.map(r =>
`${r.sourceSchema}.${r.sourceTable}.${r.sourceColumn} -> ${r.targetSchema}.${r.targetTable}.${r.targetColumn}`
));
}
if (isForeignKey || isSourceKey) {
console.log(`🔗 MATCH FOUND! Column ${tableSlug}.${columnSlug} - isSourceKey: ${isSourceKey}, isForeignKey: ${isForeignKey}`);
// Find the matching relationship for more details
const matchingRel = database.relationships.find(rel =>
(rel.targetTable === tableSlug && rel.targetSchema === schemaSlug && rel.targetColumn === columnSlug) ||
(rel.sourceTable === tableSlug && rel.sourceSchema === schemaSlug && rel.sourceColumn === columnSlug)
);
console.log(`🔗 Matching relationship:`, matchingRel);
}
}
return {
...col,
is_primary_key: col.is_primary_key || colIndex === 0, // Use existing or first column as primary key
is_foreign_key: isForeignKey,
is_source_key: isSourceKey,
relationship_info: database.relationships?.filter(rel => {
const isTarget = rel.targetTable === tableSlug && rel.targetSchema === schemaSlug && rel.targetColumn === columnSlug;
const isSource = rel.sourceTable === tableSlug && rel.sourceSchema === schemaSlug && rel.sourceColumn === columnSlug;
return isTarget || isSource;
}) || []
};
});
newNodes.push({
id: tableId,
type: 'erTable',
position: { x: tableX, y: tableY },
data: {
...table,
columns: enhancedColumns,
schema: schemaLayout.schema.sch,
database: database.name
// onUpdateTable: handleUpdateTable
},
draggable: true,
parentNode: schemaGroupId,
extent: 'parent'
});
// Move to next position
currentCol++;
if (currentCol >= schemaLayout.tablesPerRow) {
currentCol = 0;
currentRow++;
}
});
} else {
// Schema has no tables - show empty state message
console.log(`Schema "${schemaLayout.schema.name || schemaLayout.schema.sch}" (${schemaLayout.schema.sch}) has no tables`);
}
schemaXOffset += schemaLayout.width + 150; // Move to next schema position
});
}
// Create relationship edges based on API data
if (database.relationships && database.relationships.length > 0) {
console.log('Creating relationship edges:', database.relationships.length);
database.relationships.forEach((relationship, index) => {
// Find source and target table nodes by matching table names and schemas
const sourceNode = newNodes.find(node =>
node.type === 'erTable' &&
node.data?.schema === relationship.sourceSchema &&
(node.data?.name === relationship.sourceTable || node.data?.tbl === relationship.sourceTable)
);
const targetNode = newNodes.find(node =>
node.type === 'erTable' &&
node.data?.schema === relationship.targetSchema &&
(node.data?.name === relationship.targetTable || node.data?.tbl === relationship.targetTable)
);
if (sourceNode && targetNode) {
const edgeId = `edge-${relationship.id || index}`;
console.log(`Creating edge: ${sourceNode.id} -> ${targetNode.id}`);
console.log(`Relationship: ${relationship.sourceTable}.${relationship.sourceColumn} -> ${relationship.targetTable}.${relationship.targetColumn}`);
newEdges.push({
id: edgeId,
source: sourceNode.id,
target: targetNode.id,
type: 'erRelationship',
data: {
relationship_type: relationship.relationship_type || '1:N',
source_column: relationship.sourceColumn,
target_column: relationship.targetColumn,
source_key_name: relationship.sourceKeyName,
target_key_name: relationship.targetKeyName
},
style: {
stroke: '#8a2be2',
strokeWidth: 2,
},
markerEnd: {
type: MarkerType.ArrowClosed,
color: '#8a2be2',
},
animated: false,
selectable: true
});
} else {
console.warn(`Could not find nodes for relationship: ${relationship.sourceTable} -> ${relationship.targetTable}`);
console.warn(`Source node found: ${!!sourceNode}, Target node found: ${!!targetNode}`);
console.warn(`Looking for source ID: ${sourceTableId}, target ID: ${targetTableId}`);
}
});
console.log(`Created ${newEdges.length} relationship edges out of ${database.relationships.length} relationships`);
} else {
console.log('No relationships found in database data');
}
console.log('📊 Setting nodes and edges...');
console.log('New nodes count:', newNodes.length);
console.log('New edges count:', newEdges.length);
setNodes(newNodes);
setEdges(newEdges);
console.log('✅ Data Entity generation completed');
// Update available schemas and existing tables for the modal
if (database && database.schemas) {
setAvailableSchemas(database.schemas);
// Collect all existing tables from all schemas
const allTables = [];
database.schemas.forEach(schema => {
if (schema.tables) {
schema.tables.forEach(table => {
allTables.push({
...table,
schema: schema.sch,
schemaName: schema.name || schema.sch
});
});
}
});
setExistingTables(allTables);
}
// Auto-fit view after a short delay to ensure nodes are rendered
setTimeout(() => {
if (newNodes.length > 0) {
fitView({
padding: 0.1,
includeHiddenNodes: false,
minZoom: 0.15,
maxZoom: 0.8,
duration: 800 // Smooth animation
});
}
}, 300);
};
// onConnect removed - no connections allowed between tables
const handleAddClick = () => {
setShowAddMenu(!showAddMenu);
};
const handleDataSourceSelect = (dataSource) => {
setSelectedDataSource(dataSource);
setDataSourceMenuAnchor(null);
};
const handleCloseMenus = () => {
setDataSourceMenuAnchor(null);
};
if (isLoading) {
return (
{/* Breadcrumb Header */}
{/* Loading Content */}
Loading Data Entity...
Fetching schema for {selectedService?.name} - {selectedDataSource?.name}
);
}
return (
{/* Breadcrumb Header */}
{/* ReactFlow Container */}
{
if (node.type === 'erTable') {
switch(node.data.table_type) {
case 'stage': return '#00a99d';
case 'fact': return '#fa8c16';
case 'dimension': return '#52c41a';
default: return '#8a2be2';
}
}
return '#666';
}}
/>
Entity Relationship Diagram
Tables: {nodes.filter(n => n.type === 'erTable').length} •
Relationships: {edges.length} •
Schemas: {nodes.filter(n => n.type === 'erSchemaGroup').length}
{isUsingMockData && (
Using mock data - {apiError}
)}
{/* Data Sources Menu */}
{selectedService && (
)}
{/* Add Table Button */}
{/* Add Table Modal */}
setIsAddTableModalOpen(false)}
onAddTable={handleAddTable}
schemas={availableSchemas}
existingTables={existingTables}
position="bottom-right"
/>
{/* Update Table Modal */}
{/* {
setIsUpdateTableModalOpen(false);
setSelectedTableForUpdate(null);
}}
onUpdateTable={handleUpdateTableSubmit}
tableData={selectedTableForUpdate}
schemas={availableSchemas}
existingTables={existingTables}
position="center"
/> */}
);
};
// Wrapper component with ReactFlowProvider
const ERDiagramCanvas = () => {
return (
);
};
export default ERDiagramCanvas;