1217 lines
42 KiB
JavaScript
1217 lines
42 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);
|
|
|
|
// Table keys from API
|
|
const [tableKeysFromAPI, setTableKeysFromAPI] = useState([]);
|
|
const [loadingTableKeys, setLoadingTableKeys] = 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);
|
|
}
|
|
};
|
|
|
|
// Fetch table keys from API for a specific table
|
|
const fetchTableKeys = async (tableSlug) => {
|
|
if (!tableSlug) {
|
|
setTableKeysFromAPI([]);
|
|
return;
|
|
}
|
|
|
|
setLoadingTableKeys(true);
|
|
try {
|
|
const response = await fetch('https://sandbox.kezel.io/api/qbt_table_key_list_get', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
token: "abdhsg",
|
|
org: "sN05Pjv11qvH",
|
|
tbl: tableSlug
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.status === 200 && data.items) {
|
|
setTableKeysFromAPI(data.items);
|
|
} else {
|
|
console.error('Failed to fetch table keys:', data.message);
|
|
// Fallback to empty array
|
|
setTableKeysFromAPI([]);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching table keys:', error);
|
|
// Fallback to empty array
|
|
setTableKeysFromAPI([]);
|
|
} finally {
|
|
setLoadingTableKeys(false);
|
|
}
|
|
};
|
|
|
|
const resetForm = () => {
|
|
setFormData({
|
|
name: '',
|
|
description: '',
|
|
tableType: '',
|
|
schema: ''
|
|
});
|
|
setColumns([]);
|
|
setKeys([]);
|
|
setRelations([]);
|
|
setKeyTypes([]);
|
|
setLoadingKeyTypes(false);
|
|
setColumnTypesFromAPI([]);
|
|
setLoadingColumnTypes(false);
|
|
setTableKeysFromAPI([]);
|
|
setLoadingTableKeys(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: '',
|
|
tableKey: '', // New field for table key from API
|
|
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
|
|
));
|
|
};
|
|
|
|
// Handle target table selection change
|
|
const handleTargetTableChange = (relationId, tableId) => {
|
|
// Update the relation with the new target table
|
|
updateRelation(relationId, 'targetTable', tableId);
|
|
|
|
// Clear the table key selection when target table changes
|
|
updateRelation(relationId, 'tableKey', '');
|
|
|
|
// Find the selected table to get its slug
|
|
const selectedTable = existingTables.find(table => table.id === tableId);
|
|
if (selectedTable && selectedTable.tbl) {
|
|
// Fetch table keys for the selected table
|
|
fetchTableKeys(selectedTable.tbl);
|
|
} else {
|
|
// Clear table keys if no table is selected
|
|
setTableKeysFromAPI([]);
|
|
}
|
|
};
|
|
|
|
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,
|
|
table_key: rel.tableKey,
|
|
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={2.5}>
|
|
<FormControl fullWidth size="small">
|
|
<InputLabel>Target Table</InputLabel>
|
|
<Select
|
|
value={relation.targetTable}
|
|
onChange={(e) => handleTargetTableChange(relation.id, 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>Table Key</InputLabel>
|
|
<Select
|
|
value={relation.tableKey}
|
|
onChange={(e) => updateRelation(relation.id, 'tableKey', e.target.value)}
|
|
label="Table Key"
|
|
disabled={loadingTableKeys || !relation.targetTable}
|
|
>
|
|
{!relation.targetTable ? (
|
|
<MenuItem disabled>
|
|
Select a target table first
|
|
</MenuItem>
|
|
) : tableKeysFromAPI.length === 0 ? (
|
|
<MenuItem disabled>
|
|
{loadingTableKeys ? 'Loading...' : 'No keys available'}
|
|
</MenuItem>
|
|
) : (
|
|
tableKeysFromAPI.map(key => (
|
|
<MenuItem key={key.key} value={key.key}>
|
|
{key.name}
|
|
</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.5}>
|
|
<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; |