4109 lines
140 KiB
JavaScript
4109 lines
140 KiB
JavaScript
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||
import ReactFlow, {
|
||
MiniMap,
|
||
Controls,
|
||
Background,
|
||
useNodesState,
|
||
useEdgesState,
|
||
addEdge,
|
||
Panel,
|
||
useReactFlow,
|
||
ReactFlowProvider,
|
||
Handle,
|
||
Position,
|
||
BaseEdge,
|
||
EdgeLabelRenderer,
|
||
getBezierPath
|
||
} from 'reactflow';
|
||
import 'reactflow/dist/style.css';
|
||
|
||
// Import React Flow components
|
||
|
||
// Add custom CSS for React Flow
|
||
const generateCustomStyles = () => {
|
||
return `
|
||
.react-flow__node {
|
||
z-index: 1;
|
||
margin: 20px; /* Add margin to all nodes */
|
||
}
|
||
|
||
/* Ensure tables don't overlap */
|
||
.react-flow__node.table-node {
|
||
margin: 30px;
|
||
min-height: 120px;
|
||
}
|
||
|
||
.schema-background-node {
|
||
z-index: -1;
|
||
pointer-events: all;
|
||
border-radius: 25px;
|
||
cursor: grab;
|
||
user-select: none;
|
||
transition: all 0.2s ease;
|
||
padding: 30px; /* Add padding inside schema */
|
||
}
|
||
|
||
.schema-background-node:hover {
|
||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.schema-background-node:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.schema-background-node.dragging {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.schema-label {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 20px;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
padding: 10px 15px;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
z-index: 10;
|
||
}
|
||
|
||
/* Ensure proper spacing between nodes */
|
||
.react-flow__node-table {
|
||
margin-bottom: 30px !important;
|
||
margin-right: 30px !important;
|
||
}
|
||
|
||
/* Styling for draggable new tables */
|
||
.draggable-new-table {
|
||
cursor: grab !important;
|
||
transition: all 0.2s ease !important;
|
||
z-index: 10 !important;
|
||
}
|
||
|
||
.draggable-new-table:hover {
|
||
transform: scale(1.02) !important;
|
||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15) !important;
|
||
}
|
||
|
||
.draggable-new-table:active {
|
||
cursor: grabbing !important;
|
||
}
|
||
|
||
/* Process node styling */
|
||
.react-flow__node-process {
|
||
z-index: 5 !important;
|
||
background: white !important;
|
||
border-radius: 8px !important;
|
||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
|
||
}
|
||
|
||
/* Edge styling for left-to-right data flow */
|
||
.react-flow__edge {
|
||
z-index: 4 !important;
|
||
}
|
||
|
||
.react-flow__edge-path {
|
||
stroke-width: 1.5 !important;
|
||
}
|
||
|
||
/* Encourage horizontal edges for left-to-right flow */
|
||
.react-flow__edge-path {
|
||
--react-flow-bezier-edge-control-point-distance: 60 !important;
|
||
}
|
||
|
||
/* Edge label styling */
|
||
.react-flow__edge-textbg {
|
||
background-color: white !important;
|
||
padding: 2px !important;
|
||
border-radius: 4px !important;
|
||
}
|
||
|
||
.react-flow__edge-text {
|
||
font-size: 8px !important;
|
||
}
|
||
`;
|
||
};
|
||
|
||
// Import icons from react-icons
|
||
import { FaTable, FaArrowRight, FaExchangeAlt, FaFilter, FaCode, FaSync, FaCalculator, FaChartLine, FaEdit, FaTrash, FaChevronDown } from 'react-icons/fa';
|
||
import { BiSolidData, BiTransfer } from 'react-icons/bi';
|
||
import { AiFillFolder } from 'react-icons/ai';
|
||
import { TbTransform } from 'react-icons/tb';
|
||
import { CustomDatabaseIcon, CustomDocumentIcon, CustomDimensionIcon, CustomProcessIcon, CustomSchemaIcon } from './CustomIcons';
|
||
|
||
// Import Material-UI components for breadcrumb
|
||
import { Breadcrumbs, Link, Typography, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material';
|
||
|
||
// Import API data
|
||
import mockApiData, { useApiData, createSchema, deleteSchema, updateSchema, createTable, updateTable, deleteTable, createColumn, updateColumn, deleteColumn, fetchColumns } from './mockData';
|
||
|
||
// Import components
|
||
import ProcessForm from './ProcessForm';
|
||
import TableCreationPopup from './TableCreationPopup';
|
||
import ColumnManagementPopup from './ColumnManagementPopup';
|
||
import TableUpdatePopup from './TableUpdatePopup';
|
||
|
||
// Schema Background Node
|
||
const SchemaBackgroundNode = ({ data }) => {
|
||
// Different background colors for each schema
|
||
const bgColor = data.slug === 'edw_schema'
|
||
? 'rgba(24, 144, 255, 0.1)' // Light blue for Schema_1
|
||
: 'rgba(82, 196, 26, 0.1)'; // Light green for Schema_2
|
||
|
||
// Border color based on schema
|
||
const borderColor = data.slug === 'edw_schema'
|
||
? '#1890ff' // Blue for Schema_1
|
||
: '#52c41a'; // Green for Schema_2
|
||
|
||
// State for delete confirmation
|
||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||
const [isDeleting, setIsDeleting] = useState(false);
|
||
const [deleteError, setDeleteError] = useState(null);
|
||
|
||
// State for schema update popup
|
||
const [showUpdatePopup, setShowUpdatePopup] = useState(false);
|
||
const [updatedSchemaName, setUpdatedSchemaName] = useState(data.name || '');
|
||
const [updatedSchemaDescription, setUpdatedSchemaDescription] = useState(data.description || '');
|
||
const [isUpdatingSchema, setIsUpdatingSchema] = useState(false);
|
||
const [schemaUpdateError, setSchemaUpdateError] = useState(null);
|
||
|
||
// Function to handle schema deletion
|
||
const handleDeleteSchema = async (e) => {
|
||
e.stopPropagation(); // Prevent event bubbling
|
||
|
||
if (!data.database || !data.slug) {
|
||
setDeleteError('Missing database or schema information');
|
||
return;
|
||
}
|
||
|
||
setIsDeleting(true);
|
||
setDeleteError(null);
|
||
|
||
try {
|
||
console.log(`Deleting schema "${data.name}" (${data.slug}) from database ${data.database}`);
|
||
|
||
// Call the deleteSchema function
|
||
await deleteSchema(data.database, data.slug);
|
||
|
||
console.log('Schema deleted successfully');
|
||
|
||
// Close the confirmation dialog
|
||
setShowDeleteConfirm(false);
|
||
|
||
// Refresh the data to update the UI
|
||
// This assumes window.refreshCanvasData is set in the parent component
|
||
if (typeof window.refreshCanvasData === 'function') {
|
||
window.refreshCanvasData();
|
||
} else {
|
||
// If the refresh function is not available, reload the page
|
||
window.location.reload();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting schema:', error);
|
||
setDeleteError(error.message || 'Failed to delete schema');
|
||
} finally {
|
||
setIsDeleting(false);
|
||
}
|
||
};
|
||
|
||
// Function to handle schema update
|
||
const handleUpdateSchema = async (e) => {
|
||
e.stopPropagation(); // Prevent event bubbling
|
||
|
||
if (!data || !data.database || !data.slug) {
|
||
setSchemaUpdateError('Missing database or schema information');
|
||
return;
|
||
}
|
||
|
||
if (!updatedSchemaName.trim()) {
|
||
setSchemaUpdateError('Schema name is required');
|
||
return;
|
||
}
|
||
|
||
setIsUpdatingSchema(true);
|
||
setSchemaUpdateError(null);
|
||
|
||
// Store the original values for potential rollback
|
||
const originalName = data.name;
|
||
const originalDescription = data.description;
|
||
|
||
try {
|
||
console.log(`Updating schema "${data.name}" (${data.slug}) in database ${data.database}`);
|
||
|
||
// Close the update popup immediately for better UX
|
||
setShowUpdatePopup(false);
|
||
|
||
// Call the updateSchema function
|
||
const updatedSchema = await updateSchema(
|
||
data.database,
|
||
data.slug,
|
||
updatedSchemaName.trim(),
|
||
updatedSchemaDescription.trim()
|
||
);
|
||
|
||
console.log('Schema updated successfully:', updatedSchema);
|
||
|
||
// Update the node data directly to reflect changes immediately
|
||
// This provides immediate visual feedback after the API call
|
||
if (data) {
|
||
data.name = updatedSchemaName.trim();
|
||
data.description = updatedSchemaDescription.trim();
|
||
}
|
||
|
||
// Notify any listeners about the schema update
|
||
if (typeof window.onSchemaUpdated === 'function') {
|
||
try {
|
||
window.onSchemaUpdated(updatedSchema);
|
||
} catch (callbackError) {
|
||
console.error('Error in onSchemaUpdated callback:', callbackError);
|
||
}
|
||
}
|
||
|
||
// Refresh the data to update the UI
|
||
if (typeof window.refreshCanvasData === 'function') {
|
||
console.log('Calling window.refreshCanvasData to update the UI');
|
||
|
||
try {
|
||
// Call refreshCanvasData directly - no need for setTimeout
|
||
window.refreshCanvasData();
|
||
} catch (refreshError) {
|
||
console.error('Error calling refreshCanvasData:', refreshError);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating schema:', error);
|
||
setSchemaUpdateError(error.message || 'Failed to update schema');
|
||
|
||
// Re-open the popup to show the error
|
||
setShowUpdatePopup(true);
|
||
|
||
// Revert the data changes if there was an error
|
||
if (data) {
|
||
data.name = originalName;
|
||
data.description = originalDescription;
|
||
}
|
||
} finally {
|
||
setIsUpdatingSchema(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className="schema-background-node"
|
||
style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
background: bgColor,
|
||
border: `3px dashed ${data.color || borderColor}`,
|
||
boxShadow: `0 0 20px rgba(0, 0, 0, 0.05)`,
|
||
borderRadius: '25px', // Slightly larger border radius
|
||
position: 'relative' // Added for absolute positioning of delete button
|
||
}}
|
||
>
|
||
<div className="schema-label" style={{
|
||
color: data.color || borderColor,
|
||
fontSize: '14px', // Slightly larger font
|
||
padding: '8px 12px', // More padding
|
||
background: 'rgba(255, 255, 255, 0.9)', // More opaque background
|
||
boxShadow: '0 3px 6px rgba(0, 0, 0, 0.1)', // Stronger shadow
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '8px',
|
||
justifyContent: 'space-between', // Added to position buttons
|
||
width: 'auto', // Allow width to adjust to content
|
||
minWidth: '200px' // Ensure minimum width
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||
<CustomSchemaIcon width="24" height="22" />
|
||
{data.name}
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: '8px' }}>
|
||
{/* Add Table button */}
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
|
||
console.log('Add Table button clicked');
|
||
console.log('Schema data:', data);
|
||
|
||
// Create schema info object
|
||
const schemaInfo = {
|
||
sch: data.slug,
|
||
con: data.database
|
||
};
|
||
|
||
// Try multiple methods to show the table creation popup
|
||
console.log('Trying to show table creation popup...');
|
||
|
||
// Method 1: Use the window.showTableCreationPopup function
|
||
if (typeof window.showTableCreationPopup === 'function') {
|
||
console.log('Method 1: Using window.showTableCreationPopup');
|
||
window.showTableCreationPopup(schemaInfo);
|
||
return;
|
||
}
|
||
|
||
// Method 2: Use the API object
|
||
if (window.dataflowCanvasApi && typeof window.dataflowCanvasApi.showTableCreationPopup === 'function') {
|
||
console.log('Method 2: Using window.dataflowCanvasApi.showTableCreationPopup');
|
||
window.dataflowCanvasApi.showTableCreationPopup(schemaInfo);
|
||
return;
|
||
}
|
||
|
||
// Method 3: Use the state setters directly from the API object
|
||
if (window.dataflowCanvasApi &&
|
||
typeof window.dataflowCanvasApi.setSelectedSchemaForTable === 'function' &&
|
||
typeof window.dataflowCanvasApi.setShowTablePopup === 'function') {
|
||
console.log('Method 3: Using state setters from API object');
|
||
window.dataflowCanvasApi.setSelectedSchemaForTable(schemaInfo);
|
||
window.dataflowCanvasApi.setShowTablePopup(true);
|
||
return;
|
||
}
|
||
|
||
// If all methods fail, show an error
|
||
console.error('All methods to show table creation popup failed');
|
||
alert('Unable to create a new table at this time. Please try again later.');
|
||
}}
|
||
style={{
|
||
background: 'none',
|
||
border: 'none',
|
||
color: '#52c41a',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
padding: '4px 8px',
|
||
borderRadius: '4px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '4px',
|
||
transition: 'all 0.2s ease'
|
||
}}
|
||
title="Add a new table to this schema"
|
||
>
|
||
<FaTable size={14} />
|
||
Add Table
|
||
</button>
|
||
|
||
{/* Update button */}
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
|
||
// Set the form values from the current data
|
||
if (data) {
|
||
setUpdatedSchemaName(data.name || '');
|
||
setUpdatedSchemaDescription(data.description || '');
|
||
}
|
||
|
||
setShowUpdatePopup(true);
|
||
}}
|
||
style={{
|
||
background: 'none',
|
||
border: 'none',
|
||
color: '#1890ff',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
padding: '4px 8px',
|
||
borderRadius: '4px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '4px',
|
||
transition: 'all 0.2s ease'
|
||
}}
|
||
title="Update this schema"
|
||
>
|
||
<span style={{ fontSize: '16px' }}>✎</span>
|
||
Update
|
||
</button>
|
||
|
||
{/* Delete button */}
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowDeleteConfirm(true);
|
||
}}
|
||
style={{
|
||
background: 'none',
|
||
border: 'none',
|
||
color: '#ff4d4f',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
padding: '4px 8px',
|
||
borderRadius: '4px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '4px',
|
||
transition: 'all 0.2s ease'
|
||
}}
|
||
title="Delete this schema"
|
||
>
|
||
<span style={{ fontSize: '16px' }}>×</span>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Schema Update Popup */}
|
||
{showUpdatePopup && (
|
||
<div
|
||
style={{
|
||
position: 'fixed',
|
||
top: '50%',
|
||
left: '50%',
|
||
transform: 'translate(-50%, -50%)',
|
||
zIndex: 1000,
|
||
background: '#ffffff',
|
||
border: '2px solid #1890ff',
|
||
borderRadius: '8px',
|
||
padding: '20px',
|
||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.2)',
|
||
width: '400px',
|
||
maxWidth: '90vw'
|
||
}}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginBottom: '15px',
|
||
borderBottom: '1px solid #1890ff',
|
||
paddingBottom: '10px'
|
||
}}>
|
||
<div>
|
||
<h3 style={{ margin: 0, color: '#1890ff', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||
<CustomSchemaIcon width="24" height="24" />
|
||
Update Schema
|
||
</h3>
|
||
<p style={{ margin: '5px 0 0 0', fontSize: '12px', color: '#666' }}>
|
||
Database: <strong>{data.database || 'Unknown'}</strong>
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowUpdatePopup(false)}
|
||
style={{
|
||
background: 'none',
|
||
border: 'none',
|
||
fontSize: '18px',
|
||
cursor: 'pointer',
|
||
color: '#999'
|
||
}}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
{schemaUpdateError && (
|
||
<div style={{
|
||
padding: '10px',
|
||
marginBottom: '15px',
|
||
background: '#fff2f0',
|
||
border: '1px solid #ffccc7',
|
||
borderRadius: '4px',
|
||
color: '#ff4d4f'
|
||
}}>
|
||
{schemaUpdateError}
|
||
</div>
|
||
)}
|
||
|
||
<div style={{ marginBottom: '15px' }}>
|
||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
|
||
Schema Name *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={updatedSchemaName}
|
||
onChange={(e) => setUpdatedSchemaName(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '8px',
|
||
border: '1px solid #d9d9d9',
|
||
borderRadius: '4px',
|
||
fontSize: '14px'
|
||
}}
|
||
placeholder="Enter schema name"
|
||
disabled={isUpdatingSchema}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '20px' }}>
|
||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
|
||
Description
|
||
</label>
|
||
<textarea
|
||
value={updatedSchemaDescription}
|
||
onChange={(e) => setUpdatedSchemaDescription(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '8px',
|
||
border: '1px solid #d9d9d9',
|
||
borderRadius: '4px',
|
||
fontSize: '14px',
|
||
minHeight: '80px',
|
||
resize: 'vertical'
|
||
}}
|
||
placeholder="Enter schema description (optional)"
|
||
disabled={isUpdatingSchema}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}>
|
||
<button
|
||
onClick={() => setShowUpdatePopup(false)}
|
||
style={{
|
||
padding: '8px 16px',
|
||
background: '#f0f0f0',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px'
|
||
}}
|
||
disabled={isUpdatingSchema}
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={handleUpdateSchema}
|
||
style={{
|
||
padding: '8px 16px',
|
||
background: '#1890ff',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '5px'
|
||
}}
|
||
disabled={isUpdatingSchema}
|
||
>
|
||
{isUpdatingSchema ? 'Updating...' : 'Update Schema'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Delete confirmation dialog */}
|
||
{showDeleteConfirm && (
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
top: '60px',
|
||
left: '20px',
|
||
background: 'white',
|
||
padding: '15px',
|
||
borderRadius: '8px',
|
||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||
zIndex: 100,
|
||
width: '280px',
|
||
border: '1px solid #ff4d4f'
|
||
}}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<h4 style={{ margin: '0 0 10px 0', color: '#ff4d4f' }}>Delete Schema?</h4>
|
||
|
||
<p style={{ margin: '0 0 15px 0', fontSize: '14px' }}>
|
||
Are you sure you want to delete the schema "{data.name}"? This action cannot be undone.
|
||
</p>
|
||
|
||
{deleteError && (
|
||
<div style={{
|
||
padding: '8px',
|
||
marginBottom: '10px',
|
||
background: '#fff2f0',
|
||
border: '1px solid #ffccc7',
|
||
borderRadius: '4px',
|
||
color: '#ff4d4f',
|
||
fontSize: '12px'
|
||
}}>
|
||
{deleteError}
|
||
</div>
|
||
)}
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowDeleteConfirm(false);
|
||
}}
|
||
style={{
|
||
padding: '5px 10px',
|
||
background: '#f0f0f0',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '12px'
|
||
}}
|
||
disabled={isDeleting}
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={handleDeleteSchema}
|
||
style={{
|
||
padding: '5px 10px',
|
||
background: '#ff4d4f',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '12px'
|
||
}}
|
||
disabled={isDeleting}
|
||
>
|
||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 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 (
|
||
<>
|
||
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
|
||
<EdgeLabelRenderer>
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||
fontSize: 10, // Reduced font size
|
||
pointerEvents: 'all',
|
||
backgroundColor: 'rgba(255, 255, 255, 0.9)', // Added semi-transparent background
|
||
padding: '3px 6px',
|
||
borderRadius: '6px',
|
||
color: isActive ? '#333' : '#666',
|
||
fontWeight: isActive ? 500 : 400,
|
||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)', // Added subtle shadow
|
||
border: `1px solid ${style.stroke || '#ccc'}`,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
gap: '2px',
|
||
maxWidth: '150px', // Reduced max width
|
||
opacity: labelOpacity,
|
||
zIndex: 4 // Lower z-index to prevent overlapping with nodes
|
||
}}
|
||
className="nodrag nopan"
|
||
>
|
||
{/* Process name in small text */}
|
||
<div style={{
|
||
fontSize: '8px',
|
||
color: '#666',
|
||
fontStyle: 'italic',
|
||
marginBottom: '2px',
|
||
textAlign: 'center'
|
||
}}>
|
||
{processName}
|
||
{!isActive && (
|
||
<span style={{
|
||
fontSize: '7px',
|
||
background: '#f0f0f0',
|
||
padding: '0px 3px',
|
||
borderRadius: '3px',
|
||
color: '#999',
|
||
marginLeft: '2px',
|
||
animation: "none !important"
|
||
}}>
|
||
inactive
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Column mappings - show fewer columns to save space */}
|
||
<div style={{
|
||
display: 'flex',
|
||
flexWrap: 'wrap',
|
||
justifyContent: 'center',
|
||
gap: '2px'
|
||
}}>
|
||
{mappings.length > 0 ? (
|
||
// Only show up to 3 mappings to save space
|
||
mappings.slice(0, 3).map((column, index) => (
|
||
<span
|
||
key={index}
|
||
style={{
|
||
background: isSourceEdge
|
||
? 'rgba(82, 196, 26, 0.1)'
|
||
: 'rgba(24, 144, 255, 0.1)',
|
||
color: isActive
|
||
? (isSourceEdge ? '#52c41a' : '#1890ff')
|
||
: '#aaaaaa',
|
||
padding: '1px 3px',
|
||
borderRadius: '3px',
|
||
fontSize: '8px', // Reduced font size
|
||
border: `1px solid ${isActive
|
||
? (isSourceEdge ? 'rgba(82, 196, 26, 0.3)' : 'rgba(24, 144, 255, 0.3)')
|
||
: 'rgba(170, 170, 170, 0.3)'}`
|
||
}}
|
||
>
|
||
{column}
|
||
</span>
|
||
))
|
||
) : (
|
||
<span style={{ color: '#999', fontSize: '8px' }}>No mappings</span>
|
||
)}
|
||
{mappings.length > 3 && (
|
||
<span style={{
|
||
color: '#999',
|
||
fontSize: '8px',
|
||
fontStyle: 'italic'
|
||
}}>
|
||
+{mappings.length - 3} more
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</EdgeLabelRenderer>
|
||
</>
|
||
);
|
||
};
|
||
|
||
// 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 <TbTransform size={iconSize} color={iconColor} />;
|
||
case 'filter':
|
||
return <FaFilter size={iconSize} color={iconColor} />;
|
||
case 'aggregate':
|
||
return <FaCalculator size={iconSize} color={iconColor} />;
|
||
case 'sync':
|
||
return <FaSync size={iconSize} color={iconColor} />;
|
||
case 'code':
|
||
return <FaCode size={iconSize} color={iconColor} />;
|
||
case 'analytics':
|
||
return <FaChartLine size={iconSize} color={iconColor} />;
|
||
case 'transfer':
|
||
return <BiTransfer size={iconSize} color={iconColor} />;
|
||
case 'exchange':
|
||
return <FaExchangeAlt size={iconSize} color={iconColor} />;
|
||
default:
|
||
return <CustomProcessIcon width={iconSize} height={iconSize} />;
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="process-node" style={{
|
||
position: 'relative',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
background: 'white', // Add background to prevent transparency issues
|
||
padding: '10px',
|
||
borderRadius: '8px',
|
||
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)',
|
||
border: `1px solid ${isActive ? primaryColor : '#dddddd'}`,
|
||
width: '80px', // Fixed width to ensure consistent sizing
|
||
height: '80px', // Fixed height to ensure consistent sizing
|
||
zIndex: 5 // Ensure it's above the background but below other elements
|
||
}}>
|
||
{/* Connection handles */}
|
||
<Handle
|
||
type="target"
|
||
position={Position.Left}
|
||
id="left"
|
||
style={{
|
||
background: primaryColor,
|
||
width: '10px',
|
||
height: '10px',
|
||
left: '-5px',
|
||
zIndex: 10
|
||
}}
|
||
isConnectable={true}
|
||
/>
|
||
<Handle
|
||
type="source"
|
||
position={Position.Right}
|
||
id="right"
|
||
style={{
|
||
background: primaryColor,
|
||
width: '10px',
|
||
height: '10px',
|
||
right: '-5px',
|
||
zIndex: 10
|
||
}}
|
||
isConnectable={true}
|
||
/>
|
||
|
||
{/* Process Icon with integrated content */}
|
||
<div
|
||
onClick={handleProcessEdit}
|
||
style={{
|
||
position: 'relative',
|
||
display: 'flex',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
filter: isActive
|
||
? 'drop-shadow(0 0 8px rgba(250, 140, 22, 0.3))'
|
||
: 'drop-shadow(0 0 5px rgba(170, 170, 170, 0.2))',
|
||
cursor: 'pointer',
|
||
zIndex: 100
|
||
}}
|
||
>
|
||
{getProcessIcon()}
|
||
|
||
{/* Status indicator (small dot) */}
|
||
<div style={{
|
||
position: 'absolute',
|
||
top: '-5px',
|
||
right: '-5px',
|
||
width: '10px',
|
||
height: '10px',
|
||
background: statusColor,
|
||
borderRadius: '50%',
|
||
border: '2px solid white'
|
||
}} />
|
||
</div>
|
||
|
||
{/* Process Label (below icon) */}
|
||
<div style={{
|
||
fontWeight: 'bold',
|
||
fontSize: '11px',
|
||
color: '#333',
|
||
textAlign: 'center',
|
||
maxWidth: '70px',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
whiteSpace: 'nowrap',
|
||
marginTop: '5px',
|
||
background: 'rgba(255, 255, 255, 0.9)',
|
||
padding: '2px 4px',
|
||
borderRadius: '8px'
|
||
}}>
|
||
{data.label}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 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 = <CustomDatabaseIcon width="16" height="16" style={{ marginRight: '8px' }} />;
|
||
} else if (isFact) {
|
||
tableColor = '#1890ff'; // Blue for FACT tables
|
||
tableLabel = 'FACT';
|
||
tableIcon = <CustomDocumentIcon width="16" height="16" style={{ marginRight: '8px' }} />;
|
||
} else {
|
||
tableColor = '#52c41a'; // Green for DIM tables
|
||
tableLabel = 'DIM';
|
||
tableIcon = <CustomDimensionIcon width="16" height="16" style={{ marginRight: '8px' }} />;
|
||
}
|
||
|
||
const background = '#ffffff';
|
||
|
||
return (
|
||
<div
|
||
className="table-node"
|
||
draggable={true}
|
||
onDragStart={onDragStart}
|
||
style={{
|
||
padding: '15px', // Increased padding
|
||
borderRadius: '8px', // Increased border radius for better visual separation
|
||
background: background,
|
||
border: `2px solid ${tableColor}`,
|
||
width: '200px', // Slightly reduced width to allow more space between tables
|
||
position: 'relative',
|
||
boxShadow: `0 4px 12px rgba(0, 0, 0, 0.1)`, // Improved shadow for better depth
|
||
color: '#333333',
|
||
margin: '15px', // Added margin to prevent overlapping
|
||
minHeight: '120px', // Minimum height to ensure consistent sizing
|
||
cursor: 'grab', // Indicate draggability
|
||
}}>
|
||
{/* Connection handles */}
|
||
<Handle
|
||
type="source"
|
||
position={Position.Right}
|
||
id="right"
|
||
style={{
|
||
background: tableColor,
|
||
width: '12px',
|
||
height: '12px',
|
||
right: '-6px',
|
||
border: '2px solid white',
|
||
zIndex: 10
|
||
}}
|
||
isConnectable={true}
|
||
/>
|
||
<Handle
|
||
type="target"
|
||
position={Position.Left}
|
||
id="left"
|
||
style={{
|
||
background: tableColor,
|
||
width: '12px',
|
||
height: '12px',
|
||
left: '-6px',
|
||
border: '2px solid white',
|
||
zIndex: 10
|
||
}}
|
||
isConnectable={true}
|
||
/>
|
||
|
||
<div style={{
|
||
fontWeight: 'bold',
|
||
marginBottom: '10px', // Increased margin for better spacing
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between' // Better alignment of elements
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', flex: 1, overflow: 'hidden' }}>
|
||
{/* Different icons for different table types */}
|
||
{tableIcon}
|
||
<span style={{
|
||
maxWidth: '110px',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
whiteSpace: 'nowrap',
|
||
fontSize: '13px' // Slightly larger font
|
||
}}>
|
||
{data.label}
|
||
</span>
|
||
</div>
|
||
<span style={{
|
||
fontSize: '10px',
|
||
background: tableColor,
|
||
color: 'white',
|
||
padding: '2px 6px', // Increased padding
|
||
borderRadius: '4px', // Increased border radius
|
||
marginLeft: '5px',
|
||
fontWeight: 'bold'
|
||
}}>
|
||
{tableLabel}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Table Action Buttons */}
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'flex-end',
|
||
gap: '5px',
|
||
marginBottom: '8px'
|
||
}}>
|
||
<button
|
||
onClick={handleUpdateTable}
|
||
title="Edit Table"
|
||
style={{
|
||
background: '#1890ff',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
padding: '3px 8px',
|
||
fontSize: '10px',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '3px'
|
||
}}
|
||
>
|
||
<FaEdit size={10} /> Edit
|
||
</button>
|
||
<button
|
||
onClick={handleDeleteTable}
|
||
title="Delete Table"
|
||
style={{
|
||
background: '#ff4d4f',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
padding: '3px 8px',
|
||
fontSize: '10px',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '3px'
|
||
}}
|
||
>
|
||
<FaTrash size={10} /> Delete
|
||
</button>
|
||
</div>
|
||
|
||
{/* Always show the columns section, even if empty */}
|
||
<div style={{
|
||
fontSize: '0.75em',
|
||
color: '#555',
|
||
background: isStage
|
||
? 'rgba(250, 140, 22, 0.1)' // Light orange for STAGE tables
|
||
: isFact
|
||
? 'rgba(24, 144, 255, 0.1)' // Light blue for FACT tables
|
||
: 'rgba(82, 196, 26, 0.1)', // Light green for DIM tables
|
||
padding: '8px', // Increased padding
|
||
borderRadius: '6px', // Increased border radius
|
||
marginTop: '8px', // Increased margin
|
||
border: `1px solid ${isStage
|
||
? 'rgba(250, 140, 22, 0.2)'
|
||
: isFact
|
||
? 'rgba(24, 144, 255, 0.2)'
|
||
: 'rgba(82, 196, 26, 0.2)'}`
|
||
}}>
|
||
<div style={{
|
||
marginBottom: '5px',
|
||
color: '#333',
|
||
fontWeight: 'bold',
|
||
fontSize: '11px', // Slightly larger font
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center'
|
||
}}>
|
||
<span>Columns:</span>
|
||
<button
|
||
onClick={handleAddColumn}
|
||
style={{
|
||
background: tableColor,
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
width: 'auto',
|
||
height: '20px',
|
||
fontSize: '11px',
|
||
lineHeight: '14px',
|
||
padding: '0 8px',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginLeft: "4px"
|
||
}}
|
||
title="Manage columns for this table"
|
||
>
|
||
Manage Columns
|
||
</button>
|
||
</div>
|
||
|
||
{/* Display columns if they exist */}
|
||
{data.columns && data.columns.length > 0 ? (
|
||
<div style={{
|
||
display: 'flex',
|
||
flexWrap: 'wrap',
|
||
gap: '4px' // Increased gap
|
||
}}>
|
||
{data.columns.slice(0, 5).map((col, index) => (
|
||
<span key={index} style={{
|
||
background: 'rgba(0, 0, 0, 0.05)',
|
||
padding: '2px 6px', // Increased padding
|
||
borderRadius: '3px', // Increased border radius
|
||
fontSize: '10px',
|
||
color: '#333',
|
||
border: '1px solid rgba(0, 0, 0, 0.08)' // Added subtle border
|
||
}}>
|
||
{col}
|
||
</span>
|
||
))}
|
||
{data.columns.length > 5 && (
|
||
<span style={{
|
||
padding: '2px 6px', // Increased padding
|
||
fontSize: '10px',
|
||
color: '#666',
|
||
fontStyle: 'italic'
|
||
}}>
|
||
+{data.columns.length - 5} more
|
||
</span>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div style={{
|
||
padding: '4px 0',
|
||
fontSize: '10px',
|
||
color: '#666',
|
||
fontStyle: 'italic',
|
||
textAlign: 'center'
|
||
}}>
|
||
No columns defined. Click "Manage Columns" to add.
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 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 = () => (
|
||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||
<div>Loading data from API...</div>
|
||
</div>
|
||
);
|
||
|
||
// 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 (
|
||
<>
|
||
<div style={{
|
||
marginBottom: '16px',
|
||
padding: '8px 0'
|
||
}}>
|
||
<Breadcrumbs
|
||
aria-label="breadcrumb"
|
||
separator="›"
|
||
sx={{
|
||
'& .MuiBreadcrumbs-separator': {
|
||
color: 'black',
|
||
margin: '0 8px',
|
||
},
|
||
fontSize: '14px',
|
||
}}
|
||
>
|
||
<Link
|
||
underline="hover"
|
||
color="black"
|
||
href="#"
|
||
onClick={(e) => e.preventDefault()}
|
||
sx={{ fontWeight: 500 }}
|
||
>
|
||
Qubit
|
||
</Link>
|
||
|
||
{/* Data Mappings Dropdown */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
cursor: 'pointer',
|
||
color: '#023020',
|
||
fontWeight: 500
|
||
}}
|
||
onClick={handleDataMappingsClick}
|
||
>
|
||
<Typography
|
||
color="#023020"
|
||
sx={{ fontWeight: 500, cursor: 'pointer' }}
|
||
>
|
||
Data Mappings
|
||
</Typography>
|
||
<FaChevronDown
|
||
style={{
|
||
marginLeft: '4px',
|
||
fontSize: '12px',
|
||
transform: dataMappingsMenuAnchor ? 'rotate(180deg)' : 'rotate(0deg)',
|
||
transition: 'transform 0.2s ease'
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
{/* Selected View */}
|
||
<Typography color="#301934">{selectedView}</Typography>
|
||
</Breadcrumbs>
|
||
</div>
|
||
|
||
{/* Data Mappings Menu */}
|
||
<Menu
|
||
anchorEl={dataMappingsMenuAnchor}
|
||
open={Boolean(dataMappingsMenuAnchor)}
|
||
onClose={handleDataMappingsClose}
|
||
PaperProps={{
|
||
sx: {
|
||
mt: 1,
|
||
minWidth: 180,
|
||
'& .MuiMenuItem-root': {
|
||
px: 2,
|
||
py: 1,
|
||
},
|
||
},
|
||
}}
|
||
>
|
||
<MenuItem
|
||
onClick={() => handleViewSelect('Data Flow View')}
|
||
selected={selectedView === 'Data Flow View'}
|
||
>
|
||
<ListItemIcon>
|
||
<FaExchangeAlt size={16} />
|
||
</ListItemIcon>
|
||
<ListItemText primary="Data Flow View" />
|
||
</MenuItem>
|
||
<MenuItem
|
||
onClick={() => handleViewSelect('Pipelines')}
|
||
selected={selectedView === 'Pipelines'}
|
||
>
|
||
<ListItemIcon>
|
||
<FaArrowRight size={16} />
|
||
</ListItemIcon>
|
||
<ListItemText primary="Pipelines" />
|
||
</MenuItem>
|
||
</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 = () => (
|
||
// <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||
// <div>Loading data from API...</div>
|
||
// </div>
|
||
// );
|
||
|
||
// 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 (
|
||
<div style={{ width: '100%', height: '100%', background: '#ffffff' }} ref={reactFlowWrapper}>
|
||
<style>{customStyles}</style>
|
||
|
||
{/* Show loading state if data is still loading */}
|
||
{loading ? (
|
||
<LoadingComponent />
|
||
) : (
|
||
<>
|
||
{/* Database Header */}
|
||
{selectedDatabase && (
|
||
<div style={{
|
||
padding: '10px 15px',
|
||
background: 'linear-gradient(90deg, #00a99d, #52c41a)',
|
||
color: 'black',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||
<svg width="24" height="24" viewBox="0 0 41 39" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<rect width="40.5" height="38.7" rx="3.6" fill="white" fillOpacity="0.2"/>
|
||
<path d="M19.8845 24.789C17.0714 24.789 14.2465 24.0672 12.912 22.5097C12.8725 22.683 12.8462 22.8607 12.8462 23.0493V25.8844C12.8462 28.3962 16.4937 29.5392 19.8845 29.5392C23.2753 29.5392 26.9228 28.3962 26.9228 25.8844V23.0493C26.9228 22.8607 26.8964 22.6837 26.8569 22.5104C25.5217 24.068 22.6976 24.7904 19.8837 24.7904L19.8845 24.789ZM19.8845 19.3083C17.0714 19.3083 14.2465 18.5865 12.9127 17.0289C12.8706 17.2053 12.8486 17.3858 12.8469 17.5671V20.4022C12.8469 22.9133 16.4937 24.0563 19.8845 24.0563C23.2753 24.0563 26.9228 22.9133 26.9228 20.4015V17.5657C26.9228 17.3778 26.8964 17.2001 26.8569 17.0268C25.5217 18.5843 22.6976 19.3068 19.8837 19.3068L19.8845 19.3083ZM19.8845 8.42944C16.4937 8.42944 12.8462 9.57385 12.8462 12.0857V14.9208C12.8462 17.4326 16.4937 18.5755 19.8845 18.5755C23.2753 18.5755 26.9228 17.4333 26.9228 14.9215V12.0864C26.9228 9.57458 23.2753 8.43017 19.8845 8.43017V8.42944ZM19.8845 14.2794C16.8059 14.2794 14.3087 13.2974 14.3087 12.0857C14.3087 10.8747 16.8052 9.89194 19.8845 9.89194C22.9638 9.89194 25.4603 10.8747 25.4603 12.0857C25.4603 13.2981 22.9638 14.2794 19.8845 14.2794Z" fill="white"/>
|
||
</svg>
|
||
<div>
|
||
<h2 style={{ margin: 0, fontSize: '18px', color: 'black' }}>{selectedDatabase.name} Database</h2>
|
||
{/* <div style={{ fontSize: '12px', opacity: 0.9 }}>Data Flow View</div> */}
|
||
<HierarchicalBreadcrumb />
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<button
|
||
onClick={() => {
|
||
// Use hash-based routing to navigate back to explorer
|
||
window.location.hash = '#/qubit_service';
|
||
|
||
// Also dispatch the custom event as a fallback
|
||
const event = new CustomEvent('switchToTab', { detail: 'canvas' });
|
||
window.dispatchEvent(event);
|
||
}}
|
||
style={{
|
||
background: 'rgba(255, 255, 255, 0.2)',
|
||
border: 'none',
|
||
color: 'white',
|
||
padding: '5px 10px',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '12px'
|
||
}}
|
||
>
|
||
Back to Explorer
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<ReactFlow
|
||
nodes={nodes}
|
||
edges={edges}
|
||
onNodesChange={onNodesChange}
|
||
onEdgesChange={onEdgesChange}
|
||
onDrop={onDrop}
|
||
onDragOver={onDragOver}
|
||
onConnect={onConnect}
|
||
onConnectStart={onConnectStart}
|
||
onConnectEnd={onConnectEnd}
|
||
onInit={onInit}
|
||
onNodeClick={onNodeClick}
|
||
onMove={onMove}
|
||
onNodeDragStop={(event, node) => {
|
||
// 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 && (
|
||
<Panel position="center">
|
||
<div style={{
|
||
background: 'rgba(255, 255, 255, 0.9)',
|
||
padding: '30px',
|
||
borderRadius: '8px',
|
||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||
textAlign: 'center',
|
||
maxWidth: '500px'
|
||
}}>
|
||
<svg width="80" height="80" viewBox="0 0 41 39" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ margin: '0 auto 20px' }}>
|
||
<rect width="40.5" height="38.7" rx="3.6" fill="url(#db_paint0_linear)"/>
|
||
<path d="M19.8845 24.789C17.0714 24.789 14.2465 24.0672 12.912 22.5097C12.8725 22.683 12.8462 22.8607 12.8462 23.0493V25.8844C12.8462 28.3962 16.4937 29.5392 19.8845 29.5392C23.2753 29.5392 26.9228 28.3962 26.9228 25.8844V23.0493C26.9228 22.8607 26.8964 22.6837 26.8569 22.5104C25.5217 24.068 22.6976 24.7904 19.8837 24.7904L19.8845 24.789ZM19.8845 19.3083C17.0714 19.3083 14.2465 18.5865 12.9127 17.0289C12.8706 17.2053 12.8486 17.3858 12.8469 17.5671V20.4022C12.8469 22.9133 16.4937 24.0563 19.8845 24.0563C23.2753 24.0563 26.9228 22.9133 26.9228 20.4015V17.5657C26.9228 17.3778 26.8964 17.2001 26.8569 17.0268C25.5217 18.5843 22.6976 19.3068 19.8837 19.3068L19.8845 19.3083ZM19.8845 8.42944C16.4937 8.42944 12.8462 9.57385 12.8462 12.0857V14.9208C12.8462 17.4326 16.4937 18.5755 19.8845 18.5755C23.2753 18.5755 26.9228 17.4333 26.9228 14.9215V12.0864C26.9228 9.57458 23.2753 8.43017 19.8845 8.43017V8.42944ZM19.8845 14.2794C16.8059 14.2794 14.3087 13.2974 14.3087 12.0857C14.3087 10.8747 16.8052 9.89194 19.8845 9.89194C22.9638 9.89194 25.4603 10.8747 25.4603 12.0857C25.4603 13.2981 22.9638 14.2794 19.8845 14.2794Z" fill="white"/>
|
||
<defs>
|
||
<linearGradient id="db_paint0_linear" x1="40.5" y1="19.35" x2="0" y2="19.35" gradientUnits="userSpaceOnUse">
|
||
<stop stopColor="#006064"/>
|
||
<stop offset="0.711538" stopColor="#00A1A8"/>
|
||
</linearGradient>
|
||
</defs>
|
||
</svg>
|
||
<h3 style={{ margin: '0 0 10px', color: '#333', fontSize: '20px' }}>
|
||
{selectedDatabase.name} Database is Empty
|
||
</h3>
|
||
<p style={{ color: '#666', marginBottom: '20px' }}>
|
||
{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."
|
||
}
|
||
</p>
|
||
<div style={{ display: 'flex', justifyContent: 'center', gap: '10px' }}>
|
||
<button
|
||
onClick={() => setShowTablePopup(true)}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: '5px',
|
||
padding: '8px 16px',
|
||
background: '#52c41a',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px'
|
||
}}
|
||
>
|
||
<FaTable size={14} />
|
||
Add First Table
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</Panel>
|
||
)}
|
||
|
||
{/* Process Details Popup */}
|
||
{showProcessPopup && selectedProcess && (
|
||
<div
|
||
ref={popupRef}
|
||
style={{
|
||
position: 'absolute',
|
||
top: popupPosition.y,
|
||
left: popupPosition.x,
|
||
zIndex: 1000,
|
||
background: '#ffffff',
|
||
border: '2px solid #fa8c16',
|
||
borderRadius: '8px',
|
||
padding: '15px',
|
||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.2)',
|
||
color: '#333333',
|
||
width: '350px',
|
||
maxWidth: '90vw',
|
||
maxHeight: '80vh',
|
||
overflow: 'auto',
|
||
backdropFilter: 'blur(5px)',
|
||
animation: 'fadeIn 0.3s ease',
|
||
cursor: isDragging ? 'grabbing' : 'grab',
|
||
userSelect: 'none'
|
||
}}
|
||
onMouseDown={handleMouseDown}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginBottom: '15px',
|
||
borderBottom: '1px solid rgba(250, 140, 22, 0.3)',
|
||
paddingBottom: '10px',
|
||
cursor: 'grab'
|
||
}}
|
||
className="popup-header"
|
||
>
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '8px'
|
||
}}>
|
||
{/* Drag handle indicator */}
|
||
<div style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: '2px',
|
||
marginRight: '5px',
|
||
opacity: 0.5
|
||
}}>
|
||
<div style={{ width: '20px', height: '2px', background: '#fa8c16', borderRadius: '1px' }}></div>
|
||
<div style={{ width: '20px', height: '2px', background: '#fa8c16', borderRadius: '1px' }}></div>
|
||
<div style={{ width: '20px', height: '2px', background: '#fa8c16', borderRadius: '1px' }}></div>
|
||
</div>
|
||
|
||
<h3 style={{
|
||
margin: 0,
|
||
color: '#fa8c16',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '8px'
|
||
}}>
|
||
<CustomProcessIcon width="20" height="20" /> {selectedProcess.name}
|
||
</h3>
|
||
</div>
|
||
|
||
<button
|
||
onClick={() => setShowProcessPopup(false)}
|
||
style={{
|
||
background: 'transparent',
|
||
border: 'none',
|
||
color: '#666',
|
||
fontSize: '16px',
|
||
cursor: 'pointer',
|
||
padding: '5px'
|
||
}}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '15px' }}>
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
marginBottom: '8px'
|
||
}}>
|
||
<span style={{ color: '#666' }}>Process ID:</span>
|
||
<span style={{
|
||
background: 'rgba(250, 140, 22, 0.1)',
|
||
padding: '2px 6px',
|
||
borderRadius: '4px',
|
||
fontFamily: 'monospace',
|
||
color: '#333'
|
||
}}>
|
||
{selectedProcess.slug}
|
||
</span>
|
||
</div>
|
||
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
marginBottom: '8px'
|
||
}}>
|
||
<span style={{ color: '#666' }}>Type:</span>
|
||
<span style={{
|
||
background: 'rgba(250, 140, 22, 0.1)',
|
||
padding: '2px 6px',
|
||
borderRadius: '4px',
|
||
color: '#333'
|
||
}}>
|
||
{selectedProcess.type}
|
||
</span>
|
||
</div>
|
||
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
marginBottom: '8px'
|
||
}}>
|
||
<span style={{ color: '#666' }}>Status:</span>
|
||
<span style={{
|
||
background: selectedProcess.status === 'active' ? 'rgba(82, 196, 26, 0.2)' : 'rgba(255, 77, 79, 0.2)',
|
||
color: selectedProcess.status === 'active' ? '#52c41a' : '#ff4d4f',
|
||
padding: '2px 6px',
|
||
borderRadius: '4px',
|
||
fontWeight: 'bold'
|
||
}}>
|
||
{selectedProcess.status}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '15px' }}>
|
||
<h4 style={{
|
||
margin: '0 0 8px 0',
|
||
color: '#555',
|
||
fontSize: '14px'
|
||
}}>
|
||
Description
|
||
</h4>
|
||
<p style={{
|
||
margin: 0,
|
||
padding: '8px',
|
||
background: 'rgba(250, 140, 22, 0.05)',
|
||
borderRadius: '4px',
|
||
fontSize: '14px',
|
||
color: '#333',
|
||
border: '1px solid rgba(250, 140, 22, 0.2)'
|
||
}}>
|
||
{selectedProcess.description || 'No description available'}
|
||
</p>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: '15px' }}>
|
||
<div style={{ flex: 1 }}>
|
||
<h4 style={{
|
||
margin: '0 0 8px 0',
|
||
color: '#52c41a',
|
||
fontSize: '14px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '5px'
|
||
}}>
|
||
<CustomDatabaseIcon width="16" height="16" /> Source Tables
|
||
</h4>
|
||
<ul style={{
|
||
margin: 0,
|
||
padding: '8px',
|
||
background: 'rgba(82, 196, 26, 0.1)',
|
||
borderRadius: '4px',
|
||
listStyle: 'none',
|
||
border: '1px solid rgba(82, 196, 26, 0.2)'
|
||
}}>
|
||
{selectedProcess.source_table_names.map((name, index) => (
|
||
<li key={index} style={{
|
||
padding: '4px 0',
|
||
borderBottom: index < selectedProcess.source_table_names.length - 1 ? '1px solid rgba(0, 0, 0, 0.05)' : 'none',
|
||
fontSize: '13px',
|
||
color: '#333'
|
||
}}>
|
||
{name}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
|
||
<div style={{ flex: 1 }}>
|
||
<h4 style={{
|
||
margin: '0 0 8px 0',
|
||
color: '#1890ff',
|
||
fontSize: '14px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '5px'
|
||
}}>
|
||
<CustomDatabaseIcon width="16" height="16" /> Destination Tables
|
||
</h4>
|
||
<ul style={{
|
||
margin: 0,
|
||
padding: '8px',
|
||
background: 'rgba(24, 144, 255, 0.1)',
|
||
borderRadius: '4px',
|
||
listStyle: 'none',
|
||
border: '1px solid rgba(24, 144, 255, 0.2)'
|
||
}}>
|
||
{selectedProcess.destination_table_names.map((name, index) => (
|
||
<li key={index} style={{
|
||
padding: '4px 0',
|
||
borderBottom: index < selectedProcess.destination_table_names.length - 1 ? '1px solid rgba(0, 0, 0, 0.05)' : 'none',
|
||
fontSize: '13px',
|
||
color: '#333'
|
||
}}>
|
||
{name}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{
|
||
marginTop: '15px',
|
||
display: 'flex',
|
||
justifyContent: 'flex-end',
|
||
gap: '10px',
|
||
borderTop: '1px solid rgba(250, 140, 22, 0.3)',
|
||
paddingTop: '15px'
|
||
}}>
|
||
<button
|
||
onClick={() => setShowProcessPopup(false)}
|
||
style={{
|
||
background: '#f0f0f0',
|
||
border: '1px solid #ccc',
|
||
color: '#333',
|
||
padding: '6px 12px',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
Close
|
||
</button>
|
||
<button
|
||
onClick={() => handleEditProcess(selectedProcess)}
|
||
style={{
|
||
background: '#fa8c16',
|
||
border: 'none',
|
||
color: '#fff',
|
||
padding: '6px 12px',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '5px',
|
||
boxShadow: '0 2px 5px rgba(250, 140, 22, 0.3)'
|
||
}}
|
||
>
|
||
<CustomProcessIcon width="16" height="16" /> Edit Process
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<Controls />
|
||
<MiniMap
|
||
nodeStrokeColor={(n) => {
|
||
if (n.type === 'table') return n.data.type === 'fact' ? '#1890ff' : '#52c41a';
|
||
return '#fa8c16';
|
||
}}
|
||
nodeColor={(n) => {
|
||
if (n.type === 'table') return n.data.type === 'fact' ? 'rgba(24, 144, 255, 0.2)' : 'rgba(82, 196, 26, 0.2)';
|
||
return 'rgba(250, 140, 22, 0.2)';
|
||
}}
|
||
maskColor="rgba(255, 255, 255, 0.7)"
|
||
style={{ background: '#f0f0f0', border: '1px solid #ddd' }}
|
||
/>
|
||
<Background color="#e0e0e0" gap={16} variant="dots" />
|
||
|
||
<Panel position="top-right">
|
||
<div style={{
|
||
display: 'flex',
|
||
gap: '10px',
|
||
background: 'rgba(255, 255, 255, 0.9)',
|
||
padding: '10px',
|
||
borderRadius: '5px',
|
||
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
|
||
backdropFilter: 'blur(5px)',
|
||
border: '1px solid rgba(82, 196, 26, 0.3)',
|
||
}}>
|
||
|
||
<button
|
||
onClick={addProcessNode}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '5px',
|
||
padding: '5px 10px',
|
||
background: '#fa8c16',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
<CustomProcessIcon width="18" height="18" />
|
||
Add Process
|
||
</button>
|
||
<button
|
||
onClick={() => setShowSchemaPopup(true)}
|
||
disabled={!dbSlug}
|
||
title={!dbSlug ? "Please select a database first" : "Create a new schema in this database"}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '5px',
|
||
padding: '5px 10px',
|
||
background: dbSlug ? '#1890ff' : '#d9d9d9',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: dbSlug ? 'pointer' : 'not-allowed',
|
||
opacity: dbSlug ? 1 : 0.7
|
||
}}
|
||
>
|
||
<CustomSchemaIcon width="18" height="18" />
|
||
Add Schema
|
||
</button>
|
||
<button
|
||
onClick={() => setConnectionType(connectionType === 'default' ? 'reference' : 'default')}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '5px',
|
||
padding: '5px 10px',
|
||
background: connectionType === 'reference' ? '#00a99d' : '#722ed1',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer'
|
||
}}
|
||
>
|
||
<FaExchangeAlt size={14} />
|
||
{connectionType === 'reference' ? 'Reference' : 'Default'} Connection
|
||
</button>
|
||
</div>
|
||
</Panel>
|
||
</ReactFlow>
|
||
|
||
{/* Process Form */}
|
||
<ProcessForm
|
||
isOpen={showProcessForm}
|
||
onClose={() => setShowProcessForm(false)}
|
||
onSave={handleSaveProcess}
|
||
tables={apiData?.tables || mockApiData.tables}
|
||
existingProcess={selectedProcessForEdit}
|
||
/>
|
||
|
||
{/* Table Creation Popup */}
|
||
{showTablePopup && selectedSchemaForTable && (
|
||
<TableCreationPopup
|
||
onClose={() => {
|
||
setShowTablePopup(false);
|
||
setSelectedSchemaForTable(null);
|
||
}}
|
||
onCreateTable={handleCreateTable}
|
||
schemaInfo={selectedSchemaForTable}
|
||
/>
|
||
)}
|
||
|
||
{/* Make the showTableCreationPopup function globally available */}
|
||
<div style={{ display: 'none' }}>
|
||
{/* 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;
|
||
})()}
|
||
</div>
|
||
|
||
{/* Column Management Popup */}
|
||
{showColumnPopup && selectedTableInfo && (
|
||
<ColumnManagementPopup
|
||
onClose={() => {
|
||
setShowColumnPopup(false);
|
||
setSelectedTableInfo(null);
|
||
setTableColumns([]);
|
||
}}
|
||
onCreateColumn={handleCreateColumn}
|
||
onUpdateColumn={handleUpdateColumn}
|
||
onDeleteColumn={handleDeleteColumn}
|
||
tableInfo={selectedTableInfo}
|
||
existingColumns={tableColumns}
|
||
/>
|
||
)}
|
||
|
||
{/* Table Update Popup */}
|
||
{showTableUpdatePopup && selectedTableForUpdate && (
|
||
<TableUpdatePopup
|
||
onClose={() => {
|
||
setShowTableUpdatePopup(false);
|
||
setSelectedTableForUpdate(null);
|
||
}}
|
||
onUpdateTable={handleUpdateTable}
|
||
tableInfo={selectedTableForUpdate}
|
||
/>
|
||
)}
|
||
|
||
{/* Schema Creation Popup */}
|
||
{showSchemaPopup && dbSlug && (
|
||
<div
|
||
style={{
|
||
position: 'fixed',
|
||
top: '50%',
|
||
left: '50%',
|
||
transform: 'translate(-50%, -50%)',
|
||
zIndex: 1000,
|
||
background: '#ffffff',
|
||
border: '2px solid #1890ff',
|
||
borderRadius: '8px',
|
||
padding: '20px',
|
||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.2)',
|
||
width: '400px',
|
||
maxWidth: '90vw'
|
||
}}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginBottom: '15px',
|
||
borderBottom: '1px solid #1890ff',
|
||
paddingBottom: '10px'
|
||
}}>
|
||
<div>
|
||
<h3 style={{ margin: 0, color: '#1890ff', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||
<CustomSchemaIcon width="24" height="24" />
|
||
Create New Schema
|
||
</h3>
|
||
<p style={{ margin: '5px 0 0 0', fontSize: '12px', color: '#666' }}>
|
||
Database: <strong>{dbSlug || 'No database selected'}</strong>
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowSchemaPopup(false)}
|
||
style={{
|
||
background: 'none',
|
||
border: 'none',
|
||
fontSize: '18px',
|
||
cursor: 'pointer',
|
||
color: '#999'
|
||
}}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
{schemaCreationError && (
|
||
<div style={{
|
||
padding: '10px',
|
||
marginBottom: '15px',
|
||
background: '#fff2f0',
|
||
border: '1px solid #ffccc7',
|
||
borderRadius: '4px',
|
||
color: '#ff4d4f'
|
||
}}>
|
||
{schemaCreationError}
|
||
</div>
|
||
)}
|
||
|
||
<div style={{ marginBottom: '15px' }}>
|
||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
|
||
Schema Name *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={newSchemaName}
|
||
onChange={(e) => setNewSchemaName(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '8px',
|
||
border: '1px solid #d9d9d9',
|
||
borderRadius: '4px',
|
||
fontSize: '14px'
|
||
}}
|
||
placeholder="Enter schema name"
|
||
disabled={isCreatingSchema}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '20px' }}>
|
||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
|
||
Description
|
||
</label>
|
||
<textarea
|
||
value={newSchemaDescription}
|
||
onChange={(e) => setNewSchemaDescription(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '8px',
|
||
border: '1px solid #d9d9d9',
|
||
borderRadius: '4px',
|
||
fontSize: '14px',
|
||
minHeight: '80px',
|
||
resize: 'vertical'
|
||
}}
|
||
placeholder="Enter schema description (optional)"
|
||
disabled={isCreatingSchema}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px' }}>
|
||
<button
|
||
onClick={() => setShowSchemaPopup(false)}
|
||
style={{
|
||
padding: '8px 16px',
|
||
background: '#f0f0f0',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px'
|
||
}}
|
||
disabled={isCreatingSchema}
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={handleCreateSchema}
|
||
style={{
|
||
padding: '8px 16px',
|
||
background: '#1890ff',
|
||
color: 'white',
|
||
border: 'none',
|
||
borderRadius: '4px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '5px'
|
||
}}
|
||
disabled={isCreatingSchema}
|
||
>
|
||
{isCreatingSchema ? 'Creating...' : 'Create Schema'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Wrap with ReactFlowProvider
|
||
const DataflowCanvasWrapper = ({ dbSlug, hasSchemas }) => {
|
||
return (
|
||
<ReactFlowProvider>
|
||
<DataflowCanvas dbSlug={dbSlug} hasSchemas={hasSchemas} />
|
||
</ReactFlowProvider>
|
||
);
|
||
};
|
||
|
||
export default DataflowCanvasWrapper; |