import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import ReactFlow, {
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
Panel,
useReactFlow,
ReactFlowProvider,
Handle,
Position,
BaseEdge,
EdgeLabelRenderer,
getSmoothStepPath,
getBezierPath
} from 'reactflow';
import 'reactflow/dist/style.css';
import axios from 'axios';
import planPlusLogo from '../assets/img/planPlusLogo.png';
// Import icons from react-icons
import { FaDatabase, FaTable, FaFlask, FaArrowRight, FaPlus, FaTimes, FaChevronUp, FaChevronDown } from 'react-icons/fa';
import { BiSolidData } from 'react-icons/bi';
import { AiFillFolder } from 'react-icons/ai';
import { BsFileEarmarkSpreadsheet } from 'react-icons/bs';
// Import mock data from DataflowCanvas
import mockApiData from './mockData';
// Modal component for creating entities
const Modal = ({ isOpen, title, onClose, children }) => {
if (!isOpen) return null;
return (
);
};
// Database Creation Form with nested schema and table creation
const DatabaseForm = ({ onSave, onCancel }) => {
const [formData, setFormData] = useState({
name: '',
description: '',
url: '',
key: '',
type: 'PostgreSQL',
schemas: [] // Array to hold schemas
});
const [showSchemaForm, setShowSchemaForm] = useState(false);
const [currentSchemaIndex, setCurrentSchemaIndex] = useState(null);
const [showTableForm, setShowTableForm] = useState(false);
const [currentSchemaForTable, setCurrentSchemaForTable] = useState(null);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
onSave(formData);
};
const fetchId = () => {
// In a real app, this would make an API call
alert('Fetching ID from URL: ' + formData.url);
};
// Add a new schema to the database
const addSchema = (schemaData) => {
if (currentSchemaIndex !== null) {
// Edit existing schema
const updatedSchemas = [...formData.schemas];
updatedSchemas[currentSchemaIndex] = {
...updatedSchemas[currentSchemaIndex],
...schemaData,
};
setFormData(prev => ({ ...prev, schemas: updatedSchemas }));
} else {
// Add new schema
const newSchema = {
id: `schema-${Date.now()}`,
name: schemaData.name,
description: schemaData.description,
tables: [] // Initialize with empty tables array
};
setFormData(prev => ({
...prev,
schemas: [...prev.schemas, newSchema]
}));
}
setShowSchemaForm(false);
setCurrentSchemaIndex(null);
};
// Add a new table to a schema
const addTable = (tableData) => {
const schemaIndex = formData.schemas.findIndex(s => s.id === currentSchemaForTable);
if (schemaIndex !== -1) {
const newTable = {
id: `table-${Date.now()}`,
name: tableData.name,
description: tableData.description,
type: tableData.type
};
const updatedSchemas = [...formData.schemas];
updatedSchemas[schemaIndex] = {
...updatedSchemas[schemaIndex],
tables: [...updatedSchemas[schemaIndex].tables, newTable]
};
setFormData(prev => ({ ...prev, schemas: updatedSchemas }));
}
setShowTableForm(false);
setCurrentSchemaForTable(null);
};
// Remove a schema
const removeSchema = (index) => {
const updatedSchemas = [...formData.schemas];
updatedSchemas.splice(index, 1);
setFormData(prev => ({ ...prev, schemas: updatedSchemas }));
};
// Remove a table
const removeTable = (schemaIndex, tableIndex) => {
const updatedSchemas = [...formData.schemas];
updatedSchemas[schemaIndex].tables.splice(tableIndex, 1);
setFormData(prev => ({ ...prev, schemas: updatedSchemas }));
};
return (
{showSchemaForm ? (
{currentSchemaIndex !== null ? 'Edit Schema' : 'Add Schema'}
{
setShowSchemaForm(false);
setCurrentSchemaIndex(null);
}}
style={{
background: 'none',
border: 'none',
fontSize: '18px',
cursor: 'pointer',
color: '#666'
}}
>
×
{
setShowSchemaForm(false);
setCurrentSchemaIndex(null);
}}
initialData={currentSchemaIndex !== null ? formData.schemas[currentSchemaIndex] : null}
/>
) : showTableForm ? (
Add Table
{
setShowTableForm(false);
setCurrentSchemaForTable(null);
}}
style={{
background: 'none',
border: 'none',
fontSize: '18px',
cursor: 'pointer',
color: '#666'
}}
>
×
{
setShowTableForm(false);
setCurrentSchemaForTable(null);
}}
/>
) : (
)}
);
};
// Schema Creation Form
const SchemaForm = ({ onSave, onCancel, parentDatabase, initialData }) => {
const [formData, setFormData] = useState({
name: initialData ? initialData.name : '',
description: initialData ? initialData.description : ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
onSave(parentDatabase ? { ...formData, parentDatabase } : formData);
};
return (
Schema Name
Description
Cancel
{initialData ? 'Update Schema' : 'Add Schema'}
);
};
// Entity Selector Component
const EntitySelector = ({
activeEntityType,
setActiveEntityType,
databases,
schemas,
selectedDatabaseForSchema,
setSelectedDatabaseForSchema,
selectedSchemaForTable,
setSelectedSchemaForTable,
onSaveDatabase,
onSaveSchema,
onSaveTable,
onCancel
}) => {
// Determine which databases are available for schema creation
const availableDatabases = databases || [];
// Determine which schemas are available for table creation
const availableSchemas = schemas ? schemas.filter(schema =>
selectedDatabaseForSchema ? schema.dbId === selectedDatabaseForSchema : true
) : [];
return (
{/* Entity Type Selector - Only Database */}
{
setActiveEntityType('database');
setSelectedDatabaseForSchema(null);
setSelectedSchemaForTable(null);
}}
style={{
flex: 1,
padding: '12px',
border: 'none',
background: '#00a99d',
color: 'white',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '5px',
transition: 'all 0.2s'
}}
>
{/* Database */}
{/* No parent selectors needed since we only have database */}
{/* Only Database Form */}
{/* Guidance message when no parents are available */}
{activeEntityType === 'schema' && availableDatabases.length === 0 && (
You need to create a database first before creating a schema.
)}
{activeEntityType === 'table' && availableSchemas.length === 0 && (
You need to create a schema first before creating a table.
)}
);
};
// Table Creation Form
const TableForm = ({ onSave, onCancel, parentSchema, initialData }) => {
const [formData, setFormData] = useState({
name: initialData ? initialData.name : '',
description: initialData ? initialData.description : '',
type: initialData ? initialData.type : 'dimension',
columns: initialData && initialData.columns ? initialData.columns : []
});
const [showColumnInput, setShowColumnInput] = useState(false);
const [columnName, setColumnName] = useState('');
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
onSave(parentSchema ? { ...formData, parentSchema } : formData);
};
const addColumn = () => {
if (columnName.trim()) {
setFormData(prev => ({
...prev,
columns: [...prev.columns, columnName.trim()]
}));
setColumnName('');
setShowColumnInput(false);
}
};
const removeColumn = (index) => {
const updatedColumns = [...formData.columns];
updatedColumns.splice(index, 1);
setFormData(prev => ({ ...prev, columns: updatedColumns }));
};
return (
Table Name
Table Description
Table Type
Dimension
Fact
Stage
{/* Columns Section */}
Columns
setShowColumnInput(true)}
style={{
padding: '5px 10px',
backgroundColor: '#1890ff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px',
display: 'flex',
alignItems: 'center',
gap: '5px'
}}
>
Add Column
{showColumnInput && (
setColumnName(e.target.value)}
placeholder="Column name"
style={{
flex: 1,
padding: '8px',
borderRadius: '4px',
border: '1px solid #ddd',
fontSize: '14px'
}}
/>
Add
{
setShowColumnInput(false);
setColumnName('');
}}
style={{
padding: '8px 15px',
backgroundColor: '#f5f5f5',
color: '#333',
border: '1px solid #ddd',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Cancel
)}
{formData.columns.length > 0 ? (
{formData.columns.map((column, index) => (
{column}
removeColumn(index)}
style={{
background: 'none',
border: 'none',
color: '#ff4d4f',
cursor: 'pointer',
fontSize: '14px'
}}
>
×
))}
) : (
No columns added yet. Click "Add Column" to create one.
)}
Cancel
{initialData ? 'Update Table' : 'Add Table'}
);
};
// Custom edge with animated arrow
const CustomEdge = ({ id, source, target, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, data, markerEnd }) => {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>
{data?.label || 'connects to'}
>
);
};
// Create a unique ID for edges that should be animated
const SERVICE_EDGE_CLASS = 'service-db-connection';
// Add the CSS for the animated connections directly to the stylesheet
// This avoids potential issues with React's rendering cycle
const style = document.createElement('style');
style.textContent = `
@keyframes dashdraw {
from {
stroke-dashoffset: 10;
}
to {
stroke-dashoffset: 0;
}
}
.${SERVICE_EDGE_CLASS} .react-flow__edge-path {
animation: dashdraw 0.5s linear infinite;
}
`;
// Add the style element only once
if (!document.getElementById('react-flow-animation')) {
style.id = 'react-flow-animation';
document.head.appendChild(style);
}
// Hierarchical Edge for connecting service to databases
const HierarchicalEdge = ({ id, source, target, sourceX, sourceY, targetX, targetY, style = {} }) => {
// Calculate the path points for a stepped edge with orthogonal lines
const [path] = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: Position.Bottom, // Always start from bottom
targetX,
targetY,
targetPosition: Position.Top, // Always connect to top
borderRadius: 20, // Smoother corners for longer connections
stepSize: 30, // Increased step size for longer connections
});
// Enhanced style for hierarchical connections with animated dotted line
const hierarchyStyle = {
strokeWidth: 2,
stroke: '#00a99d',
strokeDasharray: '5, 5', // Create dotted/dashed line effect
...style,
};
return (
);
};
// Custom node types
const ServiceNode = ({ data = {} }) => {
// Safety check for undefined data
if (!data) {
console.error('ServiceNode received undefined data');
data = {}; // Provide a default empty object
}
return (
{/* Logo and text container */}
Plan+
{/* Connected databases count */}
Connected Databases: {data.databases || 0}
data.onToggle && data.onToggle(data.id)}
>
{data.expanded ? (
<>
Collapse Connections
>
) : (
<>
Expand Connections
>
)}
{/* Handles for connections - explicit id for targeting */}
);
};
const DatabaseNode = ({ data = {} }) => {
// Safety check for undefined data
if (!data) {
console.error('DatabaseNode received undefined data');
data = {}; // Provide a default empty object
}
// Enhanced styles for handles used in hierarchy connections
// Debug: Log the data being received by the component
// console.log('DatabaseNode data:', data);
// Assign different colors to different databases
const colorMap = {
'my_dwh': '#00a99d', // First database - teal
'my_dwh2': '#1890ff', // Second database - blue
'default': '#9c27b0' // Default - purple
};
// Get the database slug from the id (format: db-{slug})
// Add a safety check for undefined data.id
// Use data.id if available, otherwise fall back to the node's id
const nodeId = (data && data.id) ? data.id : 'default';
const dbSlug = (nodeId && typeof nodeId === 'string' && nodeId.startsWith && nodeId.startsWith('db-'))
? nodeId.substring(3)
: 'default';
// Use the color from the map or default to a fallback color
const borderColor = colorMap[dbSlug] || '#ff9800';
const handleColor = borderColor;
const bgColor = '#1a1a1a';
// Create a safe gradient ID by removing any special characters
const safeDbSlug = (dbSlug && typeof dbSlug === 'string')
? dbSlug.replace(/[^a-zA-Z0-9]/g, '_')
: 'default';
const gradientId = `db_paint0_linear_${safeDbSlug}`;
// Database SVG icon
const DatabaseIcon = () => (
);
return (
{/* Connection handles */}
{/* Display the database name from the API response */}
{data.name || data.label || 'Unknown Database'}
{/*
{`${data.schemas} schemas • ${data.tables} tables`}
Connection: {dbSlug}
*/}
{/* View Details Button */}
{
// Stop propagation to prevent the node click handler from being triggered
event.stopPropagation();
// Call the viewDetails function passed in data with safety checks
if (data && data.onViewDetails && data.id) {
data.onViewDetails(data.id, data.name || data.label || 'Unknown Database');
} else {
console.warn('Cannot view details: missing required data properties');
}
}}
style={{
padding: '4px 8px',
backgroundColor: borderColor,
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontSize: '11px',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '5px',
transition: 'all 0.2s'
}}
>
View Data Mapping
);
};
const SchemaNode = ({ data }) => {
// Use teal color for schemas under Dbtez
const isDbtezSchema = data.dbId === 'db4';
const isSalesSchema = data.label === 'Sales';
// Use dark theme colors
const bgColor = '#1a1a1a';
const borderColor = isSalesSchema ? '#fa8c16' : (isDbtezSchema ? '#00a99d' : '#52c41a');
const handleColor = isSalesSchema ? '#fa8c16' : (isDbtezSchema ? '#00a99d' : '#52c41a');
// Schema SVG icon
const SchemaIcon = () => (
);
return (
{/* Connection handles */}
{data.label}
{isDbtezSchema ? 'Schema' : `${data.tables} tables`}
);
};
const TableNode = ({ data }) => {
const tableType = data.type || (data.isFact ? 'fact' : 'dimension');
// Determine colors based on table type
let borderColor, background, labelBg, labelColor;
switch(tableType) {
case 'fact':
borderColor = '#fa8c16'; // Orange for fact tables
background = '#1a1a1a';
labelBg = '#fa8c16';
labelColor = 'white';
break;
case 'stage':
borderColor = '#1890ff'; // Blue for stage tables
background = '#1a1a1a';
labelBg = '#1890ff';
labelColor = 'white';
break;
case 'dimension':
default:
borderColor = '#52c41a'; // Green for dimension tables
background = '#1a1a1a';
labelBg = '#52c41a';
labelColor = 'white';
}
// Table SVG icon
const TableIcon = () => (
);
return (
{/* Connection handles */}
{data.label}
{tableType.toUpperCase()}
{data.columns && data.columns.length > 0 ?
`${data.columns.length} columns` :
tableType === 'fact' ? 'Fact Table' :
tableType === 'stage' ? 'Stage Table' :
'Dimension Table'}
);
};
// Generate data for InfiniteCanvas using mockApiData from DataflowCanvas
const generateMockData = () => {
// Get unique database slugs from schemas
const uniqueDatabases = [...new Set(mockApiData.schemas.map(schema => schema.database))];
// Create databases from unique database slugs
const databases = uniqueDatabases.map((dbSlug, index) => {
// Find all schemas for this database
const dbSchemas = mockApiData.schemas.filter(schema => schema.database === dbSlug);
// Find all tables for this database
const dbTables = mockApiData.tables.filter(table => table.database === dbSlug);
// Get the database name from the first schema (if available)
const dbName = dbSchemas.length > 0 && dbSchemas[0].databaseName
? dbSchemas[0].databaseName
: `Database ${dbSlug}`;
return {
id: `db-${dbSlug}`,
name: dbName, // This will be displayed in the UI
slug: dbSlug,
schemas: dbSchemas.length,
tables: dbTables.length
};
});
// If no databases were found, add a default one
if (databases.length === 0) {
databases.push({
id: 'db-default',
name: 'Default Database',
slug: 'default',
schemas: mockApiData.schemas.length,
tables: mockApiData.tables.length
});
}
// Create schemas from mockApiData
const schemas = mockApiData.schemas.map((schema, index) => {
// Find the database for this schema
const dbSlug = schema.database || 'default';
const db = databases.find(db => db.slug === dbSlug) || databases[0];
return {
id: `schema-${schema.slug}`,
dbId: db.id,
name: schema.name,
slug: schema.slug,
tables: mockApiData.tables.filter(table => table.schema === schema.slug).length
};
});
// Create tables from mockApiData
const tables = mockApiData.tables.map((table, index) => {
// Find the schema this table belongs to
const schemaSlug = table.schema;
const schemaObj = schemas.find(s => s.slug === schemaSlug);
if (!schemaObj) {
console.warn(`Schema not found for table ${table.name} (${table.slug})`);
return null;
}
return {
id: `table-${table.slug}`,
schemaId: schemaObj.id,
name: table.name,
slug: table.slug,
isFact: table.type === 'fact',
type: table.type,
columns: table.columns,
database: table.database
};
}).filter(Boolean); // Remove any null entries
return { databases, schemas, tables };
};
const InfiniteCanvas = () => {
const reactFlowWrapper = useRef(null);
const [reactFlowInstance, setReactFlowInstance] = useState(null);
const { fitView, setViewport, getViewport } = useReactFlow();
// State to track which elements are expanded
const [expandedDatabases, setExpandedDatabases] = useState({});
const [expandedSchemas, setExpandedSchemas] = useState({});
const [selectedTable, setSelectedTable] = useState(null);
// State for infinite canvas
const [scale, setScale] = useState(1);
const [position, setPosition] = useState({ x: 0, y: 0 });
// State for connection mode
const [isConnectionMode, setIsConnectionMode] = useState(false);
const [connectionSource, setConnectionSource] = useState(null);
const [connectionType, setConnectionType] = useState('default'); // default, reference, dependency
// State for entity creation modals
const [showAddEntityModal, setShowAddEntityModal] = useState(false);
const [activeEntityType, setActiveEntityType] = useState('database'); // 'database', 'schema', or 'table'
const [selectedDatabaseForSchema, setSelectedDatabaseForSchema] = useState(null);
const [selectedSchemaForTable, setSelectedSchemaForTable] = useState(null);
// Initialize with mock data
const mockData = generateMockData();
// State for storing created entities
const [databases, setDatabases] = useState(() => {
// Try to load databases from localStorage
try {
const savedDatabases = localStorage.getItem('databases');
if (savedDatabases) {
return JSON.parse(savedDatabases);
}
} catch (error) {
console.error('Error loading databases from localStorage:', error);
}
// If no saved databases, use the databases from our API
return mockData.databases.map(db => ({
...db,
description: db.description || `Database ${db.name}`,
type: db.type || 'PostgreSQL'
}));
});
// Track if we've already fetched the database data
const [hasFetchedDatabases, setHasFetchedDatabases] = useState(false);
// Fetch real database data from API - only once
useEffect(() => {
// Skip if we've already fetched the data
if (hasFetchedDatabases) {
console.log('Skipping database fetch - already loaded');
return;
}
const fetchDatabasesFromAPI = async () => {
try {
// API configuration
const API_BASE_URL = 'https://sandbox.kezel.io/api';
const token = "abdhsg"; // Replace with your actual token
const orgSlug = "sN05Pjv11qvH"; // Replace with your actual org slug
console.log('Fetching databases from API...');
const response = await axios.post(
`${API_BASE_URL}/qbt_database_list_get`,
{
token: token,
org: orgSlug,
},
{
headers: {
'Content-Type': 'application/json'
}
}
);
console.log('API Response:', response.data);
if (response.data && response.data.items && Array.isArray(response.data.items)) {
// Map API response to our database format
const apiDatabases = response.data.items.map((db, index) => ({
id: `db-${db.con || index}`,
name: db.name, // Use the name from API
slug: db.con,
description: db.description || `Database ${db.name}`,
type: db.database_type || 'PostgreSQL',
schemas: 0, // We'll update these later if needed
tables: 0
}));
console.log('Mapped databases from API:', apiDatabases);
// Update state with API data
setDatabases(apiDatabases);
// Mark that we've fetched the data
setHasFetchedDatabases(true);
}
} catch (error) {
console.error('Error fetching databases from API:', error);
}
};
// Call the function to fetch databases
fetchDatabasesFromAPI();
}, [hasFetchedDatabases]); // Only re-run if hasFetchedDatabases changes
const [schemas, setSchemas] = useState(() => {
// Try to load schemas from localStorage
try {
const savedSchemas = localStorage.getItem('schemas');
if (savedSchemas) {
return JSON.parse(savedSchemas);
}
} catch (error) {
console.error('Error loading schemas from localStorage:', error);
}
return [];
});
const [tables, setTables] = useState(() => {
// Try to load tables from localStorage
try {
const savedTables = localStorage.getItem('tables');
if (savedTables) {
return JSON.parse(savedTables);
}
} catch (error) {
console.error('Error loading tables from localStorage:', error);
}
return [];
});
// Save databases, schemas, and tables to localStorage whenever they change
useEffect(() => {
localStorage.setItem('databases', JSON.stringify(databases));
}, [databases]);
useEffect(() => {
localStorage.setItem('schemas', JSON.stringify(schemas));
}, [schemas]);
useEffect(() => {
localStorage.setItem('tables', JSON.stringify(tables));
}, [tables]);
// Handler for creating a new database with nested schemas and tables
const handleCreateDatabase = (formData) => {
// Create the database
const dbId = `db-${Date.now()}`;
const newDatabase = {
id: dbId,
name: formData.name,
description: formData.description,
url: formData.url,
key: formData.key,
type: formData.type,
schemas: formData.schemas.length,
tables: formData.schemas.reduce((count, schema) => count + schema.tables.length, 0)
};
setDatabases(prev => [...prev, newDatabase]);
// Add the new database node to the canvas
const dbNode = {
id: dbId,
type: 'database',
data: {
label: newDatabase.name,
name: newDatabase.name, // Include the name explicitly
schemas: newDatabase.schemas,
tables: newDatabase.tables,
expanded: false,
onToggle: toggleDatabaseExpansion,
onViewDetails: handleViewDataFlow // Add the function to handle redirection
},
position: {
x: Math.random() * 300,
y: Math.random() * 300
},
};
const newNodes = [dbNode];
const newEdges = [];
// Process schemas
const createdSchemas = formData.schemas.map((schemaData, schemaIndex) => {
const schemaId = `schema-${Date.now()}-${schemaIndex}`;
const newSchema = {
id: schemaId,
dbId: dbId,
name: schemaData.name,
description: schemaData.description,
slug: schemaData.name.toLowerCase().replace(/\s+/g, '_'),
tables: schemaData.tables.length
};
// Create schema node (positioned relative to database)
const schemaNode = {
id: schemaId,
type: 'schema',
data: {
label: schemaData.name,
tables: schemaData.tables.length,
dbId: dbId,
expanded: false,
onToggle: toggleSchemaExpansion
},
position: {
x: dbNode.position.x + 300,
y: dbNode.position.y + (schemaIndex * 150)
},
};
newNodes.push(schemaNode);
// Create edge from database to schema
newEdges.push({
id: `e-${dbId}-${schemaId}`,
source: dbId,
target: schemaId,
type: 'custom',
animated: true,
style: { stroke: '#00a99d', strokeWidth: 2 },
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: '#00a99d',
},
data: {
label: 'contains'
}
});
// Process tables for this schema
const schemaTables = schemaData.tables.map((tableData, tableIndex) => {
const tableId = `table-${Date.now()}-${schemaIndex}-${tableIndex}`;
const newTable = {
id: tableId,
schemaId: schemaId,
name: tableData.name,
description: tableData.description,
slug: tableData.name.toLowerCase().replace(/\s+/g, '_'),
type: tableData.type,
isFact: tableData.type === 'fact',
columns: tableData.columns || []
};
// Create table node (positioned relative to schema)
const tableNode = {
id: tableId,
type: 'table',
data: {
label: tableData.name,
type: tableData.type,
isFact: tableData.type === 'fact',
schemaId: schemaId,
columns: tableData.columns || []
},
position: {
x: schemaNode.position.x + 300,
y: schemaNode.position.y + (tableIndex * 100)
},
};
newNodes.push(tableNode);
// Create edge from schema to table
newEdges.push({
id: `e-${schemaId}-${tableId}`,
source: schemaId,
target: tableId,
type: 'custom',
animated: true,
style: {
stroke: tableData.type === 'fact' ? '#fa8c16' :
tableData.type === 'stage' ? '#1890ff' : '#52c41a',
strokeWidth: 2
},
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: tableData.type === 'fact' ? '#fa8c16' :
tableData.type === 'stage' ? '#1890ff' : '#52c41a',
},
data: {
label: 'contains'
}
});
return newTable;
});
// Add tables to state
setTables(prev => [...prev, ...schemaTables]);
return newSchema;
});
// Add schemas to state
setSchemas(prev => [...prev, ...createdSchemas]);
// Add all nodes and edges to the canvas
setNodes(nodes => [...nodes, ...newNodes]);
setEdges(edges => [...edges, ...newEdges]);
// Auto-expand the database if it has schemas
if (formData.schemas.length > 0) {
setTimeout(() => {
toggleDatabaseExpansion(dbId);
// Auto-expand schemas if they have tables
formData.schemas.forEach((schema, index) => {
if (schema.tables.length > 0) {
const schemaId = `schema-${Date.now()}-${index}`;
toggleSchemaExpansion(schemaId);
}
});
}, 500);
}
setShowAddEntityModal(false);
// Fit view to show all the new nodes
setTimeout(() => {
fitView();
}, 1000);
};
// Handler for creating a new schema
const handleCreateSchema = (formData) => {
const parentDb = formData.parentDatabase;
const newSchema = {
id: `schema-${Date.now()}`,
dbId: parentDb.id,
name: formData.name,
description: formData.description,
slug: formData.name.toLowerCase().replace(/\s+/g, '_'),
tables: 0
};
setSchemas(prev => [...prev, newSchema]);
// Update the parent database's schema count
setDatabases(prev => prev.map(db =>
db.id === parentDb.id
? { ...db, schemas: db.schemas + 1 }
: db
));
// If the parent database is expanded, add the schema node to the canvas
if (expandedDatabases[parentDb.id]) {
const dbNode = nodes.find(n => n.id === parentDb.id);
const newNode = {
id: newSchema.id,
type: 'schema',
data: {
label: newSchema.name,
tables: newSchema.tables,
dbId: parentDb.id,
expanded: false,
onToggle: toggleSchemaExpansion
},
position: {
x: dbNode.position.x + 150,
y: dbNode.position.y + 150 + (schemas.filter(s => s.dbId === parentDb.id).length * 100)
},
};
setNodes(nodes => [...nodes, newNode]);
// Add an edge from the database to the schema
const newEdge = {
id: `e-${parentDb.id}-${newSchema.id}`,
source: parentDb.id,
target: newSchema.id,
type: 'custom',
animated: true,
style: { stroke: '#00a99d', strokeWidth: 2 },
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: '#00a99d',
},
data: {
label: 'contains'
}
};
setEdges(edges => [...edges, newEdge]);
}
setShowAddEntityModal(false);
setSelectedDatabaseForSchema(null);
// Fit view to show the new node
setTimeout(() => {
fitView();
}, 10);
};
// Handler for creating a new table
const handleCreateTable = (formData) => {
const parentSchema = formData.parentSchema;
const newTable = {
id: `table-${Date.now()}`,
schemaId: parentSchema.id,
name: formData.name,
description: formData.description,
slug: formData.name.toLowerCase().replace(/\s+/g, '_'),
type: formData.type,
isFact: formData.type === 'fact',
columns: []
};
setTables(prev => [...prev, newTable]);
// Update the parent schema's table count
setSchemas(prev => prev.map(schema =>
schema.id === parentSchema.id
? { ...schema, tables: schema.tables + 1 }
: schema
));
// Update the parent database's table count
const parentDb = databases.find(db => db.id === parentSchema.dbId);
if (parentDb) {
setDatabases(prev => prev.map(db =>
db.id === parentDb.id
? { ...db, tables: db.tables + 1 }
: db
));
}
// If the parent schema is expanded, add the table node to the canvas
if (expandedSchemas[parentSchema.id]) {
const schemaNode = nodes.find(n => n.id === parentSchema.id);
const newNode = {
id: newTable.id,
type: 'table',
data: {
label: newTable.name,
type: newTable.type,
isFact: newTable.isFact,
schemaId: parentSchema.id,
columns: newTable.columns
},
position: {
x: schemaNode.position.x + 150,
y: schemaNode.position.y + 150 + (tables.filter(t => t.schemaId === parentSchema.id).length * 100)
},
};
setNodes(nodes => [...nodes, newNode]);
// Add an edge from the schema to the table
const newEdge = {
id: `e-${parentSchema.id}-${newTable.id}`,
source: parentSchema.id,
target: newTable.id,
type: 'custom',
animated: true,
style: {
stroke: newTable.isFact ? '#fa8c16' : '#52c41a',
strokeWidth: 2
},
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: newTable.isFact ? '#fa8c16' : '#52c41a',
},
data: {
label: 'contains'
}
};
setEdges(edges => [...edges, newEdge]);
}
setShowAddEntityModal(false);
setSelectedSchemaForTable(null);
// Fit view to show the new node
setTimeout(() => {
fitView();
}, 10);
};
// Define node types (memoized to prevent unnecessary re-renders)
const nodeTypes = useMemo(() => ({
service: ServiceNode,
database: DatabaseNode,
schema: SchemaNode,
table: TableNode,
}), []);
// Define edge types
const edgeTypes = useMemo(() => ({
custom: CustomEdge,
hierarchical: HierarchicalEdge,
}), []);
// Function to handle redirection to DataFlow view
const handleViewDataFlow = (dbId, dbName) => {
// Safety check for undefined parameters
if (!dbId || !dbName) {
console.error('Invalid database ID or name provided to handleViewDataFlow');
return;
}
// Find the database object to get the slug
const database = databases.find(db => db.id === dbId);
if (!database) {
console.error(`Database with ID ${dbId} not found`);
return;
}
// Get the database slug, with a fallback
const dbSlug = database.slug || (dbId.startsWith('db-') ? dbId.substring(3) : 'default');
console.log(`Viewing data flow for database: ${dbName} (${dbSlug})`);
// Selectively clear cached data for the current database only
try {
// Only clear the cache for the current database
if (dbSlug === 'my_dwh2') {
console.log('Setting empty schemas for service 2 database');
// For service 2, we want to ensure it always shows as empty
localStorage.setItem(`schemas_${dbSlug}`, JSON.stringify([]));
localStorage.setItem(`tables_${dbSlug}`, JSON.stringify([]));
} else {
// For other databases like MyDataWarehouseDB, we want to preserve their data
// but ensure we're not using stale data
console.log(`Preserving schemas for database: ${dbName}`);
// We don't need to clear anything for MyDataWarehouseDB
// This ensures its schemas will be loaded from the API or mock data
}
} catch (error) {
console.error('Error managing cached data in localStorage:', error);
}
// Store the selected database info in localStorage for the DataFlow component to use
localStorage.setItem('selectedDatabase', JSON.stringify({
id: dbId,
name: dbName,
slug: dbSlug, // Include the slug for API calls
isEmpty: dbSlug === 'my_dwh2', // Flag to indicate if this database has no schemas
timestamp: Date.now() // Add timestamp to ensure uniqueness
}));
// Set the current database slug in mockData.js
if (typeof window.setCurrentDbSlug === 'function') {
// First clear any existing data
window.setCurrentDbSlug(null);
// Then set the new database slug
setTimeout(() => {
window.setCurrentDbSlug(dbSlug);
}, 50);
} else {
console.warn('window.setCurrentDbSlug function is not available');
}
// Check if this database should have empty schemas
const isEmpty = dbSlug === 'my_dwh2';
// Trigger an event that App.jsx can listen to
const event = new CustomEvent('viewDataFlow', {
detail: {
databaseId: dbId,
databaseName: dbName,
databaseSlug: dbSlug,
isEmpty: isEmpty, // Flag to indicate if this database has no schemas
timestamp: Date.now() // Add timestamp to ensure uniqueness
}
});
window.dispatchEvent(event);
// No alert needed for MVP - the view will change automatically
};
// Create a service node for "Plan Plus"
const serviceNode = {
id: 'service-plan-plus',
type: 'service',
data: {
id: 'service-plan-plus',
// label: 'Qubit Service: Plan+',
databases: databases.length,
expanded: true,
onToggle: (id) => {
// Toggle expansion of the service node
setNodes(nodes => nodes.map(node => {
if (node.id === id) {
return { ...node, data: { ...node.data, expanded: !node.data.expanded } };
}
return node;
}));
}
},
position: { x: 400, y: 70 } // Positioned even higher on the canvas for maximum spacing
};
// Initialize with database nodes as children of the service
const databaseNodes = databases
.filter(db => db && db.id) // Filter out any invalid database objects
.map((db, index) => {
// Create a safe ID for the database
const safeId = db.id;
return {
id: safeId,
type: 'database',
data: {
id: safeId, // Explicitly include id in data to ensure it's available
label: db.name || `Database ${index + 1}`,
name: db.name, // Include the name from the API response
schemas: db.schemas || 0,
tables: db.tables || 0,
expanded: false,
onToggle: (id) => toggleDatabaseExpansion(id),
onViewDetails: handleViewDataFlow // Add the function to handle redirection
},
// Position databases in a grid layout with significantly increased vertical spacing
position: {
x: 250 + (index % 2) * 400, // 2 columns
y: 400 + Math.floor(index / 2) * 250 // further increased vertical spacing (400px) from service node
},
};
});
// Combine the service node with database nodes
const initialNodes = [
serviceNode,
...databaseNodes
];
// Create edges from service node to each database with custom hierarchical connections
const initialEdges = databases
.filter(db => db && db.id)
.map((db) => ({
id: `edge-service-to-${db.id}`,
source: 'service-plan-plus',
target: db.id,
type: 'hierarchical', // Using our custom hierarchical edge
animated: true,
style: {
stroke: '#00a99d',
strokeWidth: 2,
strokeDasharray: '5, 5', // Create dotted/dashed line effect
},
// No need for handles as they're built into the edge type
}));
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
// Update nodes when databases change (e.g., after API fetch)
useEffect(() => {
console.log('Databases updated, updating nodes:', databases);
// Create a parent service node for "Plan Plus"
const serviceNode = {
id: 'service-plan-plus',
type: 'service',
data: {
id: 'service-plan-plus',
// label: 'Qubit Service: Plan+',
databases: databases.length,
expanded: true,
onToggle: (id) => {
// Toggle expansion of the service node
setNodes(nodes => nodes.map(node => {
if (node.id === id) {
return { ...node, data: { ...node.data, expanded: !node.data.expanded } };
}
return node;
}));
}
},
position: { x: 400, y: 70 } // Positioned even higher on the canvas for maximum spacing
};
// Map database nodes as children of the service node
const databaseNodes = databases
.filter(db => db && db.id)
.map((db, index) => {
const existingNode = nodes.find(node => node.id === db.id);
// If node already exists, update its data
if (existingNode) {
return {
...existingNode,
data: {
...existingNode.data,
label: db.name || `Database ${index + 1}`,
name: db.name, // Make sure name is set from the API response
schemas: db.schemas || 0,
tables: db.tables || 0,
},
// Update position with significantly increased vertical spacing
position: {
x: 250 + (index % 2) * 400, // 2 columns
y: 400 + Math.floor(index / 2) * 250 // further increased vertical spacing (400px) from service node
}
};
}
// Otherwise create a new node
return {
id: db.id,
type: 'database',
data: {
id: db.id,
label: db.name || `Database ${index + 1}`,
name: db.name, // Include the name from the API response
schemas: db.schemas || 0,
tables: db.tables || 0,
expanded: false,
onToggle: (id) => toggleDatabaseExpansion(id),
onViewDetails: handleViewDataFlow
},
// Position databases in a grid layout with significantly increased vertical spacing
position: {
x: 250 + (index % 2) * 400,
y: 400 + Math.floor(index / 2) * 250 // further increased vertical spacing (400px) from service node
},
};
});
// Create edges from service node to each database with custom hierarchical connections
const serviceEdges = databases
.filter(db => db && db.id)
.map((db) => ({
id: `edge-service-to-${db.id}`,
source: 'service-plan-plus',
target: db.id,
type: 'hierarchical', // Using our custom hierarchical edge
animated: true,
style: {
stroke: '#00a99d',
strokeWidth: 2,
strokeDasharray: '5, 5', // Create dotted/dashed line effect
},
// No need for handles as they're built into the edge type
}));
// Combine service node with database nodes
const updatedNodes = [
serviceNode,
...databaseNodes
];
// Only update if we have nodes to show
if (updatedNodes.length > 0) {
setNodes(updatedNodes);
setEdges(serviceEdges);
}
}, [databases]);
// Track viewport changes using the onMove callback instead of event listeners
const onMove = useCallback((event, viewport) => {
setScale(viewport.zoom);
setPosition({ x: viewport.x, y: viewport.y });
}, []);
const onConnect = useCallback(
(params) => {
// Create a custom edge with the selected connection type
const edgeColor = connectionType === 'reference' ? '#00a99d' :
connectionType === 'dependency' ? '#ff4d4f' : '#722ed1';
const edgeLabel = connectionType === 'reference' ? 'references' :
connectionType === 'dependency' ? 'depends on' : 'connects to';
const newEdge = {
...params,
id: `e-custom-${params.source}-${params.target}`,
type: 'custom',
animated: true,
style: {
stroke: edgeColor,
strokeWidth: 2
},
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: edgeColor,
},
data: {
label: edgeLabel
}
};
// Add the new edge to the edges array
setEdges(eds => [...eds, newEdge]);
// If in connection mode, exit it after creating a connection
if (isConnectionMode) {
setIsConnectionMode(false);
setConnectionSource(null);
}
console.log(`Connection created: ${params.source} -> ${params.target} (${connectionType})`);
},
[setEdges, connectionType, isConnectionMode]
);
const onInit = (instance) => {
setReactFlowInstance(instance);
// Auto-expand the first database and its schemas on load
setTimeout(() => {
// Find the first database in our list
if (databases.length > 0) {
const firstDb = databases[0];
if (firstDb && firstDb.id) {
console.log(`Auto-expanding first database: ${firstDb.id}`);
// Expand the first database
toggleDatabaseExpansion(firstDb.id);
// After expanding the database, also expand all its schemas
setTimeout(() => {
const schemas = mockData.schemas.filter(schema => schema && schema.dbId === firstDb.id);
schemas.forEach(schema => {
if (schema && schema.id) {
toggleSchemaExpansion(schema.id);
}
});
// Fit view after all expansions
setTimeout(() => {
try {
fitView({ padding: 0.5, maxZoom: 0.7 });
} catch (error) {
console.error('Error calling fitView:', error);
}
}, 100);
}, 100);
}
}
}, 100);
};
// Handle node click for expanding/collapsing
const onNodeClick = (event, node) => {
// Only handle expansion/collapse if not in connection mode
if (!isConnectionMode) {
if (node.type === 'database') {
toggleDatabaseExpansion(node.id);
} else if (node.type === 'schema') {
toggleSchemaExpansion(node.id);
} else if (node.type === 'table') {
showTableDetails(node.id);
}
}
};
// Handle connection creation
const onConnectStart = (event, params) => {
console.log('Connection started:', params);
setConnectionSource(params);
};
// Handle connection completion
const onConnectEnd = (event) => {
console.log('Connection ended');
};
// Toggle database expansion to show/hide schemas
const toggleDatabaseExpansion = (dbId) => {
// Safety check for undefined or invalid dbId
if (!dbId) {
console.error('Invalid database ID provided to toggleDatabaseExpansion');
return;
}
const isExpanded = expandedDatabases[dbId];
// Update the database node to show the correct toggle state
setNodes(nodes => nodes.map(node => {
if (node.id === dbId) {
return {
...node,
data: {
...node.data,
expanded: !isExpanded,
onToggle: toggleDatabaseExpansion
}
};
}
return node;
}));
if (isExpanded) {
// Collapse: remove all schemas and tables for this database
setNodes(nodes => nodes.filter(node => {
if (node.type === 'database') return true;
if (node.type === 'schema' && node.data) {
return node.data.dbId !== dbId;
}
if (node.type === 'table' && node.data && node.data.schemaId) {
const schema = mockData.schemas.find(s => s.id === node.data.schemaId);
return !schema || schema.dbId !== dbId;
}
return true;
}));
// Safely filter edges
setEdges(edges => edges.filter(edge => {
if (!edge.source || !edge.target) return true;
const sourceStr = String(edge.source);
const targetStr = String(edge.target);
return !sourceStr.startsWith(dbId) && !targetStr.startsWith(dbId);
}));
// Update expanded state
setExpandedDatabases({
...expandedDatabases,
[dbId]: false
});
// Also collapse any expanded schemas
const updatedExpandedSchemas = { ...expandedSchemas };
mockData.schemas.forEach(schema => {
if (schema && schema.dbId === dbId) {
updatedExpandedSchemas[schema.id] = false;
}
});
setExpandedSchemas(updatedExpandedSchemas);
} else {
// Expand: add schema nodes for this database
const dbNode = nodes.find(n => n.id === dbId);
// Safety check if database node exists
if (!dbNode) {
console.error(`Database node with ID ${dbId} not found`);
return;
}
const dbSchemas = mockData.schemas.filter(schema => schema && schema.dbId === dbId);
// Check if we have schemas for this database
if (dbSchemas.length === 0) {
console.warn(`No schemas found for database ${dbId}`);
}
const schemaNodes = dbSchemas.map((schema, index) => ({
id: schema.id,
type: 'schema',
data: {
id: schema.id, // Include id in data
label: schema.name || `Schema ${index + 1}`,
tables: schema.tables || 0,
dbId: dbId,
expanded: false,
onToggle: toggleSchemaExpansion
},
position: {
x: dbNode.position.x - 150 + (index * 150),
y: dbNode.position.y + 150
},
}));
const schemaEdges = dbSchemas.map(schema => ({
id: `e-${dbId}-${schema.id}`,
source: dbId,
target: schema.id,
type: 'custom',
animated: true,
style: { stroke: '#00a99d', strokeWidth: 2 },
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: '#00a99d',
},
data: {
label: 'contains'
}
}));
setNodes(nodes => [...nodes, ...schemaNodes]);
setEdges(edges => [...edges, ...schemaEdges]);
// Update expanded state
setExpandedDatabases({
...expandedDatabases,
[dbId]: true
});
}
// Safely call fitView with a delay
setTimeout(() => {
try {
fitView();
} catch (error) {
console.error('Error calling fitView:', error);
}
}, 10);
};
// Toggle schema expansion to show/hide tables
const toggleSchemaExpansion = (schemaId) => {
// Safety check for undefined or invalid schemaId
if (!schemaId) {
console.error('Invalid schema ID provided to toggleSchemaExpansion');
return;
}
const isExpanded = expandedSchemas[schemaId];
// Update the schema node to show the correct toggle state
setNodes(nodes => nodes.map(node => {
if (node.id === schemaId) {
return {
...node,
data: {
...node.data,
expanded: !isExpanded,
onToggle: toggleSchemaExpansion
}
};
}
return node;
}));
if (isExpanded) {
// Collapse: remove all tables for this schema
setNodes(nodes => nodes.filter(node => {
if (node.type !== 'table') return true;
return !node.data || node.data.schemaId !== schemaId;
}));
// Safely filter edges
setEdges(edges => edges.filter(edge => {
if (!edge.source || !edge.target) return true;
const sourceStr = String(edge.source);
const targetStr = String(edge.target);
return !sourceStr.startsWith(schemaId) && !targetStr.startsWith(schemaId);
}));
// Update expanded state
setExpandedSchemas({
...expandedSchemas,
[schemaId]: false
});
} else {
// Expand: add table nodes for this schema
const schemaNode = nodes.find(n => n.id === schemaId);
// Safety check if schema node exists
if (!schemaNode) {
console.error(`Schema node with ID ${schemaId} not found`);
return;
}
const schemaTables = mockData.tables.filter(table => table && table.schemaId === schemaId);
// Check if we have tables for this schema
if (schemaTables.length === 0) {
console.warn(`No tables found for schema ${schemaId}`);
}
const tableNodes = schemaTables.map((table, index) => ({
id: table.id,
type: 'table',
data: {
id: table.id, // Include id in data
label: table.name || `Table ${index + 1}`,
isFact: table.isFact || false,
type: table.type || (table.isFact ? 'fact' : 'dimension'),
schemaId: schemaId,
columns: table.columns || []
},
position: {
x: schemaNode.position.x - 200 + (index * 150),
y: schemaNode.position.y + 150
},
}));
const tableEdges = schemaTables.map(table => ({
id: `e-${schemaId}-${table.id}`,
source: schemaId,
target: table.id,
type: 'custom',
animated: true,
style: {
stroke: table.isFact ? '#fa8c16' : '#52c41a',
strokeWidth: 2
},
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: table.isFact ? '#fa8c16' : '#52c41a',
},
data: {
label: table.isFact ? 'fact table' : 'dimension'
}
}));
setNodes(nodes => [...nodes, ...tableNodes]);
setEdges(edges => [...edges, ...tableEdges]);
// Update expanded state
setExpandedSchemas({
...expandedSchemas,
[schemaId]: true
});
}
setTimeout(() => {
fitView();
}, 10);
};
// Show table details when a table is clicked
const showTableDetails = (tableId) => {
// Look for the table in both the state tables and mock data
const table = tables.find(t => t.id === tableId) || mockData.tables.find(t => t.id === tableId);
if (!table) return;
setSelectedTable(table);
// Format the table type
const tableType = table.type || (table.isFact ? 'fact' : 'dimension');
// Format the columns list
const columnsText = table.columns && table.columns.length > 0
? `Columns:\n${table.columns.join('\n')}`
: 'No columns defined';
// In a real application, you would show a modal with detailed information
alert(`Table Details: ${table.name}\nType: ${tableType.toUpperCase()}\n\n${columnsText}\n\nIn a real application, this would show a modal with columns, relationships, and metrics.`);
};
// Delete a custom connection
const deleteConnection = (edgeId) => {
setEdges(edges => edges.filter(edge => edge.id !== edgeId));
};
// Get node label by ID
const getNodeLabel = (nodeId) => {
const node = nodes.find(n => n.id === nodeId);
return node ? node.data.label : nodeId;
};
// Reset the canvas to show only databases
const resetView = () => {
setNodes(initialNodes);
setEdges(initialEdges);
setExpandedDatabases({});
setExpandedSchemas({});
setSelectedTable(null);
setIsConnectionMode(false);
setConnectionSource(null);
setTimeout(() => {
fitView();
}, 10);
};
return (
{/* Add Button */}
setShowAddEntityModal(true)}
style={{
background: 'transparent',
border: 'none',
padding: 0,
cursor: 'pointer'
}}
title="Add DataSource"
>
{/* Minimal control panel */}
Reset View
{
if (reactFlowInstance) {
const viewport = reactFlowInstance.getViewport();
reactFlowInstance.setViewport({
x: viewport.x,
y: viewport.y,
zoom: Math.max(0.05, viewport.zoom * 0.5)
});
}
}}
style={{
padding: '5px 8px',
background: 'linear-gradient(45deg, #52c41a, #00a99d)',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontSize: '12px',
boxShadow: '0 0 5px rgba(82, 196, 26, 0.3)'
}}
className="glow-effect"
>
Zoom Out
Zoom: {Math.round(scale * 70)}%
Click nodes to expand
{/* Dbtez Control Panel */}
Dbtez Data Warehouse
toggleDatabaseExpansion('db4')}
style={{
padding: '4px 0',
background: 'linear-gradient(45deg, #00a99d, #52c41a)',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 'bold',
width: '30px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 5px rgba(0, 169, 157, 0.3)'
}}
className="glow-effect"
title="Expand/Collapse Database"
>
{expandedDatabases['db4'] ? '−' : '+'}
{
if (!expandedDatabases['db4']) {
toggleDatabaseExpansion('db4');
setTimeout(() => {
toggleSchemaExpansion('schema10');
}, 100);
} else if (!expandedSchemas['schema10']) {
toggleSchemaExpansion('schema10');
} else {
// If both are expanded, collapse schema
toggleSchemaExpansion('schema10');
}
}}
style={{
padding: '4px 0',
background: 'linear-gradient(45deg, #fa8c16, #faad14)',
color: 'white',
border: 'none',
borderRadius: '3px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: 'bold',
width: '30px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 5px rgba(250, 140, 22, 0.3)'
}}
className="glow-effect"
title="Expand/Collapse Sales Schema"
>
{expandedSchemas['schema10'] ? '−' : '+'}
{/* Connection Mode Indicator */}
{isConnectionMode && (
Connection Mode Active -
Drag from a connection point (dot) to another node
)}
{/* Connections Panel */}
{edges.some(edge => edge.id.startsWith('e-custom')) && (
Custom Connections
{edges
.filter(edge => edge.id.startsWith('e-custom'))
.map(edge => (
{getNodeLabel(edge.source)}
→
{getNodeLabel(edge.target)}
{edge.label || 'Connected to'}
deleteConnection(edge.id)}
style={{
background: 'transparent',
border: 'none',
color: '#ff4d4f',
cursor: 'pointer',
fontSize: '12px',
padding: '2px 5px'
}}
>
✕
))}
)}
{/* Entity Creation Modal */}
{
setShowAddEntityModal(false);
setActiveEntityType('database');
setSelectedDatabaseForSchema(null);
setSelectedSchemaForTable(null);
}}
>
{
setShowAddEntityModal(false);
setActiveEntityType('database');
setSelectedDatabaseForSchema(null);
setSelectedSchemaForTable(null);
}}
/>
);
};
export default function InfiniteCanvasWrapper() {
return (
);
}