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 */} {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 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 && ( {selectedService.dataSources.map((dataSource) => ( handleDataSourceSelect(dataSource)} selected={selectedDataSource?.id === dataSource.id} > {dataSource.icon} ))} )} {/* 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;