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 axios from 'axios'; // Import icons from react-icons import { FaDatabase, FaTable, FaPlus, FaTimes, FaKey, FaLink, FaLayerGroup, FaColumns, FaProjectDiagram, FaChevronDown, FaChevronRight, FaServer, FaCloud, FaHdd } 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'; // Custom styles for ER Diagram const generateERStyles = () => { return ` .react-flow__node { z-index: 1; margin: 20px; } .er-database-wrapper { z-index: -2; pointer-events: all; border-radius: 25px; cursor: grab; user-select: none; transition: all 0.2s ease; padding: 100px; border: 3px solid rgba(0, 169, 157, 0.4); background: rgba(0, 169, 157, 0.03); box-shadow: 0 0 30px rgba(0, 169, 157, 0.1); } .er-database-wrapper:hover { border-color: rgba(0, 169, 157, 0.6); background: rgba(0, 169, 157, 0.05); box-shadow: 0 0 40px rgba(0, 169, 157, 0.15); } .er-database-wrapper:active { cursor: grabbing; } .er-database-label { position: absolute; top: 20px; left: 20px; background: rgba(0, 169, 157, 0.95); color: white; padding: 12px 16px; border-radius: 8px; font-size: 14px; font-weight: bold; box-shadow: 0 4px 12px rgba(0, 169, 157, 0.3); display: flex; align-items: center; gap: 10px; z-index: 15; } .er-schema-group { z-index: -1; pointer-events: all; border-radius: 20px; cursor: grab; user-select: none; transition: all 0.2s ease; padding: 80px; border: 2px dashed rgba(138, 43, 226, 0.3); background: rgba(138, 43, 226, 0.05); position: relative; overflow: visible; } .er-schema-group:hover { border-color: rgba(138, 43, 226, 0.5); background: rgba(138, 43, 226, 0.08); box-shadow: 0 0 20px rgba(138, 43, 226, 0.1); } .er-schema-group:active { cursor: grabbing; } .er-schema-label { position: absolute; top: 15px; left: 15px; background: rgba(138, 43, 226, 0.9); color: white; padding: 8px 12px; border-radius: 6px; font-size: 12px; font-weight: bold; box-shadow: 0 2px 8px rgba(138, 43, 226, 0.3); display: flex; align-items: center; gap: 8px; z-index: 10; } .er-table-node { background: white; border: 2px solid #e1e5e9; border-radius: 10px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); width: 260px; min-height: 200px; transition: all 0.2s ease; } .er-table-node:hover { border-color: #8a2be2; box-shadow: 0 4px 16px rgba(138, 43, 226, 0.2); } .er-table-header { background: linear-gradient(135deg, #8a2be2, #9932cc); color: white; padding: 14px 16px; border-radius: 8px 8px 0 0; display: flex; align-items: center; gap: 10px; font-weight: bold; font-size: 15px; } .er-table-header.stage { background: linear-gradient(135deg, #00a99d, #52c41a); } .er-table-header.fact { background: linear-gradient(135deg, #fa8c16, #faad14); } .er-table-header.dimension { background: linear-gradient(135deg, #52c41a, #73d13d); } .er-column-list { padding: 0; margin: 0; list-style: none; max-height: 300px; overflow-y: auto; } .er-column-item { padding: 10px 16px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 10px; font-size: 13px; transition: background-color 0.2s ease; } .er-column-item:hover { background-color: #f8f9fa; } .er-column-item:last-child { border-bottom: none; } .er-column-name { flex: 1; font-weight: 500; color: #333; } .er-column-type { color: #666; font-size: 11px; background: #f5f5f5; padding: 2px 6px; border-radius: 3px; } .er-primary-key { color: #faad14; } .er-foreign-key { color: #8a2be2; } .er-relationship-edge { stroke: #8a2be2; stroke-width: 2; } .er-relationship-edge .react-flow__edge-path { stroke: #8a2be2; stroke-width: 2; } .er-relationship-edge-animated { stroke: #8a2be2; stroke-width: 2; stroke-dasharray: 5; animation: dashdraw 0.5s linear infinite; } @keyframes dashdraw { to { stroke-dashoffset: -10; } } /* Enhanced arrow markers */ .react-flow__edge.selected .react-flow__edge-path { stroke: #faad14 !important; stroke-width: 3 !important; } .react-flow__edge:hover .react-flow__edge-path { stroke: #52c41a !important; stroke-width: 3 !important; } /* Custom arrow styling */ .er-custom-arrow { pointer-events: none; z-index: 10; } /* Cardinality indicators */ .er-cardinality-indicator { background: rgba(255, 255, 255, 0.95) !important; border: 1px solid #8a2be2 !important; border-radius: 3px !important; padding: 2px 6px !important; font-size: 10px !important; font-weight: bold !important; color: #8a2be2 !important; box-shadow: 0 1px 3px rgba(0,0,0,0.2) !important; } /* Relationship direction indicators */ .er-direction-arrow { fill: #8a2be2; stroke: #8a2be2; stroke-width: 1; } .er-direction-arrow.selected { fill: #faad14; stroke: #faad14; } .er-relationship-label { background: rgba(138, 43, 226, 0.9); color: white; padding: 4px 8px; border-radius: 4px; font-size: 10px; font-weight: bold; } .er-relationship-label:hover + .relationship-tooltip { opacity: 1 !important; } .er-add-button { position: fixed; bottom: 30px; right: 30px; width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #8a2be2, #9932cc); border: none; color: white; font-size: 24px; cursor: pointer; box-shadow: 0 4px 16px rgba(138, 43, 226, 0.3); transition: all 0.2s ease; z-index: 1000; display: flex; align-items: center; justify-content: center; } .er-add-button:hover { transform: scale(1.1); box-shadow: 0 6px 20px rgba(138, 43, 226, 0.4); } .er-add-button:active { transform: scale(0.95); } .er-breadcrumb-dropdown { display: inline-flex; align-items: center; cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: background-color 0.2s ease; } .er-breadcrumb-dropdown:hover { background-color: rgba(0, 169, 157, 0.1); } .er-breadcrumb-dropdown .dropdown-arrow { margin-left: 4px; font-size: 12px; transition: transform 0.2s ease; } .er-breadcrumb-dropdown.open .dropdown-arrow { transform: rotate(180deg); } .er-service-menu { background: rgba(26, 26, 26, 0.95) !important; border: 1px solid rgba(0, 169, 157, 0.3) !important; border-radius: 8px !important; backdrop-filter: blur(10px) !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important; } .er-service-menu .MuiMenuItem-root { color: #ffffff !important; padding: 12px 16px !important; border-radius: 4px !important; margin: 4px 8px !important; transition: all 0.2s ease !important; } .er-service-menu .MuiMenuItem-root:hover { background-color: rgba(0, 169, 157, 0.1) !important; } .er-service-menu .MuiListItemIcon-root { color: #00a99d !important; min-width: 32px !important; } /* Ensure ReactFlow markers are visible */ .react-flow__edges { z-index: 1; } .react-flow__edge { pointer-events: all; } .react-flow__edge path { stroke: #8a2be2; stroke-width: 2; } .react-flow__edge.selected path { stroke: #faad14 !important; stroke-width: 3 !important; } /* Custom arrows are always visible */ .er-custom-arrow { pointer-events: none; z-index: 10; } `; }; // Custom Table Node Component for ER Diagram 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) => (
  • {column.is_primary_key && } {column.is_foreign_key && } {column.name} {column.data_type}
  • ))}
); }; // 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 ER Diagram const nodeTypes = { erTable: ERTableNode, erSchemaGroup: ERSchemaGroupNode, erDatabaseWrapper: ERDatabaseWrapperNode, }; // Edge types for ER Diagram 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 ER Diagram {/* DBTEZ Services Dropdown */}
DBTEZ Services
{/* Selected Service */} {selectedService && ( {selectedService.icon} {selectedService.name} )}
{/* Services Menu */} {mockServices.map((service) => ( handleServiceSelect(service)} selected={selectedService?.id === service.id} > {service.icon} ))} {/* Data Sources Menu */} {selectedService && ( {selectedService.dataSources.map((dataSource) => ( handleDataSourceSelect(dataSource)} selected={selectedDataSource?.id === dataSource.id} > {dataSource.icon} ))} )} ); }; // Main ER Diagram 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([]); // 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`, 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", // table_type: "dimension", // columns: [ // { name: "account_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, // { name: "account_number", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false }, // { name: "account_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, // { name: "account_type", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }, // { name: "balance", data_type: "DECIMAL(15,2)", is_primary_key: false, is_foreign_key: false }, // { name: "created_date", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false } // ] // }, // { // id: 2, // name: "transactions", // table_type: "fact", // columns: [ // { name: "transaction_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, // { name: "account_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true }, // { name: "transaction_date", data_type: "DATE", is_primary_key: false, is_foreign_key: false }, // { name: "amount", data_type: "DECIMAL(12,2)", is_primary_key: false, is_foreign_key: false }, // { name: "transaction_type", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false }, // { name: "description", data_type: "VARCHAR(255)", is_primary_key: false, is_foreign_key: false } // ] // }, // { // id: 3, // name: "customers", // table_type: "dimension", // columns: [ // { name: "customer_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, // { name: "first_name", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }, // { name: "last_name", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }, // { name: "email", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, // { name: "phone", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false }, // { name: "registration_date", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false } // ] // }, // { // id: 4, // name: "financial_reports", // table_type: "fact", // columns: [ // { name: "report_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, // { name: "account_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true }, // { name: "report_date", data_type: "DATE", is_primary_key: false, is_foreign_key: false }, // { name: "balance_amount", data_type: "DECIMAL(15,2)", is_primary_key: false, is_foreign_key: false }, // { name: "profit_loss", data_type: "DECIMAL(12,2)", is_primary_key: false, is_foreign_key: false } // ] // } // ] // }, // { // sch: "Schema100", // tables: [ // { // id: 5, // name: "products", // table_type: "dimension", // columns: [ // { name: "product_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, // { name: "product_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, // { name: "category", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }, // { name: "price", data_type: "DECIMAL(10,2)", is_primary_key: false, is_foreign_key: false }, // { name: "stock_quantity", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false } // ] // }, // { // id: 6, // name: "orders", // table_type: "fact", // columns: [ // { name: "order_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, // { name: "customer_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true }, // { name: "product_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true }, // { name: "order_date", data_type: "DATE", is_primary_key: false, is_foreign_key: false }, // { name: "quantity", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false }, // { name: "total_amount", data_type: "DECIMAL(12,2)", is_primary_key: false, is_foreign_key: false } // ] // }, // { // id: 7, // name: "inventory", // table_type: "stage", // columns: [ // { name: "inventory_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, // { name: "product_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true }, // { name: "warehouse_location", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, // { name: "stock_level", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false }, // { name: "last_updated", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false } // ] // }, // { // id: 8, // name: "suppliers", // table_type: "dimension", // columns: [ // { name: "supplier_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, // { name: "supplier_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, // { name: "contact_email", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, // { name: "phone_number", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false } // ] // } // ] // }, // { // sch: "New_sch", // tables: [ // { // id: 9, // name: "analytics_summary", // table_type: "fact", // columns: [ // { name: "summary_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, // { name: "date", data_type: "DATE", is_primary_key: false, is_foreign_key: false }, // { name: "total_revenue", data_type: "DECIMAL(15,2)", is_primary_key: false, is_foreign_key: false }, // { name: "total_orders", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false }, // { name: "unique_customers", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false } // ] // }, // { // id: 10, // name: "user_sessions", // table_type: "stage", // columns: [ // { name: "session_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, // { name: "customer_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true }, // { name: "session_start", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false }, // { name: "session_end", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false }, // { name: "pages_viewed", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false }, // { name: "device_type", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false } // ] // }, // { // id: 11, // name: "reports", // table_type: "dimension", // columns: [ // { name: "report_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, // { name: "report_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, // { name: "report_type", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }, // { name: "created_by", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }, // { name: "created_date", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false } // ] // } // ] // } // ] // }; // 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 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; } }; // Generate dummy ER relationship data based on single database structure const generateDummyERData = (database) => { const erData = []; if (database.schemas && database.schemas.length > 0) { database.schemas.forEach(schema => { if (schema.tables && schema.tables.length > 0) { // Create relationships between tables in the same schema for (let i = 0; i < schema.tables.length - 1; i++) { const sourceTable = schema.tables[i]; const targetTable = schema.tables[i + 1]; // Create a dummy relationship with varied types const relationshipTypes = ['1:N', '1:1', 'N:M']; const randomType = relationshipTypes[Math.floor(Math.random() * relationshipTypes.length)]; erData.push({ source_column_set: [ { table_id: sourceTable.id, column_name: sourceTable.columns?.[0]?.name || 'id', table_name: sourceTable.name, schema_name: schema.sch } ], destination_column_set: [ { table_id: targetTable.id, column_name: `${sourceTable.name}_id`, table_name: targetTable.name, schema_name: schema.sch } ], relationship_type: randomType }); } } }); } return erData; }; // Fetch databases and generate ER diagram 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 ER diagram:', 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); // 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 }; 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 ER diagram 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 ER diagram 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]); // 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); // Here you would typically make an API call to create the table // For now, we'll add it to the current diagram // Find the target schema const targetSchema = availableSchemas.find(schema => schema.sch === tableData.schema); if (!targetSchema) { throw new Error('Target schema not found'); } // Create a new table object const newTable = { id: `new-table-${Date.now()}`, name: tableData.name, description: tableData.description, table_type: tableData.table_type, columns: tableData.columns, schema: tableData.schema, database: selectedDatabase?.name || 'Unknown' }; // Add the table to existing tables list setExistingTables(prev => [...prev, newTable]); // Update the current database structure and regenerate the diagram 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); // Regenerate the ER diagram with the new table generateERDiagram(updatedDatabase); console.log('Table added successfully:', newTable); } } catch (error) { console.error('Error adding table:', error); throw error; } }; // Generate ER diagram with Database Wrapper structure const generateERDiagram = (database) => { const newNodes = []; const newEdges = []; // Generate dummy relationships const relationships = generateDummyERData(database); // Process the selected database console.log('Generating ER diagram 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 }))); 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 tableWidth = 260; // Width of each table node const tableHeight = 280; // Height of each table node (including spacing) const tableSpacingCalc = 330; // Horizontal spacing between tables const tableRowSpacingCalc = 320; // Vertical spacing between table rows const schemaPadding = 200; // Padding around the schema content // 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(900, requiredWidth); const schemaHeight = Math.max(650, 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}`}`; // 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 const enhancedColumns = (table.columns || []).map((col, colIndex) => ({ ...col, is_primary_key: colIndex === 0, // First column as primary key is_foreign_key: relationships.some(rel => rel.destination_column_set.some(dest => dest.column_name === col.name && dest.table_name === table.name ) ) })); newNodes.push({ id: tableId, type: 'erTable', position: { x: tableX, y: tableY }, data: { ...table, columns: enhancedColumns, schema: schemaLayout.schema.sch, database: database.name }, 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 edges for relationships relationships.forEach((rel, index) => { const sourceTableId = `table-${rel.source_column_set[0]?.table_id || `${rel.source_column_set[0]?.schema_name}-${rel.source_column_set[0]?.table_name}`}`; const targetTableId = `table-${rel.destination_column_set[0]?.table_id || `${rel.destination_column_set[0]?.schema_name}-${rel.destination_column_set[0]?.table_name}`}`; // Check if both nodes exist const sourceExists = newNodes.some(node => node.id === sourceTableId); const targetExists = newNodes.some(node => node.id === targetTableId); if (sourceExists && targetExists) { newEdges.push({ id: `relationship-${index}`, type: 'erRelationship', source: sourceTableId, target: targetTableId, data: { relationship_type: rel.relationship_type, source_column: rel.source_column_set[0]?.column_name, target_column: rel.destination_column_set[0]?.column_name }, style: { stroke: '#8a2be2', strokeWidth: 2 }, animated: false }); } }); setNodes(newNodes); setEdges(newEdges); // 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); }; const onConnect = useCallback( (params) => setEdges((eds) => addEdge({ ...params, type: 'erRelationship', data: { relationship_type: '1:N', source_column: 'id', target_column: 'foreign_key_id' }, style: { stroke: '#8a2be2', strokeWidth: 2 }, animated: true }, eds)), [setEdges] ); const handleAddClick = () => { setShowAddMenu(!showAddMenu); }; const handleDataSourceSelect = (dataSource) => { setSelectedDataSource(dataSource); setDataSourceMenuAnchor(null); }; const handleCloseMenus = () => { setDataSourceMenuAnchor(null); }; if (isLoading) { return (
{/* Breadcrumb Header */}
{/* Loading Content */}

Loading ER Diagram...

Fetching schema for {selectedService?.name} - {selectedDataSource?.name}

); } return (