Qubit_EPM/src/components/AddTableModal.jsx

1125 lines
39 KiB
JavaScript

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 (
<Dialog
open={open}
onClose={onClose}
maxWidth={position === 'bottom-right' ? false : 'lg'}
fullWidth={position !== 'bottom-right'}
sx={getDialogStyles()}
PaperProps={{
sx: position === 'bottom-right' ? {} : {
minHeight: '80vh',
maxHeight: '90vh'
}
}}
>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 2, pb: 1 }}>
<TableIcon color="primary" />
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Add New Table
</Typography>
<IconButton onClick={onClose} size="small">
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent dividers sx={{ p: position === 'bottom-right' ? 2 : 3 }}>
{errors.submit && (
<Alert severity="error" sx={{ mb: 3 }}>
{errors.submit}
</Alert>
)}
{/* Basic Table Information */}
<Paper elevation={1} sx={{ p: position === 'bottom-right' ? 2 : 3, mb: position === 'bottom-right' ? 2 : 3 }}>
<Typography variant="h6" gutterBottom color="primary">
Table Information
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="Table Name"
value={formData.name}
onChange={(e) => handleFormChange('name', e.target.value)}
error={!!errors.name}
helperText={errors.name}
required
/>
</Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth error={!!errors.tableType} required>
<InputLabel>Table Type</InputLabel>
<Select
value={formData.tableType}
onChange={(e) => handleFormChange('tableType', e.target.value)}
label="Table Type"
>
{tableTypes.map(type => (
<MenuItem key={type.value} value={type.value}>
{type.label}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth error={!!errors.schema} required>
<InputLabel>Schema</InputLabel>
<Select
value={formData.schema}
onChange={(e) => handleFormChange('schema', e.target.value)}
label="Schema"
>
{schemas.map(schema => (
<MenuItem key={schema.sch} value={schema.sch}>
{schema.name || schema.sch}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Description"
value={formData.description}
onChange={(e) => handleFormChange('description', e.target.value)}
multiline
rows={3}
placeholder="Enter table description..."
/>
</Grid>
</Grid>
</Paper>
{/* Columns Section */}
<Paper elevation={1} sx={{ p: position === 'bottom-right' ? 2 : 3, mb: position === 'bottom-right' ? 2 : 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" color="primary">
Columns
</Typography>
<Button
startIcon={<AddIcon />}
onClick={addColumn}
variant="outlined"
size="small"
>
Add Column
</Button>
</Box>
{columns.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
No columns added yet. Click "Add Column" to get started.
</Typography>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{columns.map((column) => (
<Paper key={column.id} variant="outlined" sx={{ p: 2 }}>
<Grid container spacing={2} alignItems="center">
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="Column Name"
value={column.name}
onChange={(e) => 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"
/>
</Grid>
<Grid item xs={12} md={3}>
<FormControl fullWidth size="small">
<InputLabel>Data Type</InputLabel>
<Select
value={column.type}
onChange={(e) => updateColumn(column.id, 'type', e.target.value)}
label="Data Type"
disabled={loadingColumnTypes}
>
{loadingColumnTypes ? (
<MenuItem value="">Loading...</MenuItem>
) : columnTypesFromAPI.length > 0 ? (
columnTypesFromAPI.map(type => (
<MenuItem key={type.cltp} value={type.name} title={type.description}>
<Box>
<Typography variant="body2" component="div">
{type.name}
</Typography>
<Typography variant="caption" color="text.secondary" component="div">
{type.description}
</Typography>
</Box>
</MenuItem>
))
) : (
columnTypes.map(type => (
<MenuItem key={type} value={type}>
{type}
</MenuItem>
))
)}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={4}>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Chip
label="Nullable"
variant={column.isNullable ? "filled" : "outlined"}
size="small"
onClick={() => updateColumn(column.id, 'isNullable', !column.isNullable)}
color={column.isNullable ? "default" : "primary"}
/>
</Box>
</Grid>
<Grid item xs={12} md={1}>
<IconButton
onClick={() => removeColumn(column.id)}
color="error"
size="small"
>
<DeleteIcon />
</IconButton>
</Grid>
</Grid>
</Paper>
))}
</Box>
)}
</Paper>
{/* Keys Section */}
<Paper elevation={1} sx={{ p: position === 'bottom-right' ? 2 : 3, mb: position === 'bottom-right' ? 2 : 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" color="primary">
<KeyIcon style={{ marginRight: '8px', verticalAlign: 'middle' }} />
Keys
{keysViewMode === 'columns' && selectedKeyForColumns && (
<Typography component="span" variant="body2" sx={{ ml: 2, color: 'text.secondary' }}>
- {keys.find(k => k.id === selectedKeyForColumns)?.name || 'Unnamed Key'}
</Typography>
)}
</Typography>
{keysViewMode === 'keys' ? (
<Button
startIcon={<AddIcon />}
onClick={addKey}
variant="outlined"
size="small"
disabled={columns.length === 0}
>
Add Key
</Button>
) : (
<Button
startIcon={<BackIcon />}
onClick={handleBackToKeys}
variant="outlined"
size="small"
>
Add Key
</Button>
)}
</Box>
{keysViewMode === 'keys' ? (
// Keys Table View
<TableContainer component={Paper} variant="outlined">
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox disabled />
</TableCell>
<TableCell>Key Name</TableCell>
<TableCell>Key Type</TableCell>
<TableCell width="100">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{keys.map((key) => (
<TableRow
key={key.id}
hover
onClick={() => handleKeyRowClick(key.id)}
sx={{ cursor: 'pointer' }}
>
<TableCell padding="checkbox">
<Checkbox
checked={false}
onChange={(e) => e.stopPropagation()}
/>
</TableCell>
<TableCell>
<TextField
value={key.name}
onChange={(e) => {
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
/>
</TableCell>
<TableCell>
<FormControl size="small" variant="standard" sx={{ minWidth: 120 }}>
<Select
value={key.keyType}
onChange={(e) => {
e.stopPropagation();
updateKey(key.id, 'keyType', e.target.value);
}}
onClick={(e) => e.stopPropagation()}
disabled={loadingKeyTypes}
>
{loadingKeyTypes ? (
<MenuItem value="">Loading...</MenuItem>
) : (
keyTypes.map((keyType) => (
<MenuItem key={keyType.kytp} value={keyType.kytp}>
{keyType.name} Key
</MenuItem>
))
)}
</Select>
</FormControl>
</TableCell>
<TableCell>
<IconButton
onClick={(e) => {
e.stopPropagation();
removeKey(key.id);
}}
color="error"
size="small"
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
{/* Empty row for adding new key */}
<TableRow hover onClick={addKey} sx={{ cursor: 'pointer', backgroundColor: 'rgba(0, 0, 0, 0.02)' }}>
<TableCell padding="checkbox">
<Checkbox disabled />
</TableCell>
<TableCell>
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
Click to add new key...
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2" color="text.secondary">
-
</Typography>
</TableCell>
<TableCell>
<AddIcon style={{ color: '#ccc' }} />
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
) : (
// Columns Table View for Selected Key
selectedKeyForColumns && (
<TableContainer component={Paper} variant="outlined">
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox disabled />
</TableCell>
<TableCell>Column Name</TableCell>
<TableCell>Sequence</TableCell>
</TableRow>
</TableHead>
<TableBody>
{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 (
<TableRow key={column.id}>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
onChange={(e) => {
if (e.target.checked) {
addColumnToKey(selectedKeyForColumns, column.id);
} else {
removeColumnFromKey(selectedKeyForColumns, column.id);
}
}}
/>
</TableCell>
<TableCell>{column.name || 'Unnamed Column'}</TableCell>
<TableCell>
{isSelected ? (
<TextField
type="number"
value={keyColumn?.sequence || 1}
onChange={(e) => updateKeyColumnSequence(selectedKeyForColumns, column.id, e.target.value)}
size="small"
inputProps={{ min: 1, style: { width: '80px' } }}
error={hasSequenceError}
helperText={hasSequenceError ? 'Duplicate' : ''}
/>
) : (
<Typography variant="body2" color="text.secondary">-</Typography>
)}
</TableCell>
</TableRow>
);
})}
{/* Empty row for adding new column */}
<TableRow sx={{ backgroundColor: 'rgba(0, 0, 0, 0.02)' }}>
<TableCell padding="checkbox">
<Checkbox disabled />
</TableCell>
<TableCell>
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
Select columns above to add to this key...
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2" color="text.secondary">
-
</Typography>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
)
)}
{/* Show message when no columns exist */}
{columns.length === 0 && (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
Add columns first before defining keys.
</Typography>
)}
</Paper>
{/* Relations Section */}
<Paper elevation={1} sx={{ p: position === 'bottom-right' ? 2 : 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" color="primary">
<LinkIcon style={{ marginRight: '8px', verticalAlign: 'middle' }} />
Relations
</Typography>
<Button
startIcon={<AddIcon />}
onClick={addRelation}
variant="outlined"
size="small"
disabled={keys.length === 0 || existingTables.length === 0}
>
Add Relation
</Button>
</Box>
{relations.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
{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."
}
</Typography>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{relations.map((relation) => (
<Paper key={relation.id} variant="outlined" sx={{ p: 2 }}>
<Grid container spacing={2} alignItems="center">
<Grid item xs={12} md={3}>
<FormControl fullWidth size="small">
<InputLabel>Target Table</InputLabel>
<Select
value={relation.targetTable}
onChange={(e) => updateRelation(relation.id, 'targetTable', e.target.value)}
label="Target Table"
>
{existingTables.map(table => (
<MenuItem key={table.id} value={table.id}>
{table.name}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={2}>
<FormControl fullWidth size="small">
<InputLabel>Source Key</InputLabel>
<Select
value={relation.sourceKey}
onChange={(e) => updateRelation(relation.id, 'sourceKey', e.target.value)}
label="Source Key"
>
{getAvailableKeys().map(key => (
<MenuItem key={key.id} value={key.id}>
{key.name} ({key.columnName})
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={2}>
<FormControl fullWidth size="small">
<InputLabel>Target Key</InputLabel>
<Select
value={relation.targetKey}
onChange={(e) => updateRelation(relation.id, 'targetKey', e.target.value)}
label="Target Key"
disabled={!relation.targetTable}
>
{getTargetTableKeys(relation.targetTable).map(key => (
<MenuItem key={key.id} value={key.id}>
{key.name} ({key.type})
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={2}>
<FormControl fullWidth size="small">
<InputLabel>Relation Type</InputLabel>
<Select
value={relation.relationType}
onChange={(e) => updateRelation(relation.id, 'relationType', e.target.value)}
label="Relation Type"
>
<MenuItem value="1:1">One to One</MenuItem>
<MenuItem value="1:N">One to Many</MenuItem>
<MenuItem value="N:M">Many to Many</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={1}>
<IconButton
onClick={() => removeRelation(relation.id)}
color="error"
size="small"
>
<DeleteIcon />
</IconButton>
</Grid>
</Grid>
</Paper>
))}
</Box>
)}
</Paper>
</DialogContent>
<DialogActions sx={{ p: position === 'bottom-right' ? 2 : 3, gap: 1 }}>
<Button onClick={onClose} variant="outlined">
Cancel
</Button>
<Button
onClick={handleSubmit}
variant="contained"
disabled={isSubmitting}
startIcon={isSubmitting ? null : <AddIcon />}
>
{isSubmitting ? 'Adding Table...' : 'Add Table'}
</Button>
</DialogActions>
</Dialog>
);
};
export default AddTableModal;