Compare commits
No commits in common. "5fa97985655e9191c9eb611bdfbe0248056d715c" and "8dd63e1953b4f699aeabeb08fa07449f4dee654a" have entirely different histories.
5fa9798565
...
8dd63e1953
|
|
@ -284,7 +284,7 @@ function App() {
|
|||
</span>
|
||||
</div>
|
||||
|
||||
{/* Data Entity Item */}
|
||||
{/* ER Diagram Item */}
|
||||
<div
|
||||
className={`nav-button ${activeTab === 'er_diagram' ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
|
|
@ -310,7 +310,7 @@ function App() {
|
|||
color: activeTab === 'er_diagram' ? '#fff' : '#aaa',
|
||||
fontWeight: activeTab === 'er_diagram' ? '500' : 'normal'
|
||||
}}>
|
||||
Data Entity
|
||||
ER Diagram
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -568,7 +568,7 @@ function App() {
|
|||
margin: '0 0 8px 0',
|
||||
color: '#ffffff'
|
||||
}}>
|
||||
Data Entity
|
||||
ER Diagram
|
||||
</h1>
|
||||
</div>
|
||||
<ERDiagramCanvas />
|
||||
|
|
|
|||
|
|
@ -1122,7 +1122,23 @@ const AddTableModal = ({
|
|||
</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>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import AddTableModal from './AddTableModal';
|
|||
|
||||
|
||||
|
||||
// Custom Table Node Component for Data Entity
|
||||
// Custom Table Node Component for ER Diagram
|
||||
const ERTableNode = ({ data, id }) => {
|
||||
const getTableIcon = () => {
|
||||
switch(data.table_type) {
|
||||
|
|
@ -357,14 +357,14 @@ const ERSchemaGroupNode = ({ data, id }) => {
|
|||
);
|
||||
};
|
||||
|
||||
// Node types for Data Entity
|
||||
// Node types for ER Diagram
|
||||
const nodeTypes = {
|
||||
erTable: ERTableNode,
|
||||
erSchemaGroup: ERSchemaGroupNode,
|
||||
erDatabaseWrapper: ERDatabaseWrapperNode,
|
||||
};
|
||||
|
||||
// Edge types for Data Entity
|
||||
// Edge types for ER Diagram
|
||||
const edgeTypes = {
|
||||
erRelationship: ERRelationshipEdge,
|
||||
};
|
||||
|
|
@ -461,7 +461,7 @@ const HierarchicalBreadcrumb = ({
|
|||
Qubit
|
||||
</Link>
|
||||
|
||||
<Typography color="text.secondary">Data Entity</Typography>
|
||||
<Typography color="text.secondary">ER Diagram</Typography>
|
||||
|
||||
{/* DBTEZ Services Dropdown */}
|
||||
<div
|
||||
|
|
@ -546,7 +546,7 @@ const HierarchicalBreadcrumb = ({
|
|||
);
|
||||
};
|
||||
|
||||
// Main Data Entity Canvas Component
|
||||
// Main ER Diagram Canvas Component
|
||||
const ERDiagramCanvasContent = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
|
@ -934,7 +934,7 @@ const ERDiagramCanvasContent = () => {
|
|||
|
||||
|
||||
|
||||
// Fetch databases and generate Data Entity using real API
|
||||
// Fetch databases and generate ER diagram using real API
|
||||
useEffect(() => {
|
||||
const fetchERData = async () => {
|
||||
try {
|
||||
|
|
@ -955,7 +955,7 @@ const ERDiagramCanvasContent = () => {
|
|||
selectedDataSource?.slug === db.con
|
||||
) || databases[0]; // Fallback to first database if no match
|
||||
|
||||
console.log('Selected database for Data Entity:', targetDatabase);
|
||||
console.log('Selected database for ER diagram:', targetDatabase);
|
||||
console.log('Available databases:', databases.map(db => ({ name: db.name, con: db.con })));
|
||||
|
||||
// Fetch complete structure (schemas, tables, columns) for the selected database
|
||||
|
|
@ -989,7 +989,7 @@ const ERDiagramCanvasContent = () => {
|
|||
generateERDiagram(completeDatabase);
|
||||
setIsUsingMockData(false);
|
||||
setApiError(null);
|
||||
console.log('Successfully fetched and generated Data Entity from API data');
|
||||
console.log('Successfully fetched and generated ER diagram from API data');
|
||||
|
||||
} else {
|
||||
throw new Error('No databases found from API');
|
||||
|
|
@ -1024,7 +1024,7 @@ const ERDiagramCanvasContent = () => {
|
|||
setDatabases([singleDbData]); // Wrap in array for compatibility
|
||||
setSelectedDatabase(singleDbData); // Set selected database for modal
|
||||
generateERDiagram(singleDbData);
|
||||
console.log('Using mock data for Data Entity due to API error');
|
||||
console.log('Using mock data for ER diagram due to API error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Unexpected error in fetchERData:', error);
|
||||
|
|
@ -1208,7 +1208,7 @@ const ERDiagramCanvasContent = () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Regenerate the Data Entity with the new table immediately
|
||||
// Regenerate the ER diagram with the new table immediately
|
||||
console.log('Scheduling diagram regeneration...');
|
||||
setTimeout(() => {
|
||||
console.log('Regenerating diagram with updated database...');
|
||||
|
|
@ -1259,9 +1259,9 @@ const ERDiagramCanvasContent = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Generate Data Entity with Database Wrapper structure
|
||||
// Generate ER diagram with Database Wrapper structure
|
||||
const generateERDiagram = (database) => {
|
||||
console.log('🔄 Starting Data Entity generation...');
|
||||
console.log('🔄 Starting ER diagram generation...');
|
||||
console.log('Database received:', database?.name);
|
||||
console.log('Total schemas:', database?.schemas?.length);
|
||||
console.log('Relationships received:', database?.relationships?.length);
|
||||
|
|
@ -1281,7 +1281,7 @@ const ERDiagramCanvasContent = () => {
|
|||
const newEdges = []; // Will be populated with relationship edges
|
||||
|
||||
// Process the selected database
|
||||
console.log('Generating Data Entity for database:', database?.name);
|
||||
console.log('Generating ER diagram for database:', database?.name);
|
||||
console.log('Total schemas in database:', database?.schemas?.length);
|
||||
console.log('Schema details:', database?.schemas?.map(s => ({
|
||||
name: s.name,
|
||||
|
|
@ -1613,7 +1613,7 @@ const ERDiagramCanvasContent = () => {
|
|||
setNodes(newNodes);
|
||||
setEdges(newEdges);
|
||||
|
||||
console.log('✅ Data Entity generation completed');
|
||||
console.log('✅ ER diagram generation completed');
|
||||
|
||||
// Update available schemas and existing tables for the modal
|
||||
if (database && database.schemas) {
|
||||
|
|
@ -1695,7 +1695,7 @@ const ERDiagramCanvasContent = () => {
|
|||
}}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<FaProjectDiagram style={{ fontSize: '48px', color: '#8a2be2', marginBottom: '16px' }} />
|
||||
<h3>Loading Data Entity...</h3>
|
||||
<h3>Loading ER Diagram...</h3>
|
||||
<p>Fetching schema for {selectedService?.name} - {selectedDataSource?.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,924 +0,0 @@
|
|||
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,
|
||||
FaSave as SaveIcon
|
||||
} from 'react-icons/fa';
|
||||
|
||||
const UpdateTableModal = ({
|
||||
open,
|
||||
onClose,
|
||||
onUpdateTable,
|
||||
tableData,
|
||||
schemas = [],
|
||||
existingTables = [],
|
||||
position = 'center',
|
||||
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');
|
||||
const [selectedKeyForColumns, setSelectedKeyForColumns] = useState(null);
|
||||
|
||||
// Populate form when modal opens with existing table data
|
||||
useEffect(() => {
|
||||
if (open && tableData) {
|
||||
populateFormWithTableData();
|
||||
fetchKeyTypes();
|
||||
fetchColumnTypes();
|
||||
}
|
||||
}, [open, tableData]);
|
||||
|
||||
// Populate form with existing table data
|
||||
const populateFormWithTableData = () => {
|
||||
if (!tableData) return;
|
||||
|
||||
setFormData({
|
||||
name: tableData.name || '',
|
||||
description: tableData.description || '',
|
||||
tableType: tableData.table_type || '',
|
||||
schema: tableData.schema || ''
|
||||
});
|
||||
|
||||
// Populate columns
|
||||
if (tableData.columns) {
|
||||
const formattedColumns = tableData.columns.map((col, index) => ({
|
||||
id: col.id || Date.now() + index,
|
||||
name: col.name || '',
|
||||
type: col.data_type || 'VARCHAR',
|
||||
isPrimaryKey: col.is_primary_key || false,
|
||||
isForeignKey: col.is_foreign_key || false,
|
||||
isNullable: col.is_nullable !== false
|
||||
}));
|
||||
setColumns(formattedColumns);
|
||||
}
|
||||
|
||||
// Populate keys (if available in tableData)
|
||||
if (tableData.keys) {
|
||||
const formattedKeys = tableData.keys.map((key, index) => ({
|
||||
id: key.id || Date.now() + index,
|
||||
name: key.name || '',
|
||||
keyType: key.key_type || 'PRIMARY',
|
||||
columnIds: key.column_ids || [],
|
||||
keyColumns: key.key_columns || []
|
||||
}));
|
||||
setKeys(formattedKeys);
|
||||
}
|
||||
|
||||
// Populate relations (if available in tableData)
|
||||
if (tableData.relations) {
|
||||
const formattedRelations = tableData.relations.map((rel, index) => ({
|
||||
id: rel.id || Date.now() + index,
|
||||
targetTable: rel.target_table || '',
|
||||
sourceKey: rel.source_key || '',
|
||||
targetKey: rel.target_key || '',
|
||||
tableKey: rel.table_key || '',
|
||||
relationType: rel.relation_type || '1:N'
|
||||
}));
|
||||
setRelations(formattedRelations);
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
setKeyTypes([
|
||||
{ kytp: 'PRIMARY', name: 'Primary' },
|
||||
{ kytp: 'FOREIGN', name: 'Foreign' },
|
||||
{ kytp: 'UNIQUE', name: 'Unique' }
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching key types:', error);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
setTableKeysFromAPI([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching table keys:', error);
|
||||
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
|
||||
}));
|
||||
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
|
||||
));
|
||||
|
||||
if (field === 'name' && errors.columns?.[id]) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
columns: {
|
||||
...prev.columns,
|
||||
[id]: null
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const removeColumn = (id) => {
|
||||
setColumns(prev => prev.filter(col => col.id !== id));
|
||||
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: [],
|
||||
keyType: defaultKeyType,
|
||||
keyColumns: []
|
||||
};
|
||||
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));
|
||||
};
|
||||
|
||||
// Relation management
|
||||
const addRelation = () => {
|
||||
const newRelation = {
|
||||
id: Date.now(),
|
||||
targetTable: '',
|
||||
sourceKey: '',
|
||||
targetKey: '',
|
||||
tableKey: '',
|
||||
relationType: '1:N'
|
||||
};
|
||||
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) => {
|
||||
updateRelation(relationId, 'targetTable', tableId);
|
||||
updateRelation(relationId, 'tableKey', '');
|
||||
|
||||
const selectedTable = existingTables.find(table => table.id === tableId);
|
||||
if (selectedTable && selectedTable.tbl) {
|
||||
fetchTableKeys(selectedTable.tbl);
|
||||
} else {
|
||||
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'
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Form validation
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = 'Table name is required';
|
||||
}
|
||||
|
||||
if (!formData.tableType) {
|
||||
newErrors.tableType = 'Table type is required';
|
||||
}
|
||||
|
||||
if (columns.length === 0) {
|
||||
newErrors.columns = 'At least one column is required';
|
||||
} else {
|
||||
const columnErrors = {};
|
||||
columns.forEach(col => {
|
||||
if (!col.name.trim()) {
|
||||
columnErrors[col.id] = 'Column name is required';
|
||||
}
|
||||
});
|
||||
if (Object.keys(columnErrors).length > 0) {
|
||||
newErrors.columns = columnErrors;
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const tableUpdateData = {
|
||||
id: tableData.id,
|
||||
name: formData.name.trim(),
|
||||
description: formData.description.trim(),
|
||||
table_type: formData.tableType,
|
||||
schema: formData.schema,
|
||||
columns: columns.map(col => ({
|
||||
id: col.id,
|
||||
name: col.name.trim(),
|
||||
data_type: col.type,
|
||||
is_primary_key: col.isPrimaryKey,
|
||||
is_foreign_key: col.isForeignKey,
|
||||
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
|
||||
}))
|
||||
};
|
||||
|
||||
await onUpdateTable(tableUpdateData);
|
||||
onClose();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating table:', error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="lg"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
height: '90vh',
|
||||
maxHeight: '90vh',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider'
|
||||
}}>
|
||||
<TableIcon />
|
||||
Update Table: {tableData?.name}
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
sx={{ ml: 'auto' }}
|
||||
size="small"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ p: 0, overflow: 'hidden' }}>
|
||||
<Box sx={{ p: 3, height: '100%', overflow: 'auto' }}>
|
||||
{/* Basic Information */}
|
||||
<Paper variant="outlined" sx={{ p: 2, mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TableIcon />
|
||||
Basic Information
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<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}
|
||||
size="small"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<FormControl fullWidth size="small" error={!!errors.tableType}>
|
||||
<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}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Description"
|
||||
value={formData.description}
|
||||
onChange={(e) => handleFormChange('description', e.target.value)}
|
||||
multiline
|
||||
rows={2}
|
||||
size="small"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
|
||||
{/* Columns Section */}
|
||||
<Paper variant="outlined" sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<FaColumns />
|
||||
Columns ({columns.length})
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={addColumn}
|
||||
>
|
||||
Add Column
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{errors.columns && typeof errors.columns === 'string' && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{errors.columns}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{columns.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
|
||||
No columns defined yet. Click "Add Column" to create table columns.
|
||||
</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={3}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Column Name"
|
||||
value={column.name}
|
||||
onChange={(e) => updateColumn(column.id, 'name', e.target.value)}
|
||||
error={!!errors.columns?.[column.id]}
|
||||
helperText={errors.columns?.[column.id]}
|
||||
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"
|
||||
>
|
||||
{(columnTypesFromAPI.length > 0 ? columnTypesFromAPI : columnTypes.map(type => ({ name: type }))).map(type => (
|
||||
<MenuItem key={type.name} value={type.name}>
|
||||
{type.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<FormControl size="small">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Checkbox
|
||||
checked={column.isPrimaryKey}
|
||||
onChange={(e) => updateColumn(column.id, 'isPrimaryKey', e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
<Typography variant="caption">Primary</Typography>
|
||||
</Box>
|
||||
</FormControl>
|
||||
<FormControl size="small">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Checkbox
|
||||
checked={column.isForeignKey}
|
||||
onChange={(e) => updateColumn(column.id, 'isForeignKey', e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
<Typography variant="caption">Foreign</Typography>
|
||||
</Box>
|
||||
</FormControl>
|
||||
<FormControl size="small">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Checkbox
|
||||
checked={column.isNullable}
|
||||
onChange={(e) => updateColumn(column.id, 'isNullable', e.target.checked)}
|
||||
size="small"
|
||||
/>
|
||||
<Typography variant="caption">Nullable</Typography>
|
||||
</Box>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={2}>
|
||||
<IconButton
|
||||
onClick={() => removeColumn(column.id)}
|
||||
color="error"
|
||||
size="small"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Keys Section */}
|
||||
<Paper variant="outlined" sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<KeyIcon />
|
||||
Keys ({keys.length})
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={addKey}
|
||||
disabled={columns.length === 0}
|
||||
>
|
||||
Add Key
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{keys.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
|
||||
{columns.length === 0
|
||||
? "Define columns first before creating keys."
|
||||
: "No keys defined yet. Click \"Add Key\" to create table keys."
|
||||
}
|
||||
</Typography>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{keys.map((key) => (
|
||||
<Paper key={key.id} variant="outlined" sx={{ p: 2 }}>
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Key Name"
|
||||
value={key.name}
|
||||
onChange={(e) => updateKey(key.id, 'name', e.target.value)}
|
||||
size="small"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={3}>
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Key Type</InputLabel>
|
||||
<Select
|
||||
value={key.keyType}
|
||||
onChange={(e) => updateKey(key.id, 'keyType', e.target.value)}
|
||||
label="Key Type"
|
||||
disabled={loadingKeyTypes}
|
||||
>
|
||||
{keyTypes.map(type => (
|
||||
<MenuItem key={type.kytp} value={type.kytp}>
|
||||
{type.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Columns: {key.keyColumns?.length || 0} selected
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={1}>
|
||||
<IconButton
|
||||
onClick={() => removeKey(key.id)}
|
||||
color="error"
|
||||
size="small"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Relations Section */}
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<LinkIcon />
|
||||
Relations ({relations.length})
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={addRelation}
|
||||
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}
|
||||
>
|
||||
{/* This would need to be populated based on target table keys */}
|
||||
</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={1.5}>
|
||||
<IconButton
|
||||
onClick={() => removeRelation(relation.id)}
|
||||
color="error"
|
||||
size="small"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ p: 3, gap: 1 }}>
|
||||
<Button onClick={handleClose} variant="outlined">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="contained"
|
||||
disabled={isSubmitting}
|
||||
startIcon={isSubmitting ? null : <SaveIcon />}
|
||||
>
|
||||
{isSubmitting ? 'Updating Table...' : 'Update Table'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateTableModal;
|
||||
Loading…
Reference in New Issue