import React, { useState, useEffect } from 'react'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Select, MenuItem, FormControl, InputLabel, IconButton, Box, Typography, Divider, Grid, Paper, Chip, Alert, Autocomplete, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Checkbox } from '@mui/material'; import { FaPlus as AddIcon, FaTrash as DeleteIcon, FaTimes as CloseIcon, FaTable as TableIcon, FaKey as KeyIcon, FaLink as LinkIcon, FaArrowLeft as BackIcon } from 'react-icons/fa'; const AddTableModal = ({ open, onClose, onAddTable, schemas = [], existingTables = [], position = 'center', // 'center' | 'bottom-right' tableTypes = [ { value: 'fact', label: 'Fact Table' }, { value: 'dimension', label: 'Dimension Table' }, { value: 'stage', label: 'Stage Table' } ], columnTypes = [ 'INTEGER', 'VARCHAR(255)', 'VARCHAR(100)', 'VARCHAR(50)', 'TEXT', 'DECIMAL(10,2)', 'DECIMAL(15,2)', 'DATE', 'TIMESTAMP', 'BOOLEAN', 'BIGINT', 'SMALLINT', 'FLOAT', 'DOUBLE' ] }) => { // Main form state const [formData, setFormData] = useState({ name: '', description: '', tableType: '', schema: '' }); // Dynamic sections state const [columns, setColumns] = useState([]); const [keys, setKeys] = useState([]); const [relations, setRelations] = useState([]); // Key types from API const [keyTypes, setKeyTypes] = useState([]); const [loadingKeyTypes, setLoadingKeyTypes] = useState(false); // Column types from API const [columnTypesFromAPI, setColumnTypesFromAPI] = useState([]); const [loadingColumnTypes, setLoadingColumnTypes] = useState(false); // Validation and UI state const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); // Keys section navigation state const [keysViewMode, setKeysViewMode] = useState('keys'); // 'keys' or 'columns' const [selectedKeyForColumns, setSelectedKeyForColumns] = useState(null); // Reset form when modal opens/closes useEffect(() => { if (open) { resetForm(); fetchKeyTypes(); fetchColumnTypes(); } }, [open]); // Fetch key types from API const fetchKeyTypes = async () => { setLoadingKeyTypes(true); try { const response = await fetch('https://sandbox.kezel.io/api/qbt_table_key_type_list_get', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: "abdhsg", org: "sN05Pjv11qvH" }) }); const data = await response.json(); if (data.status === 200 && data.items) { setKeyTypes(data.items); } else { console.error('Failed to fetch key types:', data.message); } } catch (error) { console.error('Error fetching key types:', error); // Fallback to default key types setKeyTypes([ { kytp: 'PRIMARY', name: 'Primary' }, { kytp: 'FOREIGN', name: 'Foreign' }, { kytp: 'UNIQUE', name: 'Unique' } ]); } finally { setLoadingKeyTypes(false); } }; // Fetch column types from API const fetchColumnTypes = async () => { setLoadingColumnTypes(true); try { const response = await fetch('https://sandbox.kezel.io/api/qbt_column_type_list_get', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: "abdhsg", org: "sN05Pjv11qvH" }) }); const data = await response.json(); if (data.status === 200 && data.items) { setColumnTypesFromAPI(data.items); } else { console.error('Failed to fetch column types:', data.message); // Fallback to default column types setColumnTypesFromAPI([ { cltp: 'VARCHAR', name: 'VARCHAR', description: 'Variable-length character string' }, { cltp: 'INT', name: 'INT', description: 'Standard integer value' }, { cltp: 'TEXT', name: 'TEXT', description: 'Variable-length character string for long text' }, { cltp: 'DATE', name: 'DATE', description: 'Date value' }, { cltp: 'TIMESTAMP', name: 'TIMESTAMP', description: 'Timestamp value' }, { cltp: 'BOOLEAN', name: 'BOOLEAN', description: 'Logical boolean value' } ]); } } catch (error) { console.error('Error fetching column types:', error); // Fallback to default column types setColumnTypesFromAPI([ { cltp: 'VARCHAR', name: 'VARCHAR', description: 'Variable-length character string' }, { cltp: 'INT', name: 'INT', description: 'Standard integer value' }, { cltp: 'TEXT', name: 'TEXT', description: 'Variable-length character string for long text' }, { cltp: 'DATE', name: 'DATE', description: 'Date value' }, { cltp: 'TIMESTAMP', name: 'TIMESTAMP', description: 'Timestamp value' }, { cltp: 'BOOLEAN', name: 'BOOLEAN', description: 'Logical boolean value' } ]); } finally { setLoadingColumnTypes(false); } }; const resetForm = () => { setFormData({ name: '', description: '', tableType: '', schema: '' }); setColumns([]); setKeys([]); setRelations([]); setKeyTypes([]); setLoadingKeyTypes(false); setColumnTypesFromAPI([]); setLoadingColumnTypes(false); setErrors({}); setIsSubmitting(false); setKeysViewMode('keys'); setSelectedKeyForColumns(null); }; // Form field handlers const handleFormChange = (field, value) => { setFormData(prev => ({ ...prev, [field]: value })); // Clear error when user starts typing if (errors[field]) { setErrors(prev => ({ ...prev, [field]: null })); } }; // Column management const addColumn = () => { const defaultColumnType = columnTypesFromAPI.length > 0 ? columnTypesFromAPI[0].name : 'VARCHAR'; const newColumn = { id: Date.now(), name: '', type: defaultColumnType, isPrimaryKey: false, isForeignKey: false, isNullable: true }; setColumns(prev => [...prev, newColumn]); }; const updateColumn = (id, field, value) => { setColumns(prev => prev.map(col => col.id === id ? { ...col, [field]: value } : col )); // Clear column errors when user starts typing if (field === 'name' && errors.columns?.[id]) { setErrors(prev => ({ ...prev, columns: { ...prev.columns, [id]: null } })); } }; // Helper function to check if column name is duplicate const isColumnNameDuplicate = (columnId, columnName) => { if (!columnName.trim()) return false; const trimmedName = columnName.trim().toLowerCase(); return columns.some(col => col.id !== columnId && col.name.trim().toLowerCase() === trimmedName ); }; const removeColumn = (id) => { setColumns(prev => prev.filter(col => col.id !== id)); // Remove any keys that reference this column or update key columns setKeys(prev => prev.map(key => ({ ...key, columnIds: key.columnIds?.filter(colId => colId !== id) || [], keyColumns: key.keyColumns?.filter(keyCol => keyCol.columnId !== id) || [] })).filter(key => key.keyColumns?.length > 0 || key.columnIds?.length > 0)); }; // Key management const addKey = () => { const defaultKeyType = keyTypes.length > 0 ? keyTypes[0].kytp : 'PRIMARY'; const newKey = { id: Date.now(), name: '', columnIds: [], // Changed to array for multi-select keyType: defaultKeyType, // Use first available key type from API keyColumns: [] // Array of {columnId, sequence} objects }; setKeys(prev => [...prev, newKey]); }; const updateKey = (id, field, value) => { setKeys(prev => prev.map(key => key.id === id ? { ...key, [field]: value } : key )); }; const removeKey = (id) => { setKeys(prev => prev.filter(key => key.id !== id)); }; // Key column management const addColumnToKey = (keyId, columnId) => { setKeys(prev => prev.map(key => { if (key.id === keyId) { const existingSequences = key.keyColumns?.map(kc => kc.sequence) || []; const nextSequence = existingSequences.length > 0 ? Math.max(...existingSequences) + 1 : 1; return { ...key, columnIds: [...(key.columnIds || []), columnId], keyColumns: [...(key.keyColumns || []), { columnId, sequence: nextSequence }] }; } return key; })); }; const removeColumnFromKey = (keyId, columnId) => { setKeys(prev => prev.map(key => { if (key.id === keyId) { return { ...key, columnIds: (key.columnIds || []).filter(id => id !== columnId), keyColumns: (key.keyColumns || []).filter(kc => kc.columnId !== columnId) }; } return key; })); }; const updateKeyColumnSequence = (keyId, columnId, sequence) => { setKeys(prev => prev.map(key => { if (key.id === keyId) { return { ...key, keyColumns: (key.keyColumns || []).map(kc => kc.columnId === columnId ? { ...kc, sequence: parseInt(sequence) || 1 } : kc ) }; } return key; })); }; // Helper function to check for duplicate sequences within a key const hasSequenceDuplicates = (keyId) => { const key = keys.find(k => k.id === keyId); if (!key || !key.keyColumns) return false; const sequences = key.keyColumns.map(kc => kc.sequence); return sequences.length !== new Set(sequences).size; }; // Keys section navigation functions const handleKeyRowClick = (keyId) => { setSelectedKeyForColumns(keyId); setKeysViewMode('columns'); }; const handleBackToKeys = () => { setKeysViewMode('keys'); setSelectedKeyForColumns(null); }; // Relation management const addRelation = () => { const newRelation = { id: Date.now(), targetTable: '', sourceKey: '', targetKey: '', relationType: '1:N' // 1:1, 1:N, N:M }; setRelations(prev => [...prev, newRelation]); }; const updateRelation = (id, field, value) => { setRelations(prev => prev.map(rel => rel.id === id ? { ...rel, [field]: value } : rel )); }; const removeRelation = (id) => { setRelations(prev => prev.filter(rel => rel.id !== id)); }; // Get available keys for relations const getAvailableKeys = () => { return keys.map(key => { const keyColumnNames = (key.keyColumns || []) .sort((a, b) => a.sequence - b.sequence) .map(kc => { const column = columns.find(col => col.id === kc.columnId); return column?.name || 'Unknown Column'; }) .join(', '); return { id: key.id, name: key.name, columnName: keyColumnNames || 'No columns selected' }; }); }; // Get keys from selected target table const getTargetTableKeys = (tableId) => { const targetTable = existingTables.find(table => table.id === tableId); if (!targetTable || !targetTable.columns) return []; return targetTable.columns .filter(col => col.is_primary_key || col.is_foreign_key) .map(col => ({ id: col.name, name: col.name, type: col.is_primary_key ? 'PRIMARY' : 'FOREIGN' })); }; // Form validation const validateForm = () => { const newErrors = {}; if (!formData.name.trim()) { newErrors.name = 'Table name is required'; } if (!formData.schema) { newErrors.schema = 'Schema selection is required'; } if (!formData.tableType) { newErrors.tableType = 'Table type is required'; } // Validate columns const columnErrors = {}; const columnNames = new Set(); const duplicateNames = new Set(); // First pass: identify duplicate names columns.forEach(col => { const trimmedName = col.name.trim().toLowerCase(); if (trimmedName && columnNames.has(trimmedName)) { duplicateNames.add(trimmedName); } if (trimmedName) { columnNames.add(trimmedName); } }); // Second pass: validate each column columns.forEach(col => { const trimmedName = col.name.trim(); if (!trimmedName) { columnErrors[col.id] = 'Column name is required'; } else if (duplicateNames.has(trimmedName.toLowerCase())) { columnErrors[col.id] = 'Column name must be unique within the table'; } }); if (Object.keys(columnErrors).length > 0) { newErrors.columns = columnErrors; } // Validate keys const keyErrors = {}; keys.forEach(key => { if (!key.name.trim()) { keyErrors[key.id] = 'Key name is required'; } if (!key.keyColumns || key.keyColumns.length === 0) { keyErrors[key.id] = 'At least one column must be selected'; } else { // Check for duplicate sequences within the key const sequences = key.keyColumns.map(kc => kc.sequence); const duplicateSequences = sequences.filter((seq, index) => sequences.indexOf(seq) !== index); if (duplicateSequences.length > 0) { keyErrors[key.id] = `Duplicate sequence numbers found: ${[...new Set(duplicateSequences)].join(', ')}`; } } }); if (Object.keys(keyErrors).length > 0) { newErrors.keys = keyErrors; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; // Form submission const handleSubmit = async () => { if (!validateForm()) { return; } setIsSubmitting(true); try { // Prepare table data const tableData = { name: formData.name.trim(), description: formData.description.trim(), table_type: formData.tableType, schema: formData.schema, columns: columns.map(col => ({ name: col.name.trim(), data_type: col.type, is_primary_key: keys.some(key => { const keyTypeObj = keyTypes.find(kt => kt.kytp === key.keyType); return key.keyColumns?.some(kc => kc.columnId === col.id) && keyTypeObj?.name?.toLowerCase() === 'primary'; }), is_foreign_key: keys.some(key => { const keyTypeObj = keyTypes.find(kt => kt.kytp === key.keyType); return key.keyColumns?.some(kc => kc.columnId === col.id) && keyTypeObj?.name?.toLowerCase() === 'foreign'; }), is_nullable: col.isNullable })), keys: keys.flatMap(key => (key.keyColumns || []).map(kc => ({ name: key.name.trim(), column_name: columns.find(col => col.id === kc.columnId)?.name || '', key_type: key.keyType, sequence: kc.sequence })) ), relations: relations.map(rel => ({ target_table: rel.targetTable, source_key: rel.sourceKey, target_key: rel.targetKey, relation_type: rel.relationType })) }; // Call the parent component's add table function await onAddTable(tableData); // Reset form (modal will be closed by parent component) resetForm(); } catch (error) { console.error('Error adding table:', error); setErrors({ submit: 'Failed to add table. Please try again.' }); } finally { setIsSubmitting(false); } }; // Define positioning styles based on position prop const getDialogStyles = () => { if (position === 'bottom-right') { return { '& .MuiDialog-container': { justifyContent: 'flex-end', alignItems: 'flex-end', padding: '20px', }, '& .MuiDialog-paper': { margin: 0, maxWidth: '780px', // Increased by 30% from 600px width: '780px', // Increased by 30% from 600px maxHeight: '80vh', minHeight: '60vh', borderRadius: '12px', boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)', } }; } return { '& .MuiDialog-paper': { minHeight: '80vh', maxHeight: '90vh', maxWidth: '1170px', // Increased by 30% from 900px (lg breakpoint) } }; }; return ( Add New Table {errors.submit && ( {errors.submit} )} {/* Basic Table Information */} Table Information handleFormChange('name', e.target.value)} error={!!errors.name} helperText={errors.name} required /> Table Type Schema handleFormChange('description', e.target.value)} multiline rows={3} placeholder="Enter table description..." /> {/* Columns Section */} Columns {columns.length === 0 ? ( No columns added yet. Click "Add Column" to get started. ) : ( {columns.map((column) => ( updateColumn(column.id, 'name', e.target.value)} error={!!errors.columns?.[column.id] || isColumnNameDuplicate(column.id, column.name)} helperText={ errors.columns?.[column.id] || (isColumnNameDuplicate(column.id, column.name) ? 'Column name must be unique within the table' : '') } size="small" /> Data Type updateColumn(column.id, 'isNullable', !column.isNullable)} color={column.isNullable ? "default" : "primary"} /> removeColumn(column.id)} color="error" size="small" > ))} )} {/* Keys Section */} Keys {keysViewMode === 'columns' && selectedKeyForColumns && ( - {keys.find(k => k.id === selectedKeyForColumns)?.name || 'Unnamed Key'} )} {keysViewMode === 'keys' ? ( ) : ( )} {keysViewMode === 'keys' ? ( // Keys Table View Key Name Key Type Actions {keys.map((key) => ( handleKeyRowClick(key.id)} sx={{ cursor: 'pointer' }} > e.stopPropagation()} /> { e.stopPropagation(); updateKey(key.id, 'name', e.target.value); }} onClick={(e) => e.stopPropagation()} size="small" variant="standard" error={!!errors.keys?.[key.id]} helperText={errors.keys?.[key.id]} placeholder="Enter key name" fullWidth /> { e.stopPropagation(); removeKey(key.id); }} color="error" size="small" > ))} {/* Empty row for adding new key */} Click to add new key... -
) : ( // Columns Table View for Selected Key selectedKeyForColumns && ( Column Name Sequence {columns.map((column) => { const selectedKey = keys.find(k => k.id === selectedKeyForColumns); const keyColumn = selectedKey?.keyColumns?.find(kc => kc.columnId === column.id); const isSelected = !!keyColumn; const hasSequenceError = hasSequenceDuplicates(selectedKeyForColumns); return ( { if (e.target.checked) { addColumnToKey(selectedKeyForColumns, column.id); } else { removeColumnFromKey(selectedKeyForColumns, column.id); } }} /> {column.name || 'Unnamed Column'} {isSelected ? ( updateKeyColumnSequence(selectedKeyForColumns, column.id, e.target.value)} size="small" inputProps={{ min: 1, style: { width: '80px' } }} error={hasSequenceError} helperText={hasSequenceError ? 'Duplicate' : ''} /> ) : ( - )} ); })} {/* Empty row for adding new column */} Select columns above to add to this key... -
) )} {/* Show message when no columns exist */} {columns.length === 0 && ( Add columns first before defining keys. )}
{/* Relations Section */} Relations {relations.length === 0 ? ( {keys.length === 0 ? "Define keys first before creating relations." : existingTables.length === 0 ? "No existing tables available for relations." : "No relations defined yet. Click \"Add Relation\" to create table relationships." } ) : ( {relations.map((relation) => ( Target Table Source Key Target Key Relation Type removeRelation(relation.id)} color="error" size="small" > ))} )}
); }; export default AddTableModal;