Are you sure you want to delete the schema "{data.name}"? This action cannot be undone.
{deleteError && (
{deleteError}
)}
)}
);
};
// Custom edge with animated arrow
const CustomEdge = ({ id, source, target, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, data, markerEnd }) => {
// For left-to-right data flow, use a gentler curve
const curvature = 0.2; // Reduced curvature for straighter, more horizontal flow
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
curvature
});
// Check if the process is active
const isActive = data?.isActive !== false; // Default to active if not specified
// Determine if this is a source or target edge
const isSourceEdge = data?.edgeType === 'source';
// Get the process name and mappings
const processName = data?.processName || 'Process';
const mappings = data?.mappings || [];
// Adjust label style based on active status
const labelBgColor = isActive ? 'white' : '#f5f5f5';
const labelOpacity = isActive ? 1 : 0.8;
return (
<>
{/* Process name in small text */}
{processName}
{!isActive && (
inactive
)}
{/* Column mappings - show fewer columns to save space */}
{mappings.length > 0 ? (
// Only show up to 3 mappings to save space
mappings.slice(0, 3).map((column, index) => (
{column}
))
) : (
No mappings
)}
{mappings.length > 3 && (
+{mappings.length - 3} more
)}
>
);
};
// Process Node (represents ETL or data transformation)
const ProcessNode = ({ data, id }) => {
// Function to handle process edit
const handleProcessEdit = () => {
// Find the process in the data
// We'll use the data that's passed to the component directly
if (data && data.fullProcessData) {
// We'll use window.processEditCallback which will be set in the main component
if (window.processEditCallback) {
window.processEditCallback(data.fullProcessData);
}
}
};
// Determine status color
const statusColor = data.status === 'active' ? '#52c41a' : '#ff4d4f';
const isActive = data.status === 'active';
// Define colors
const primaryColor = isActive ? '#fa8c16' : '#aaaaaa';
const secondaryColor = isActive ? '#ff4d4f' : '#cccccc';
// Determine which icon to use based on process type or other properties
const getProcessIcon = () => {
// You can customize this logic based on your data model
const processType = data.processType || 'default';
const iconSize = 40; // Reduced icon size to prevent overlapping
const iconColor = isActive ? primaryColor : '#cccccc';
switch(processType.toLowerCase()) {
case 'transform':
return ;
case 'filter':
return ;
case 'aggregate':
return ;
case 'sync':
return ;
case 'code':
return ;
case 'analytics':
return ;
case 'transfer':
return ;
case 'exchange':
return ;
default:
return ;
}
};
return (
{/* Connection handles */}
{/* Process Icon with integrated content */}
{getProcessIcon()}
{/* Status indicator (small dot) */}
{/* Process Label (below icon) */}
{data.label}
);
};
// Table Node
const TableNode = ({ data, id }) => {
// Debug the data object to ensure all required fields are present
console.log(`TableNode ${id} data:`, {
slug: data.slug,
schema: data.schema,
database: data.database,
label: data.label,
type: data.type
});
// Set up drag functionality for new tables
const onDragStart = (event) => {
// Only allow dragging for tables that are not in a schema yet
if (!event.target.closest('.draggable-new-table')) {
return;
}
event.dataTransfer.setData('application/reactflow', id);
event.dataTransfer.effectAllowed = 'move';
};
// Function to handle add column button click
const handleAddColumn = (e) => {
e.stopPropagation(); // Prevent event bubbling
// Access the global function to show column popup
if (typeof window.showColumnPopup === 'function') {
window.showColumnPopup({
tbl: data.slug,
sch: data.schema,
con: data.database
});
} else {
console.error('showColumnPopup function not available');
}
};
// Function to handle table update button click
const handleUpdateTable = (e) => {
e.stopPropagation(); // Prevent event bubbling
// Access the global function to show table update popup
if (typeof window.showTableUpdatePopup === 'function') {
window.showTableUpdatePopup({
tbl: data.slug,
sch: data.schema,
con: data.database,
name: data.label,
table_type: data.type,
external_name: data.external_name || data.label // Use external_name if available, otherwise use label
});
} else {
console.error('showTableUpdatePopup function not available');
}
};
// Function to handle table delete button click
const handleDeleteTable = (e) => {
e.stopPropagation(); // Prevent event bubbling
// Confirm deletion with a warning about dependent objects
if (window.confirm(`Are you sure you want to delete the table "${data.label}"?\n\nThis action cannot be undone.`)) {
// Access the global function to delete table
if (typeof window.deleteTable === 'function') {
// Log the data being sent to ensure all fields are present
console.log('Sending table data for deletion:', {
tbl: data.slug,
sch: data.schema,
con: data.database,
label: data.label,
type: data.type
});
// Get the current database slug from the global variable
const currentDbSlug = window.getCurrentDbSlug ? window.getCurrentDbSlug() : null;
console.log(`Current database slug from global: ${currentDbSlug}`);
// Show a loading message
const loadingMessage = document.createElement('div');
loadingMessage.style.position = 'fixed';
loadingMessage.style.top = '50%';
loadingMessage.style.left = '50%';
loadingMessage.style.transform = 'translate(-50%, -50%)';
loadingMessage.style.padding = '20px';
loadingMessage.style.background = 'rgba(0, 0, 0, 0.8)';
loadingMessage.style.color = 'white';
loadingMessage.style.borderRadius = '8px';
loadingMessage.style.zIndex = '9999';
loadingMessage.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
loadingMessage.textContent = `Deleting table "${data.label}"...`;
document.body.appendChild(loadingMessage);
// Delete the table
window.deleteTable({
tbl: data.slug,
sch: data.schema,
// Use the database from data if available, otherwise use the current database slug
con: data.database || currentDbSlug
}).finally(() => {
// Remove the loading message when done
document.body.removeChild(loadingMessage);
});
} else {
console.error('deleteTable function not available');
}
}
};
// Determine table type
const isStage = data.type === 'stage';
const isFact = data.type === 'fact';
const isDim = data.type === 'dimension';
// Different colors for different table types
let tableColor, tableLabel, tableIcon;
if (isStage) {
tableColor = '#fa8c16'; // Orange for STAGE tables
tableLabel = 'STAGE';
tableIcon = ;
} else if (isFact) {
tableColor = '#1890ff'; // Blue for FACT tables
tableLabel = 'FACT';
tableIcon = ;
} else {
tableColor = '#52c41a'; // Green for DIM tables
tableLabel = 'DIM';
tableIcon = ;
}
const background = '#ffffff';
return (
{/* Connection handles */}
{/* Different icons for different table types */}
{tableIcon}
{data.label}
{tableLabel}
{/* Table Action Buttons */}
{/* Always show the columns section, even if empty */}
Columns:
{/* Display columns if they exist */}
{data.columns && data.columns.length > 0 ? (
No columns defined. Click "Manage Columns" to add.
)}
);
};
// Create a global reference to store component functions
window.dataflowCanvasApi = window.dataflowCanvasApi || {};
const DataflowCanvas = ({ dbSlug, hasSchemas = true }) => {
// React Flow refs and state
const reactFlowWrapper = useRef(null);
// const popupRef = useRef(null);
const [reactFlowInstance, setReactFlowInstance] = useState(null);
const { fitView, setViewport, getViewport } = useReactFlow();
// Get data from API - force refresh when dbSlug changes
const { data: apiData, loading, error, refreshData } = useApiData(true, dbSlug);
// All state declarations must be at the top level
// State for infinite canvas
const [scale, setScale] = useState(1);
const [position, setPosition] = useState({ x: 0, y: 0 });
// State for selected database
const [selectedDatabase, setSelectedDatabase] = useState(null);
// State for connection mode
const [isConnectionMode, setIsConnectionMode] = useState(false);
const [connectionSource, setConnectionSource] = useState(null);
const [connectionType, setConnectionType] = useState('default');
// State for process form
const [showProcessForm, setShowProcessForm] = useState(false);
const [selectedProcessForEdit, setSelectedProcessForEdit] = useState(null);
// State for process details popup
const [showProcessPopup, setShowProcessPopup] = useState(false);
const [selectedProcess, setSelectedProcess] = useState(null);
const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 });
// State for table creation popup
const [showTablePopup, setShowTablePopup] = useState(false);
const [selectedSchemaForTable, setSelectedSchemaForTable] = useState(null);
const [isDragging, setIsDragging] = useState(false);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
// State for column management popup
const [showColumnPopup, setShowColumnPopup] = useState(false);
const [selectedTableInfo, setSelectedTableInfo] = useState(null);
const [tableColumns, setTableColumns] = useState([]);
// State for table update popup
const [showTableUpdatePopup, setShowTableUpdatePopup] = useState(false);
const [selectedTableForUpdate, setSelectedTableForUpdate] = useState(null);
// State for schema creation popup
const [showSchemaPopup, setShowSchemaPopup] = useState(false);
const [newSchemaName, setNewSchemaName] = useState('');
const [newSchemaDescription, setNewSchemaDescription] = useState('');
const [isCreatingSchema, setIsCreatingSchema] = useState(false);
const [schemaCreationError, setSchemaCreationError] = useState(null);
// Read selected database from localStorage on component mount or when dbSlug changes
useEffect(() => {
try {
console.log(`DataflowCanvas: Loading database with slug: ${dbSlug}`);
// Get the database info from localStorage
const dbData = localStorage.getItem('selectedDatabase');
if (dbData) {
const parsedData = JSON.parse(dbData);
// Only use the database from localStorage if it matches the current dbSlug
// or if no dbSlug is provided (backward compatibility)
if (!dbSlug || parsedData.slug === dbSlug) {
setSelectedDatabase(parsedData);
console.log('DataFlow view initialized with database:', parsedData);
// 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 with a small delay to ensure cleanup
setTimeout(() => {
window.setCurrentDbSlug(parsedData.slug);
// Force a re-render of the component
setSelectedDatabase({...parsedData, timestamp: Date.now()});
}, 50);
}
} else {
console.log(`Database in localStorage (${parsedData.slug}) doesn't match current dbSlug (${dbSlug})`);
// If we have a dbSlug but it doesn't match localStorage, update localStorage
if (dbSlug) {
// Find the database name based on slug
let dbName = "Unknown Database";
if (dbSlug === 'my_dwh') dbName = "MyDataWarehouseDB";
if (dbSlug === 'my_dwh2') dbName = "service 2";
// Create a new database object
const newDbData = {
id: `db-${dbSlug}`,
name: dbName,
slug: dbSlug,
isEmpty: dbSlug === 'my_dwh2',
timestamp: Date.now()
};
// Update localStorage
localStorage.setItem('selectedDatabase', JSON.stringify(newDbData));
setSelectedDatabase(newDbData);
// Set the current database slug in mockData.js
if (typeof window.setCurrentDbSlug === 'function') {
window.setCurrentDbSlug(dbSlug);
}
}
}
} else if (dbSlug) {
// If we have a dbSlug but no localStorage data, create it
let dbName = "Unknown Database";
if (dbSlug === 'my_dwh') dbName = "MyDataWarehouseDB";
if (dbSlug === 'my_dwh2') dbName = "service 2";
// Create a new database object
const newDbData = {
id: `db-${dbSlug}`,
name: dbName,
slug: dbSlug,
isEmpty: dbSlug === 'my_dwh2',
timestamp: Date.now()
};
// Update localStorage
localStorage.setItem('selectedDatabase', JSON.stringify(newDbData));
setSelectedDatabase(newDbData);
// Set the current database slug in mockData.js
if (typeof window.setCurrentDbSlug === 'function') {
window.setCurrentDbSlug(dbSlug);
}
}
} catch (error) {
console.error('Error reading database from localStorage:', error);
}
}, [dbSlug]); // Re-run when dbSlug changes
// Use API data when available
useEffect(() => {
if (apiData && !loading) {
console.log('API data loaded successfully:', apiData);
console.log('Schemas:', apiData.schemas?.length);
console.log('Tables:', apiData.tables?.length);
}
}, [apiData, loading]);
// Create a loading component
const LoadingComponent = () => (
Loading data from API...
);
// Hierarchical Breadcrumb Component
const HierarchicalBreadcrumb = () => {
const [dataMappingsMenuAnchor, setDataMappingsMenuAnchor] = useState(null);
const [selectedView, setSelectedView] = useState('Data Flow View');
const handleDataMappingsClick = (event) => {
setDataMappingsMenuAnchor(event.currentTarget);
};
const handleDataMappingsClose = () => {
setDataMappingsMenuAnchor(null);
};
const handleViewSelect = (view) => {
setSelectedView(view);
setDataMappingsMenuAnchor(null);
// Handle navigation based on selected view
if (view === 'Pipelines') {
// Add logic to switch to Pipelines view
console.log('Switching to Pipelines view');
// You can add navigation logic here
} else {
// Data Flow View is the current view
console.log('Staying in Data Flow View');
}
};
return (
<>
{/* Data Mappings Menu */}
>
);
};
// Log error but continue with mockApiData as fallback
useEffect(() => {
if (error) {
console.error('Error loading API data:', error);
}
}, [error]);
// Create a loading component
// const LoadingComponent = () => (
//
//
Loading data from API...
//
// );
// Log error but continue with mockApiData as fallback
useEffect(() => {
if (error) {
console.error('Error loading API data:', error);
}
}, [error]);
// Set up global callbacks for process editing and column creation
// This is used by the ProcessNode and TableNode components
useEffect(() => {
// Store references to the state setters in the global API object
window.dataflowCanvasApi = {
...window.dataflowCanvasApi,
setSelectedProcessForEdit,
setShowProcessForm,
setTableColumns,
setSelectedTableInfo,
setShowColumnPopup,
setSelectedSchemaForTable,
setShowTablePopup,
fetchColumns
};
// Process edit callback
window.processEditCallback = (process) => {
setSelectedProcessForEdit(process);
setShowProcessForm(true);
};
// Column management callback
window.showColumnPopup = async (tableInfo) => {
console.log('Show column popup called with table info:', tableInfo);
try {
// Fetch columns for the selected table
const columns = await fetchColumns(tableInfo.tbl, tableInfo.sch, tableInfo.con);
console.log('Fetched columns for table:', columns);
// Process columns to ensure they have both slug and col properties
const processedColumns = columns.map(column => ({
...column,
col: column.col || column.slug, // Ensure col exists
slug: column.slug || column.col // Ensure slug exists
}));
console.log('Processed columns:', processedColumns);
// Set the table columns
setTableColumns(processedColumns || []);
// Set the selected table info
setSelectedTableInfo(tableInfo);
// Show the column popup
setShowColumnPopup(true);
} catch (error) {
console.error('Error fetching columns for table:', error);
alert(`Error fetching columns: ${error.message}`);
// Still show the popup but with empty columns
setTableColumns([]);
setSelectedTableInfo(tableInfo);
setShowColumnPopup(true);
}
};
// Table creation callback - define it directly on the window object
window.showTableCreationPopup = (schemaInfo) => {
console.log('Show table creation popup called with schema info:', schemaInfo);
// Set the selected schema for table creation
setSelectedSchemaForTable(schemaInfo);
// Show the table creation popup
setShowTablePopup(true);
};
// Table update popup callback
window.showTableUpdatePopup = (tableInfo) => {
console.log('Show table update popup called with table info:', tableInfo);
setSelectedTableForUpdate(tableInfo);
setShowTableUpdatePopup(true);
};
// Table delete callback
window.deleteTable = async (tableInfo) => {
console.log('Delete table called with table info:', tableInfo);
try {
// Always ensure the database identifier is present
// First try to use the provided con
// Then try to use the selected database
// Then try to use the global currentDbSlug
if (!tableInfo.con) {
if (selectedDatabase && selectedDatabase.slug) {
console.log(`Adding missing database identifier from selected database: ${selectedDatabase.slug}`);
tableInfo.con = selectedDatabase.slug;
} else if (typeof window.getCurrentDbSlug === 'function') {
const currentDbSlug = window.getCurrentDbSlug();
if (currentDbSlug) {
console.log(`Adding missing database identifier from global currentDbSlug: ${currentDbSlug}`);
tableInfo.con = currentDbSlug;
}
}
}
// Log the final table info being sent
console.log('Final table info for deletion:', tableInfo);
await handleDeleteTable(tableInfo);
} catch (error) {
console.error('Error deleting table:', error);
alert(`Error deleting table: ${error.message}`);
}
};
// Also define it on our API object for redundancy
window.dataflowCanvasApi.showTableCreationPopup = window.showTableCreationPopup;
window.dataflowCanvasApi.showTableUpdatePopup = window.showTableUpdatePopup;
window.dataflowCanvasApi.deleteTable = window.deleteTable;
console.log('Global callbacks set up:', {
processEditCallback: typeof window.processEditCallback,
showColumnPopup: typeof window.showColumnPopup,
showTableCreationPopup: typeof window.showTableCreationPopup,
showTableUpdatePopup: typeof window.showTableUpdatePopup,
deleteTable: typeof window.deleteTable
});
return () => {
// Clean up when component unmounts
window.processEditCallback = null;
window.showColumnPopup = null;
window.showTableCreationPopup = null;
window.showTableUpdatePopup = null;
window.deleteTable = null;
// Clear the API object
window.dataflowCanvasApi = {};
};
}, []);
// Define node types
const nodeTypes = useMemo(() => ({
table: TableNode,
process: ProcessNode,
schemaBackground: SchemaBackgroundNode,
}), []);
// Define edge types
const edgeTypes = useMemo(() => ({
custom: CustomEdge,
}), []);
// Create initial nodes from tables data
const initialNodes = useMemo(() => {
// If hasSchemas is false, return empty array immediately
if (!hasSchemas) {
console.log('Database is known to have no schemas - skipping node creation');
return [];
}
// Calculate schema boundaries based on their tables
const schemaBoundaries = {};
// Use API data when available, otherwise fall back to mock data
// Make sure we're using a stable reference to the data
const schemas = apiData?.schemas || mockApiData.schemas;
const tables = apiData?.tables || mockApiData.tables;
console.log('DataflowCanvas - Schemas available:', schemas?.length || 0);
console.log('DataflowCanvas - Tables available:', tables?.length || 0);
console.log('DataflowCanvas - hasSchemas prop:', hasSchemas);
console.log('DataflowCanvas - dbSlug prop:', dbSlug);
console.log('DataflowCanvas - selectedDatabase:', selectedDatabase?.name);
// Special handling for MyDataWarehouseDB
if (dbSlug === 'my_dwh' && (!schemas || schemas.length === 0)) {
console.log('MyDataWarehouseDB should have schemas but none found - using default mock data');
// Force the use of mock data for MyDataWarehouseDB
return createNodesFromMockData();
}
// Check if we have any schemas
if (!schemas || schemas.length === 0) {
console.log('No schemas available for this database');
// Return an empty array if there are no schemas
return [];
}
// Helper function to create nodes from mock data
function createNodesFromMockData() {
console.log('Creating nodes from default mock data');
const mockSchemas = mockApiData.schemas;
const mockTables = mockApiData.tables;
// Initialize schema boundaries
mockSchemas.forEach(schema => {
schemaBoundaries[schema.slug] = {
minX: schema.position?.x || 0,
minY: schema.position?.y || 0,
maxX: (schema.position?.x || 0) + (schema.width || 500),
maxY: (schema.position?.y || 0) + (schema.height || 500)
};
});
// Update boundaries based on table positions
mockTables.forEach(table => {
const schemaSlug = table.schema;
if (schemaBoundaries[schemaSlug]) {
const tableMinX = table.orientation.x - 250;
const tableMinY = table.orientation.y - 250;
const tableMaxX = table.orientation.x + 450;
const tableMaxY = table.orientation.y + 450;
schemaBoundaries[schemaSlug].minX = Math.min(schemaBoundaries[schemaSlug].minX, tableMinX);
schemaBoundaries[schemaSlug].minY = Math.min(schemaBoundaries[schemaSlug].minY, tableMinY);
schemaBoundaries[schemaSlug].maxX = Math.max(schemaBoundaries[schemaSlug].maxX, tableMaxX);
schemaBoundaries[schemaSlug].maxY = Math.max(schemaBoundaries[schemaSlug].maxY, tableMaxY);
}
});
// Create schema background nodes
const schemaBackgroundNodes = mockSchemas.map(schema => {
const bounds = schemaBoundaries[schema.slug];
const width = bounds.maxX - bounds.minX;
const height = bounds.maxY - bounds.minY;
return {
id: `schema-bg-${schema.slug}`,
type: 'schemaBackground',
position: { x: bounds.minX, y: bounds.minY },
style: { width, height },
data: {
name: schema.name,
slug: schema.slug,
color: schema.color,
database: schema.database || dbSlug // Include the database slug
},
draggable: true,
selectable: false
};
});
// Create table nodes
const tableNodes = mockTables.map(table => ({
id: `table-${table.slug}`,
type: 'table',
data: {
name: table.name,
slug: table.slug,
schema: table.schema,
columns: table.columns,
type: table.type || 'dimension'
},
position: { x: table.orientation.x, y: table.orientation.y },
parentNode: `schema-bg-${table.schema}`,
extent: 'parent',
style: {
marginBottom: '30px',
marginRight: '30px'
},
className: 'table-node'
}));
return [...schemaBackgroundNodes, ...tableNodes];
}
// Initialize with default values from schema definitions
schemas.forEach(schema => {
schemaBoundaries[schema.slug] = {
minX: schema.position?.x || 0,
minY: schema.position?.y || 0,
maxX: (schema.position?.x || 0) + (schema.width || 500),
maxY: (schema.position?.y || 0) + (schema.height || 500)
};
});
// Update boundaries based on table positions
tables.forEach(table => {
const schemaSlug = table.schema;
if (schemaBoundaries[schemaSlug]) {
// Add more padding around tables (250px on each side for better spacing)
const tableMinX = table.orientation.x - 250;
const tableMinY = table.orientation.y - 250;
const tableMaxX = table.orientation.x + 450; // Table width + padding
const tableMaxY = table.orientation.y + 450; // Table height + padding
schemaBoundaries[schemaSlug].minX = Math.min(schemaBoundaries[schemaSlug].minX, tableMinX);
schemaBoundaries[schemaSlug].minY = Math.min(schemaBoundaries[schemaSlug].minY, tableMinY);
schemaBoundaries[schemaSlug].maxX = Math.max(schemaBoundaries[schemaSlug].maxX, tableMaxX);
schemaBoundaries[schemaSlug].maxY = Math.max(schemaBoundaries[schemaSlug].maxY, tableMaxY);
}
});
// Schema background nodes (add these first so they appear behind other nodes)
const schemaBackgroundNodes = schemas.map(schema => {
const bounds = schemaBoundaries[schema.slug];
const width = bounds.maxX - bounds.minX;
const height = bounds.maxY - bounds.minY;
// Add 60% extra size for better spacing and to ensure all elements are contained
const extraWidth = width * 0.6;
const extraHeight = height * 0.6;
return {
id: `schema-bg-${schema.slug}`,
type: 'schemaBackground',
data: {
name: schema.name,
color: schema.color,
slug: schema.slug,
database: schema.database || dbSlug // Include the database slug
},
position: {
x: bounds.minX - (extraWidth / 2),
y: bounds.minY - (extraHeight / 2)
},
style: {
width: width + extraWidth,
height: height + extraHeight,
zIndex: -1 // Ensure it's behind other nodes
},
draggable: true,
selectable: true,
zIndex: -1,
};
});
// Table nodes with additional spacing
const tableNodes = tables.map(table => ({
id: table.slug,
type: 'table',
data: {
label: table.name,
type: table.type,
columns: table.columns,
slug: table.slug,
schema: table.schema, // Include schema information
database: dbSlug // Include database information
},
position: (table.config && table.config.orientation) ? table.config.orientation :
(table.orientation || {
// Try to get stored position from localStorage
...((() => {
try {
const storageKey = `table_position_${dbSlug}_${table.schema}_${table.slug}`;
const storedPosition = localStorage.getItem(storageKey);
if (storedPosition) {
return JSON.parse(storedPosition);
}
} catch (e) {
console.error('Error retrieving stored position:', e);
}
// Default random position if nothing is stored
return {
x: Math.floor(Math.random() * 700) + 100,
y: Math.floor(Math.random() * 700) + 100
};
})())
}),
parentNode: `schema-bg-${table.schema}`, // Connect to parent schema
extent: 'parent', // Keep within parent boundaries
style: {
marginBottom: '30px', // Add margin to prevent overlapping
marginRight: '30px' // Add margin to prevent overlapping
},
className: 'table-node' // Add class for styling
}));
return [...schemaBackgroundNodes, ...tableNodes];
}, [apiData, hasSchemas, dbSlug]); // Add dependencies to ensure re-render when they change
// Create process nodes
const processNodes = useMemo(() => {
// If hasSchemas is false, return empty array immediately
if (!hasSchemas) {
console.log('Database is known to have no schemas - skipping process node creation');
return [];
}
// Use API data when available, otherwise fall back to mock data
const processes = apiData?.processes || mockApiData.processes;
const tables = apiData?.tables || mockApiData.tables;
return processes.map((process, index) => {
// Calculate position between source and destination tables
const sourceTable = tables.find(t => t.slug === process.source_table[0]);
const destTable = tables.find(t => t.slug === process.destination_table[0]);
let x = 300;
let y = 200;
let parentSchema = null;
if (sourceTable && destTable) {
// For a left-to-right data flow, position the process node between the source and destination
// but slightly closer to the source to maintain the flow direction
// Calculate the position 1/3 of the way from source to destination
const sourceX = sourceTable.orientation.x;
const sourceY = sourceTable.orientation.y;
const destX = destTable.orientation.x;
const destY = destTable.orientation.y;
// Position process node at 1/3 of the distance from source to destination
// This creates a clearer left-to-right flow
x = sourceX + (destX - sourceX) / 3 + 100; // Add offset to position after source table
// Keep the y-coordinate aligned with the source table for a cleaner look
// If source and destination are at different y positions, adjust slightly
if (Math.abs(sourceY - destY) > 50) {
// If vertical difference is significant, position between them
y = sourceY + (destY - sourceY) / 2;
} else {
// Otherwise keep aligned with source
y = sourceY;
}
// Determine which schema this process belongs to
// If source and destination are in the same schema, use that schema
if (sourceTable.schema === destTable.schema) {
parentSchema = sourceTable.schema;
} else {
// If they're in different schemas, use the source schema
parentSchema = sourceTable.schema;
}
} else if (sourceTable) {
parentSchema = sourceTable.schema;
} else if (destTable) {
parentSchema = destTable.schema;
}
return {
id: process.slug,
type: 'process',
data: {
label: process.name,
description: process.description,
type: process.type,
status: process.status,
processType: process.type, // Pass process type to the component
fullProcessData: process // Include the full process data for editing
},
position: { x, y },
parentNode: parentSchema ? `schema-bg-${parentSchema}` : undefined,
extent: parentSchema ? 'parent' : undefined,
style: {
zIndex: 5, // Ensure process nodes are above edges
margin: '20px', // Add margin to prevent overlapping
background: 'white', // Ensure background is white
borderRadius: '8px', // Rounded corners
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)' // Add shadow for better visibility
},
className: 'process-node' // Add class for styling
};
});
}, [apiData, hasSchemas, dbSlug]); // Add dependencies to ensure re-render when they change
// Create edges between tables and processes
const initialEdges = useMemo(() => {
// If hasSchemas is false, return empty array immediately
if (!hasSchemas) {
console.log('Database is known to have no schemas - skipping edge creation');
return [];
}
const edges = [];
// Use API data when available, otherwise fall back to mock data
const processes = apiData?.processes || mockApiData.processes;
processes.forEach(process => {
// Determine if process is active or inactive
const isActive = process.status === 'active';
// Set colors and animation based on process status
const sourceColor = isActive ? '#52c41a' : '#aaaaaa'; // Green or gray
const destColor = isActive ? '#1890ff' : '#aaaaaa'; // Blue or gray
const animated = isActive; // Only animate if active
const strokeWidth = isActive ? 2 : 1.5; // Slightly thinner if inactive
// Get the mappings for this process
const mappings = process.mappings || [];
// Format source columns for display
const sourceColumns = mappings.map(m => m.source).join(', ');
// Format target columns for display
const targetColumns = mappings.map(m => m.target).join(', ');
// Create edges from source tables to process
process.source_table.forEach(sourceId => {
edges.push({
id: `e-${sourceId}-${process.slug}`,
source: sourceId,
target: process.slug,
type: 'custom',
animated: animated,
style: {
stroke: sourceColor,
strokeWidth: strokeWidth,
opacity: isActive ? 0.9 : 0.6, // Reduced opacity for better visual clarity
zIndex: 4 // Ensure edges are below nodes
},
markerEnd: {
type: 'arrowclosed',
width: 15, // Smaller arrow
height: 15, // Smaller arrow
color: sourceColor,
},
data: {
label: sourceColumns || 'No mappings',
processName: process.name,
isActive: isActive, // Pass status to the edge component
edgeType: 'source',
mappings: mappings.map(m => m.source)
}
});
});
// Create edges from process to destination tables
process.destination_table.forEach(destId => {
edges.push({
id: `e-${process.slug}-${destId}`,
source: process.slug,
target: destId,
type: 'custom',
animated: animated,
style: {
stroke: destColor,
strokeWidth: strokeWidth,
opacity: isActive ? 0.9 : 0.6, // Reduced opacity for better visual clarity
zIndex: 4 // Ensure edges are below nodes
},
markerEnd: {
type: 'arrowclosed',
width: 15, // Smaller arrow
height: 15, // Smaller arrow
color: destColor,
},
data: {
label: targetColumns || 'No mappings',
processName: process.name,
isActive: isActive, // Pass status to the edge component
edgeType: 'target',
mappings: mappings.map(m => m.target)
}
});
});
});
return edges;
}, [apiData, hasSchemas, dbSlug]); // Add dependencies to ensure re-render when they change
// Use React Flow's node state management
const [nodes, setNodes, onNodesChangeDefault] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Update nodes and edges when initialNodes or initialEdges change
useEffect(() => {
console.log('Updating nodes from initialNodes and processNodes:', initialNodes.length, processNodes.length);
// Get current custom nodes (nodes that were added manually, not from API data)
setNodes(currentNodes => {
// First, identify any custom nodes that aren't in initialNodes
// These would be nodes that were added manually (like newly created tables)
const customNodes = currentNodes.filter(node => {
// Check if this node is not in initialNodes
// For table nodes, we check by slug
if (node.type === 'table') {
const nodeSlug = node.data.slug;
const existsInInitial = initialNodes.some(initNode =>
initNode.type === 'table' && initNode.data.slug === nodeSlug
);
// If it doesn't exist in initialNodes, it's a custom node
return !existsInInitial;
}
// For other node types, we can just include them
return false;
});
console.log('Found custom nodes that need to be preserved:', customNodes.length);
// Merge initialNodes, processNodes, and customNodes
// But avoid duplicates by checking node IDs
const mergedNodes = [...initialNodes, ...processNodes];
// Add custom nodes if they don't already exist in the merged nodes
customNodes.forEach(customNode => {
const exists = mergedNodes.some(node => node.id === customNode.id);
if (!exists) {
mergedNodes.push(customNode);
}
});
console.log('Final merged nodes count:', mergedNodes.length);
return mergedNodes;
});
}, [initialNodes, processNodes, dbSlug, apiData]);
useEffect(() => {
console.log('Updating edges from initialEdges:', initialEdges.length);
// Always set edges, even if empty, to ensure proper rendering
setEdges(initialEdges);
}, [initialEdges, dbSlug, apiData]);
// Make refreshData available globally for the SchemaBackgroundNode component
useEffect(() => {
// Create an enhanced refresh function that ensures UI updates
window.refreshCanvasData = async () => {
console.log('Enhanced refreshCanvasData called');
try {
// First call the original refreshData function to update the cache
await refreshData();
// After refreshing data, update the nodes and edges
// This ensures the UI reflects the latest data
console.log('Updating nodes and edges after data refresh');
// Get the latest data
const latestData = apiData || mockApiData;
// Create a map of tables by slug for quick lookup
const tableMap = {};
if (latestData.tables) {
latestData.tables.forEach(table => {
tableMap[table.slug] = table;
});
}
// Update the nodes with the latest data
setNodes(nodes => {
console.log('Updating nodes with latest data, preserving custom nodes');
// First, identify any custom nodes that aren't in the API data
const customNodes = [];
const apiNodeIds = new Set();
// Track which nodes are from the API
nodes.forEach(node => {
if (node.type === 'table') {
const tableSlug = node.data.slug;
if (tableMap[tableSlug]) {
// This is an API node
apiNodeIds.add(node.id);
} else {
// This might be a custom node
customNodes.push(node);
}
} else {
// Non-table nodes are preserved
customNodes.push(node);
}
});
console.log(`Found ${customNodes.length} custom nodes to preserve`);
// Update each node with the latest data
const updatedNodes = nodes.map(node => {
// Only update table nodes that exist in the API data
if (node.type === 'table') {
const tableSlug = node.data.slug;
const tableData = tableMap[tableSlug];
// If we have updated data for this table
if (tableData) {
console.log(`Updating table node ${tableSlug} with latest data:`, tableData);
// Get column names from the table data
const columnNames = tableData.columns ? tableData.columns.map(col => col.name || col) : [];
// Return updated node
return {
...node,
data: {
...node.data,
columns: columnNames,
label: tableData.name,
type: tableData.table_type
}
};
}
}
// Return unchanged node
return node;
});
console.log('Nodes updated with latest data');
return updatedNodes;
});
// Force edge re-render
setEdges(edges => {
console.log('Forcing edge re-render');
return [...edges];
});
} catch (error) {
console.error('Error in refreshCanvasData:', error);
}
};
// Set up the schema update callback
window.onSchemaUpdated = (updatedSchema) => {
if (!updatedSchema) return;
console.log('Schema update detected, updating nodes directly:', updatedSchema);
try {
// Find and update the schema node directly
setNodes(currentNodes => {
return currentNodes.map(node => {
if (node.type === 'schemaBackground' &&
node.data &&
node.data.slug === updatedSchema.slug) {
console.log('Found schema node to update:', node.id);
return {
...node,
data: {
...node.data,
name: updatedSchema.name,
description: updatedSchema.description,
updated_at: updatedSchema.updated_at
}
};
}
return node;
});
});
} catch (error) {
console.error('Error updating nodes in onSchemaUpdated:', error);
}
};
return () => {
// Clean up when component unmounts
delete window.refreshCanvasData;
delete window.onSchemaUpdated;
};
}, [refreshData, setNodes, setEdges]);
// Custom nodes change handler to update schema boundaries when nodes move
const onNodesChange = useCallback(
(changes) => {
// Apply the default node changes
onNodesChangeDefault(changes);
// Check if any of the changes are position changes
const hasPositionChanges = changes.some(
change => change.type === 'position' && change.dragging
);
// If there are position changes, force a re-render of the schema wrapper
if (hasPositionChanges) {
// Use a small timeout to ensure the node positions are updated first
setTimeout(() => {
setNodes((nds) => [...nds]);
}, 0);
}
},
[onNodesChangeDefault, setNodes]
);
// Track viewport changes
const onMove = useCallback((event, viewport) => {
setScale(viewport.zoom);
setPosition({ x: viewport.x, y: viewport.y });
}, []);
const onConnect = useCallback(
(params) => {
// Find the source and target nodes
const sourceNode = nodes.find(node => node.id === params.source);
const targetNode = nodes.find(node => node.id === params.target);
// Determine if this is a process connection
let isActive = true;
// Check if either source or target is a process node
if (sourceNode?.type === 'process') {
isActive = sourceNode.data.status === 'active';
} else if (targetNode?.type === 'process') {
isActive = targetNode.data.status === 'active';
}
// Create a custom edge with the selected connection type
const edgeColor = connectionType === 'reference' ? '#00a99d' :
connectionType === 'dependency' ? '#ff4d4f' : '#722ed1';
// If inactive, use gray color
const finalColor = isActive ? edgeColor : '#aaaaaa';
const edgeLabel = connectionType === 'reference' ? 'references' :
connectionType === 'dependency' ? 'depends on' : 'connects to';
const newEdge = {
...params,
id: `e-custom-${params.source}-${params.target}`,
type: 'custom',
animated: isActive, // Only animate if active
style: {
stroke: finalColor,
strokeWidth: isActive ? 2 : 1.5,
opacity: isActive ? 1 : 0.7
},
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: finalColor,
},
data: {
label: edgeLabel,
isActive: isActive
}
};
// 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}), Active: ${isActive}`);
},
[setEdges, connectionType, isConnectionMode, nodes]
);
// Handle connection creation
const onConnectStart = (event, params) => {
console.log('Connection started:', params);
setConnectionSource(params);
};
// Handle connection completion
const onConnectEnd = (event) => {
console.log('Connection ended');
};
// Initialize the flow
const onInit = (instance) => {
setReactFlowInstance(instance);
// Set the viewport from the API data or fallback to mock data
const viewportSettings = apiData?.viewportSettings || mockApiData.viewportSettings;
const { x, y, zoom } = viewportSettings;
instance.setViewport({ x, y, zoom });
setTimeout(() => {
// Use a larger padding and lower maxZoom to ensure all nodes are visible
fitView({ padding: 1.0, maxZoom: 0.4 });
}, 500);
};
const popupRef = useRef(null);
// Drag handlers for the popup
const handleMouseDown = (e) => {
if (popupRef.current && !e.target.closest('button')) {
const rect = popupRef.current.getBoundingClientRect();
setIsDragging(true);
setDragOffset({
x: e.clientX - rect.left,
y: e.clientY - rect.top
});
e.preventDefault();
}
};
const handleMouseMove = (e) => {
if (isDragging && popupRef.current) {
const newX = e.clientX - dragOffset.x;
const newY = e.clientY - dragOffset.y;
// Keep popup within viewport bounds
const rect = popupRef.current.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const boundedX = Math.max(0, Math.min(newX, viewportWidth - rect.width));
const boundedY = Math.max(0, Math.min(newY, viewportHeight - rect.height));
setPopupPosition({ x: boundedX, y: boundedY });
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
// Add and remove event listeners for dragging
useEffect(() => {
if (showProcessPopup) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}
}, [showProcessPopup, isDragging, dragOffset]);
// Handle node click
const onNodeClick = (event, node) => {
console.log('Node clicked:', node);
// Use API data when available, otherwise fall back to mock data
const tables = apiData?.tables || mockApiData.tables;
const processes = apiData?.processes || mockApiData.processes;
// If it's a table node, show details
if (node.type === 'table') {
const table = tables.find(t => t.slug === node.id);
if (table) {
alert(`Table: ${table.name}\nType: ${table.type}\nColumns: ${table.columns.join(', ')}`);
}
}
// If it's a process node, show details in a custom popup
if (node.type === 'process') {
// Use the full process data from the node data if available
const process = node.data.fullProcessData || processes.find(p => p.slug === node.id);
if (process) {
// Get source and destination table names
const sourceTables = process.source_table.map(slug => {
const table = tables.find(t => t.slug === slug);
return table ? table.name : slug;
});
const destTables = process.destination_table.map(slug => {
const table = tables.find(t => t.slug === slug);
return table ? table.name : slug;
});
// Add table names to the process object
const processWithTableNames = {
...process,
source_table_names: sourceTables,
destination_table_names: destTables
};
setSelectedProcess(processWithTableNames);
// Position the popup near the node but not directly on top of it
const nodeElement = document.querySelector(`[data-id="${node.id}"]`);
if (nodeElement) {
const rect = nodeElement.getBoundingClientRect();
setPopupPosition({
x: rect.right + 20,
y: rect.top
});
} else {
setPopupPosition({
x: event.clientX,
y: event.clientY
});
}
setShowProcessPopup(true);
}
}
};
// Handle edit process button click in the popup
const handleEditProcess = (process) => {
setSelectedProcessForEdit(process);
setShowProcessForm(true);
setShowProcessPopup(false);
};
// Handle table creation from the popup
const handleCreateTable = async (tableData) => {
try {
console.log('Creating new table with data:', tableData);
// Extract orientation from config or use orientation directly, or generate random one
const orientation = (tableData.config && tableData.config.orientation) ?
tableData.config.orientation :
(tableData.orientation || {
x: Math.floor(Math.random() * 700) + 100,
y: Math.floor(Math.random() * 700) + 100
});
console.log('Using orientation for new table:', orientation);
console.log('Using orientation for new table:', orientation);
// Call the createTable function with the provided data and orientation
const newTable = await createTable(
tableData.sch,
tableData.name,
tableData.table_type,
tableData.external_name,
tableData.description,
tableData.con,
orientation
);
console.log('Table created successfully:', newTable);
// Prepare column data for the node
const columnNames = tableData.columns && tableData.columns.length > 0
? tableData.columns.map(col => col.name)
: [];
// Create a new table node
const tableNode = {
id: `table-${newTable.slug}`,
type: 'table',
data: {
label: newTable.name,
type: newTable.table_type,
slug: newTable.slug,
schema: tableData.sch,
database: tableData.con,
columns: columnNames, // Use the column names from the tableData
columnDetails: tableData.columns || [] // Store the full column details
},
// Position the table inside the schema using the orientation from the API
position: newTable.orientation || {
x: Math.floor(Math.random() * 400) + 100,
y: Math.floor(Math.random() * 400) + 100
},
// Make it part of the schema
parentNode: `schema-${tableData.sch}`,
extent: 'parent',
className: 'table-node',
style: {
border: '2px solid #1890ff',
boxShadow: '0 0 10px rgba(24, 144, 255, 0.5)',
zIndex: 10
}
};
// Add the new table to the nodes, ensuring we don't duplicate
setNodes(nodes => {
// Check if a node with this ID already exists
const existingNodeIndex = nodes.findIndex(node => node.id === tableNode.id);
if (existingNodeIndex >= 0) {
console.log(`Node with ID ${tableNode.id} already exists, updating it`);
// Replace the existing node
const updatedNodes = [...nodes];
updatedNodes[existingNodeIndex] = tableNode;
return updatedNodes;
} else {
console.log(`Adding new node with ID ${tableNode.id}`);
// Add the new node
return [...nodes, tableNode];
}
});
// Create columns if provided
if (tableData.columns && tableData.columns.length > 0) {
console.log(`Creating ${tableData.columns.length} columns for table ${newTable.name}:`, tableData.columns);
// Filter out empty columns
const validColumns = tableData.columns.filter(col => col.name && col.name.trim() !== '');
if (validColumns.length > 0) {
console.log(`Creating ${validColumns.length} valid columns for table ${newTable.name}`);
// Create each column
for (const column of validColumns) {
try {
console.log(`Creating column ${column.name} with type ${column.data_type}`);
await createColumn(
newTable.slug,
tableData.sch,
column.name,
column.data_type,
"", // description
"", // alias
0, // is_key
tableData.con
);
console.log(`Column ${column.name} created successfully`);
} catch (columnError) {
console.error(`Error creating column ${column.name}:`, columnError);
}
}
// Update the table node with the new columns
setNodes(nodes => {
return nodes.map(node => {
if (node.id === `table-${newTable.slug}`) {
// Get column names
const columnNames = validColumns.map(col => col.name);
console.log(`Updating table node with columns:`, columnNames);
return {
...node,
data: {
...node.data,
columns: columnNames
}
};
}
return node;
});
});
}
}
// Instead of refreshing data immediately (which might overwrite our new node),
// we'll use a timeout to allow the UI to update first
setTimeout(() => {
console.log('Refreshing data after table creation');
refreshData();
}, 500);
// Show success message
alert(`Table "${tableData.name}" created successfully!`);
// Close the popup
setShowTablePopup(false);
setSelectedSchemaForTable(null);
} catch (error) {
console.error('Error creating table:', error);
alert(`Error creating table: ${error.message}`);
}
};
// Function to handle column creation
const handleCreateColumn = async (columnData) => {
try {
console.log('Creating new column with data:', columnData);
// Call the createColumn function with the provided data
const newColumn = await createColumn(
columnData.tbl,
columnData.sch,
columnData.name,
columnData.data_type,
columnData.description,
columnData.alias,
columnData.is_key,
columnData.con
);
console.log('Column created successfully:', newColumn);
// Add the new column to the table columns
setTableColumns(prevColumns => [...prevColumns, newColumn]);
// Refresh the data to show the new column
refreshData();
// Show success message
alert(`Column "${columnData.name}" created successfully!`);
} catch (error) {
console.error('Error creating column:', error);
alert(`Error creating column: ${error.message}`);
}
};
// Function to handle column update
const handleUpdateColumn = async (columnData) => {
try {
console.log('Updating column with data:', columnData);
// Call the updateColumn function with the provided data
const updatedColumn = await updateColumn(
columnData.col,
columnData.tbl,
columnData.sch,
columnData.name,
columnData.data_type,
columnData.description,
columnData.alias,
columnData.is_key,
columnData.con
);
console.log('Column updated successfully:', updatedColumn);
// Refresh the data to show the updated column
refreshData();
// Show success message
alert(`Column "${columnData.name}" updated successfully!`);
} catch (error) {
console.error('Error updating column:', error);
alert(`Error updating column: ${error.message}`);
}
};
// Function to handle column deletion
const handleDeleteColumn = async (columnData) => {
try {
console.log('Deleting column with data:', columnData);
// Get the column identifier (could be in col or slug property)
const columnId = columnData.col || columnData.slug;
// Validate column data
if (!columnId) {
console.error('Column identifier is missing in:', columnData);
throw new Error('Column identifier is required');
}
console.log(`Using column identifier: ${columnId}`);
// Call the deleteColumn function with the provided data
await deleteColumn(
columnId,
columnData.tbl,
columnData.sch,
columnData.con
);
console.log('Column deleted successfully');
// Refresh the data to show the changes
refreshData();
// Show success message
alert('Column deleted successfully!');
} catch (error) {
console.error('Error deleting column:', error);
alert(`Error deleting column: ${error.message}`);
}
};
// Function to handle table update
const handleUpdateTable = async (tableData) => {
try {
console.log('Updating table with data:', tableData);
// Validate table data
if (!tableData.tbl) {
throw new Error('Table identifier is required');
}
if (!tableData.name) {
throw new Error('Table name is required');
}
if (!tableData.table_type) {
throw new Error('Table type is required');
}
// Call the updateTable function with the provided data
await updateTable(tableData);
console.log('Table updated successfully');
// Refresh the data to show the updated table
refreshData();
// Show success message
alert(`Table "${tableData.name}" updated successfully!`);
// Close the popup
setShowTableUpdatePopup(false);
setSelectedTableForUpdate(null);
} catch (error) {
console.error('Error updating table:', error);
// Check if the error is actually a success message
if (error.message && error.message.toLowerCase() === 'success') {
console.log('Table update completed with success message');
// Consider it a success and close the popup
setShowTableUpdatePopup(false);
setSelectedTableForUpdate(null);
// Refresh data to show updated table
refreshData();
} else {
// Real error - show alert but don't close popup
alert(`Error updating table: ${error.message}`);
}
}
};
// Function to handle table deletion
const handleDeleteTable = async (tableData) => {
try {
console.log('Deleting table with data:', tableData);
// Validate table data
if (!tableData.tbl) {
throw new Error('Table identifier is required');
}
if (!tableData.sch) {
throw new Error('Schema identifier is required');
}
// IMPORTANT: Always ensure we have a database identifier
// Try multiple sources in order of preference
if (!tableData.con) {
// 1. Try to get it from the selected database
if (selectedDatabase && selectedDatabase.slug) {
console.log(`Using selected database slug: ${selectedDatabase.slug}`);
tableData.con = selectedDatabase.slug;
}
// 2. Try to get it from the dbSlug prop
else if (dbSlug) {
console.log(`Using dbSlug prop: ${dbSlug}`);
tableData.con = dbSlug;
}
// 3. Try to get it from the global currentDbSlug
else if (typeof window.getCurrentDbSlug === 'function') {
const currentDbSlug = window.getCurrentDbSlug();
if (currentDbSlug) {
console.log(`Using global currentDbSlug: ${currentDbSlug}`);
tableData.con = currentDbSlug;
} else {
throw new Error('Database identifier is required');
}
} else {
throw new Error('Database identifier is required');
}
}
// Log the final table data being sent to the API
console.log('Final table data for deletion:', tableData);
// Call the deleteTable function with the provided data
// The deleteTable function now always uses force: true
await deleteTable(tableData);
console.log('Table deleted successfully');
// Remove the table node from the nodes array
setNodes(nodes => nodes.filter(node => {
if (node.type === 'table') {
return node.data.slug !== tableData.tbl;
}
return true;
}));
// Refresh the data to show the changes
refreshData();
// Show success message
alert('Table deleted successfully!');
} catch (error) {
console.error('Error in handleDeleteTable:', error);
alert(`Error deleting table: ${error.message}`);
}
};
// Function to handle schema creation
const handleCreateSchema = async () => {
if (!newSchemaName.trim()) {
setSchemaCreationError('Schema name is required');
return;
}
if (!dbSlug) {
setSchemaCreationError('Database slug is required to create a schema');
return;
}
setIsCreatingSchema(true);
setSchemaCreationError(null);
try {
console.log(`Creating schema "${newSchemaName}" in database with slug: ${dbSlug}`);
// Call the createSchema function with the current database slug
const newSchema = await createSchema(
dbSlug,
newSchemaName.trim(),
newSchemaDescription.trim()
);
console.log('Schema created successfully:', newSchema);
// Reset form fields
setNewSchemaName('');
setNewSchemaDescription('');
// Close the popup
setShowSchemaPopup(false);
// Refresh the data to show the new schema
if (typeof refreshData === 'function') {
console.log('Refreshing data to show the new schema...');
refreshData();
} else {
console.log('No refreshData function available, reloading page...');
// If refreshData is not available, reload the page
window.location.reload();
}
} catch (error) {
console.error('Error creating schema:', error);
setSchemaCreationError(error.message || 'Failed to create schema');
} finally {
setIsCreatingSchema(false);
}
};
// Get canvas boundaries for process placement
const getSchemasBoundaries = () => {
// Calculate based on existing nodes
let minX = 100;
let minY = 100;
let maxX = 500;
let maxY = 500;
if (nodes.length > 0) {
minX = Math.min(...nodes.map(node => node.position.x));
minY = Math.min(...nodes.map(node => node.position.y));
maxX = Math.max(...nodes.map(node => node.position.x + 200)); // Assuming node width is ~200px
maxY = Math.max(...nodes.map(node => node.position.y + 200)); // Assuming node height is ~200px
}
return { minX, minY, maxX, maxY };
};
// We don't need the moveSchemaNodes function anymore
// Open the process form to create a new process
const addProcessNode = () => {
setSelectedProcessForEdit(null);
setShowProcessForm(true);
};
// Handle saving a new process from the form
const handleSaveProcess = (processData) => {
const { minX, minY, maxX, maxY } = getSchemasBoundaries();
// Place the new process outside of all schemas
// Either to the right of all schemas or below all schemas
let newX, newY;
// Get the current viewport to help position the new node in visible area
const viewport = reactFlowInstance ? reactFlowInstance.getViewport() : { x: 0, y: 0, zoom: 1 };
// Determine placement position - right side is preferred
newX = maxX + 200;
newY = minY + (maxY - minY) / 2;
// If we're editing an existing process, find it and update it
if (selectedProcessForEdit) {
const updatedNodes = nodes.map(node => {
if (node.id === selectedProcessForEdit.slug) {
return {
...node,
data: {
...node.data,
label: processData.name,
description: processData.description,
type: processData.type,
status: processData.status,
mappings: processData.mappings,
filters: processData.filters,
aggregations: processData.aggregations
}
};
}
return node;
});
setNodes(updatedNodes);
// Update edges if source or destination tables have changed
const processes = apiData?.processes || mockApiData.processes;
const oldProcess = processes.find(p => p.slug === selectedProcessForEdit.slug);
// Remove old edges
if (oldProcess) {
const edgesToRemove = edges.filter(edge =>
edge.source === oldProcess.slug ||
edge.target === oldProcess.slug
);
if (edgesToRemove.length > 0) {
const remainingEdges = edges.filter(edge =>
!edgesToRemove.some(e => e.id === edge.id)
);
setEdges(remainingEdges);
}
}
// Create new edges
const newEdges = [];
// Determine if process is active or inactive
const isActive = processData.status === 'active';
// Set colors and animation based on process status
const sourceColor = isActive ? '#52c41a' : '#aaaaaa'; // Green or gray
const destColor = isActive ? '#1890ff' : '#aaaaaa'; // Blue or gray
const animated = isActive; // Only animate if active
const strokeWidth = isActive ? 2 : 1.5; // Slightly thinner if inactive
const opacity = isActive ? 1 : 0.7; // Slightly transparent if inactive
// Create edges from source tables to process
processData.source_table.forEach(sourceId => {
newEdges.push({
id: `e-${sourceId}-${processData.slug}`,
source: sourceId,
target: processData.slug,
type: 'custom',
animated: animated,
style: {
stroke: sourceColor,
strokeWidth: strokeWidth,
opacity: opacity
},
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: sourceColor,
},
data: {
label: processData.name,
isActive: isActive
}
});
});
// Create edges from process to destination tables
processData.destination_table.forEach(destId => {
newEdges.push({
id: `e-${processData.slug}-${destId}`,
source: processData.slug,
target: destId,
type: 'custom',
animated: animated,
style: {
stroke: destColor,
strokeWidth: strokeWidth,
opacity: opacity
},
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: destColor,
},
data: {
label: processData.name,
isActive: isActive
}
});
});
setEdges(eds => [...eds, ...newEdges]);
// Update the data in memory
// Note: In a real application, you would make an API call to update the data on the server
// For now, we'll just update the mockApiData for consistency
const processIndex = mockApiData.processes.findIndex(p => p.slug === processData.slug);
if (processIndex !== -1) {
mockApiData.processes[processIndex] = processData;
}
// If we have API data, update it too
if (apiData && apiData.processes) {
const apiProcessIndex = apiData.processes.findIndex(p => p.slug === processData.slug);
if (apiProcessIndex !== -1) {
apiData.processes[apiProcessIndex] = processData;
}
}
} else {
// Create a new process node
const newProcess = {
id: processData.slug,
type: 'process',
data: {
label: processData.name,
description: processData.description,
type: processData.type,
status: processData.status,
mappings: processData.mappings,
filters: processData.filters,
aggregations: processData.aggregations
},
position: { x: newX, y: newY },
};
setNodes(nodes => [...nodes, newProcess]);
// Create edges
const newEdges = [];
// Determine if process is active or inactive
const isActive = processData.status === 'active';
// Set colors and animation based on process status
const sourceColor = isActive ? '#52c41a' : '#aaaaaa'; // Green or gray
const destColor = isActive ? '#1890ff' : '#aaaaaa'; // Blue or gray
const animated = isActive; // Only animate if active
const strokeWidth = isActive ? 2 : 1.5; // Slightly thinner if inactive
const opacity = isActive ? 1 : 0.7; // Slightly transparent if inactive
// Create edges from source tables to process
processData.source_table.forEach(sourceId => {
newEdges.push({
id: `e-${sourceId}-${processData.slug}`,
source: sourceId,
target: processData.slug,
type: 'custom',
animated: animated,
style: {
stroke: sourceColor,
strokeWidth: strokeWidth,
opacity: opacity
},
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: sourceColor,
},
data: {
label: processData.name,
isActive: isActive
}
});
});
// Create edges from process to destination tables
processData.destination_table.forEach(destId => {
newEdges.push({
id: `e-${processData.slug}-${destId}`,
source: processData.slug,
target: destId,
type: 'custom',
animated: animated,
style: {
stroke: destColor,
strokeWidth: strokeWidth,
opacity: opacity
},
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: destColor,
},
data: {
label: processData.name,
isActive: isActive
}
});
});
setEdges(eds => [...eds, ...newEdges]);
// Add to mock data
mockApiData.processes.push(processData);
// If we have API data, update it too
if (apiData && apiData.processes) {
apiData.processes.push(processData);
}
}
};
// Removed schema-related functions
// Handle dragging tables into schemas
const onDragOver = useCallback((event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
const onDrop = useCallback((event) => {
event.preventDefault();
// Get the node ID from the data transfer
const nodeId = event.dataTransfer.getData('application/reactflow');
if (!nodeId || !reactFlowInstance) {
return;
}
// Get the drop position
const position = reactFlowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
// Find the node being dragged
const draggedNode = nodes.find(node => node.id === nodeId);
if (!draggedNode) {
return;
}
// Check if the drop position is within any schema
const schemas = apiData?.schemas || mockApiData.schemas;
let targetSchema = null;
for (const schema of schemas) {
const schemaNode = nodes.find(node => node.id === `schema-bg-${schema.slug}`);
if (schemaNode) {
const { x, y } = schemaNode.position;
const { width, height } = schemaNode.style;
// Check if the drop position is within this schema
if (
position.x >= x &&
position.x <= x + width &&
position.y >= y &&
position.y <= y + height
) {
targetSchema = schema;
break;
}
}
}
// Update the node with the new position and parent schema
const updatedNodes = nodes.map(node => {
if (node.id === nodeId) {
return {
...node,
position,
parentNode: targetSchema ? `schema-bg-${targetSchema.slug}` : undefined,
extent: targetSchema ? 'parent' : undefined,
// Remove the draggable styling
style: {
...node.style,
border: null,
boxShadow: null
},
className: 'table-node'
};
}
return node;
});
setNodes(updatedNodes);
}, [nodes, reactFlowInstance]);
// Add the custom CSS to the document
useEffect(() => {
// Create a style element
const styleElement = document.createElement('style');
styleElement.innerHTML = generateCustomStyles();
document.head.appendChild(styleElement);
// Clean up on unmount
return () => {
document.head.removeChild(styleElement);
};
}, []);
// Create a style element with our custom styles
const customStyles = useMemo(() => generateCustomStyles(), []);
// Render the component
return (
{/* Show loading state if data is still loading */}
{loading ? (
) : (
<>
{/* Database Header */}
{selectedDatabase && (
{selectedDatabase.name} Database
{/*
Data Flow View
*/}
)}
{
// Only handle table node drag events
if (node.type === 'table' && node.data) {
console.log('Table dragged to new position:', node.position);
console.log('Table data:', node.data);
// Send the new position to the API
if (node.data.slug && node.data.schema) {
try {
console.log('Updating table position through qbt_table_update endpoint');
// Prepare table data for update with config structure
const tableData = {
tbl: node.data.slug,
sch: node.data.schema,
con: node.data.database || dbSlug, // Use node's database or current dbSlug
name: node.data.label,
table_type: node.data.type || 'stage', // Default to stage if not specified
// Use config object with orientation nested inside
config: {
orientation: node.position
}
};
// Call the regular updateTable function
updateTable(tableData)
.then(result => {
console.log('Table position updated successfully through regular update:', result);
})
.catch(error => {
console.error('Failed to update table position:', error);
});
} catch (error) {
console.error('Error preparing table update:', error);
}
} else {
console.error('Missing data for table position update:', node.data);
}
}
}}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
minZoom={0.05}
maxZoom={8}
defaultViewport={{ x: 0, y: 0, zoom: 0.4 }}
fitView
fitViewOptions={{ padding: 1.0, maxZoom: 0.4 }}
style={{ background: '#ffffff' }}
className={isConnectionMode ? 'connection-mode' : ''}
connectionMode="loose"
connectionLineStyle={{
stroke: connectionType === 'reference' ? '#00a99d' :
connectionType === 'dependency' ? '#ff4d4f' : '#52c41a',
strokeWidth: 3,
strokeDasharray: '5,5'
}}
connectionLineType="bezier"
defaultMarkerColor="#00a99d"
>
{/* Empty Database Message */}
{nodes.length === 0 && selectedDatabase && (
{selectedDatabase.name} Database is Empty
{selectedDatabase.slug === 'my_dwh2' ?
"This database (service 2) doesn't contain any schemas or tables. This is expected behavior." :
"This database doesn't have any tables or data flows yet. Start by adding tables and processes to visualize your data flow."
}
{/* Process Form */}
setShowProcessForm(false)}
onSave={handleSaveProcess}
tables={apiData?.tables || mockApiData.tables}
existingProcess={selectedProcessForEdit}
/>
{/* Table Creation Popup */}
{showTablePopup && selectedSchemaForTable && (
{
setShowTablePopup(false);
setSelectedSchemaForTable(null);
}}
onCreateTable={handleCreateTable}
schemaInfo={selectedSchemaForTable}
/>
)}
{/* Make the showTableCreationPopup function globally available */}
{/* This is a hack to ensure the function is available globally */}
{(() => {
// Define the function on the window object
window.showTableCreationPopup = (schemaInfo) => {
console.log('Global showTableCreationPopup called with:', schemaInfo);
setSelectedSchemaForTable(schemaInfo);
setShowTablePopup(true);
};
return null;
})()}