Compare commits

..

2 Commits

4 changed files with 575 additions and 112 deletions

View File

@ -17,7 +17,15 @@ import {
Grid,
Paper,
Chip,
Alert
Alert,
Autocomplete,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Checkbox
} from '@mui/material';
import {
FaPlus as AddIcon,
@ -25,7 +33,8 @@ import {
FaTimes as CloseIcon,
FaTable as TableIcon,
FaKey as KeyIcon,
FaLink as LinkIcon
FaLink as LinkIcon,
FaArrowLeft as BackIcon
} from 'react-icons/fa';
const AddTableModal = ({
@ -73,6 +82,10 @@ const AddTableModal = ({
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) {
@ -92,6 +105,8 @@ const AddTableModal = ({
setRelations([]);
setErrors({});
setIsSubmitting(false);
setKeysViewMode('keys');
setSelectedKeyForColumns(null);
};
// Form field handlers
@ -126,12 +141,38 @@ const AddTableModal = ({
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
setKeys(prev => prev.filter(key => key.columnId !== 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
@ -139,9 +180,9 @@ const AddTableModal = ({
const newKey = {
id: Date.now(),
name: '',
columnId: '',
sequence: 1,
keyType: 'PRIMARY' // PRIMARY, FOREIGN, UNIQUE
columnIds: [], // Changed to array for multi-select
keyType: 'PRIMARY', // PRIMARY, FOREIGN, UNIQUE
keyColumns: [] // Array of {columnId, sequence} objects
};
setKeys(prev => [...prev, newKey]);
};
@ -156,6 +197,70 @@ const AddTableModal = ({
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 = {
@ -181,11 +286,18 @@ const AddTableModal = ({
// Get available keys for relations
const getAvailableKeys = () => {
return keys.map(key => {
const column = columns.find(col => col.id === key.columnId);
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: column?.name || 'Unknown Column'
columnName: keyColumnNames || 'No columns selected'
};
});
};
@ -222,9 +334,27 @@ const AddTableModal = ({
// Validate columns
const columnErrors = {};
const columnNames = new Set();
const duplicateNames = new Set();
// First pass: identify duplicate names
columns.forEach(col => {
if (!col.name.trim()) {
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';
}
});
@ -238,8 +368,15 @@ const AddTableModal = ({
if (!key.name.trim()) {
keyErrors[key.id] = 'Key name is required';
}
if (!key.columnId) {
keyErrors[key.id] = 'Column selection 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(', ')}`;
}
}
});
@ -269,16 +406,22 @@ const AddTableModal = ({
columns: columns.map(col => ({
name: col.name.trim(),
data_type: col.type,
is_primary_key: keys.some(key => key.columnId === col.id && key.keyType === 'PRIMARY'),
is_foreign_key: keys.some(key => key.columnId === col.id && key.keyType === 'FOREIGN'),
is_primary_key: keys.some(key =>
key.keyColumns?.some(kc => kc.columnId === col.id) && key.keyType === 'PRIMARY'
),
is_foreign_key: keys.some(key =>
key.keyColumns?.some(kc => kc.columnId === col.id) && key.keyType === 'FOREIGN'
),
is_nullable: col.isNullable
})),
keys: keys.map(key => ({
name: key.name.trim(),
column_name: columns.find(col => col.id === key.columnId)?.name || '',
key_type: key.keyType,
sequence: key.sequence
})),
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,
@ -429,8 +572,11 @@ const AddTableModal = ({
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]}
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>
@ -483,95 +629,227 @@ const AddTableModal = ({
<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>
<Button
startIcon={<AddIcon />}
onClick={addKey}
variant="outlined"
size="small"
disabled={columns.length === 0}
>
Add Key
</Button>
{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>
{keys.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
{columns.length === 0
? "Add columns first before defining keys."
: "No keys defined yet. Click \"Add Key\" to create primary or foreign keys."
}
</Typography>
{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()}
>
<MenuItem value="PRIMARY">Primary Key</MenuItem>
<MenuItem value="FOREIGN">Foreign Key</MenuItem>
<MenuItem value="UNIQUE">Unique 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>
) : (
<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={3}>
<TextField
fullWidth
label="Key Name"
value={key.name}
onChange={(e) => updateKey(key.id, 'name', e.target.value)}
error={!!errors.keys?.[key.id]}
helperText={errors.keys?.[key.id]}
size="small"
/>
</Grid>
<Grid item xs={12} md={3}>
<FormControl fullWidth size="small">
<InputLabel>Column</InputLabel>
<Select
value={key.columnId}
onChange={(e) => updateKey(key.id, 'columnId', e.target.value)}
label="Column"
>
{columns.map(col => (
<MenuItem key={col.id} value={col.id}>
{col.name || 'Unnamed Column'}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={2}>
<FormControl fullWidth size="small">
<InputLabel>Key Type</InputLabel>
<Select
value={key.keyType}
onChange={(e) => updateKey(key.id, 'keyType', e.target.value)}
label="Key Type"
>
<MenuItem value="PRIMARY">Primary</MenuItem>
<MenuItem value="FOREIGN">Foreign</MenuItem>
<MenuItem value="UNIQUE">Unique</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={2}>
<TextField
fullWidth
label="Sequence"
type="number"
value={key.sequence}
onChange={(e) => updateKey(key.id, 'sequence', parseInt(e.target.value) || 1)}
size="small"
inputProps={{ min: 1 }}
/>
</Grid>
<Grid item xs={12} md={2}>
<IconButton
onClick={() => removeKey(key.id)}
color="error"
size="small"
>
<DeleteIcon />
</IconButton>
</Grid>
</Grid>
</Paper>
))}
</Box>
// 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>
<TableCell width="100">Actions</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>
<TableCell>
{isSelected && (
<IconButton
size="small"
color="error"
onClick={() => removeColumnFromKey(selectedKeyForColumns, column.id)}
>
<DeleteIcon />
</IconButton>
)}
</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>
<TableCell>
<AddIcon style={{ color: '#ccc' }} />
</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>

View File

@ -1229,11 +1229,8 @@ const ERDiagramCanvasContent = () => {
const rows = Math.ceil(tableCount / tablesPerRow);
// Calculate schema dimensions with proper padding for tables
const tableWidth = 260; // Width of each table node
const tableHeight = 280; // Height of each table node (including spacing)
const tableSpacingCalc = 330; // Horizontal spacing between tables
const tableRowSpacingCalc = 320; // Vertical spacing between table rows
const schemaPadding = 200; // Padding around the schema content
// Calculate required width and height based on table layout
const tableStartX = 90; // Starting X position within schema
@ -1241,8 +1238,8 @@ const ERDiagramCanvasContent = () => {
const requiredWidth = tableStartX + (tablesPerRow * tableSpacingCalc) + 100; // Extra margin
const requiredHeight = tableStartY + (rows * tableRowSpacingCalc) + 100; // Extra margin
const schemaWidth = Math.max(900, requiredWidth);
const schemaHeight = Math.max(650, requiredHeight);
const schemaWidth = Math.max(1000, requiredWidth);
const schemaHeight = Math.max(850, requiredHeight);
console.log(`Schema "${schema.name || schema.sch}" layout: ${tableCount} tables, ${tablesPerRow} per row, ${rows} rows`);
console.log(`Required dimensions: ${requiredWidth}x${requiredHeight}, Final: ${schemaWidth}x${schemaHeight}`);

View File

@ -0,0 +1,143 @@
import React, { useState } from 'react';
import { createSchema } from './mockData';
const SchemaCreationExample = () => {
// State for form inputs
const [schemaName, setSchemaName] = useState('');
const [schemaDescription, setSchemaDescription] = useState('');
const [dbSlug, setDbSlug] = useState('my_dwh'); // Default to my_dwh
// State for API response
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const [createdSchema, setCreatedSchema] = useState(null);
// Handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
// Reset states
setLoading(true);
setError(null);
setSuccess(false);
setCreatedSchema(null);
try {
// Call the createSchema function
const newSchema = await createSchema(dbSlug, schemaName, schemaDescription);
// Handle success
setSuccess(true);
setCreatedSchema(newSchema);
// Clear form
setSchemaName('');
setSchemaDescription('');
} catch (err) {
// Handle error
setError(err.message || 'An error occurred while creating the schema');
} finally {
setLoading(false);
}
};
return (
<div className="schema-creation-container">
<h2>Create New Schema</h2>
{/* Form for schema creation */}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="dbSlug">Database:</label>
<select
id="dbSlug"
value={dbSlug}
onChange={(e) => setDbSlug(e.target.value)}
required
>
<option value="my_dwh">MyDataWarehouseDB</option>
<option value="my_dwh2">Service 2 DB (No schemas allowed)</option>
</select>
</div>
<div className="form-group">
<label htmlFor="schemaName">Schema Name:</label>
<input
type="text"
id="schemaName"
value={schemaName}
onChange={(e) => setSchemaName(e.target.value)}
required
placeholder="Enter schema name"
/>
</div>
<div className="form-group">
<label htmlFor="schemaDescription">Description:</label>
<textarea
id="schemaDescription"
value={schemaDescription}
onChange={(e) => setSchemaDescription(e.target.value)}
placeholder="Enter schema description (optional)"
rows={3}
/>
</div>
<button
type="submit"
disabled={loading}
className="submit-button"
>
{loading ? 'Creating...' : 'Create Schema'}
</button>
</form>
{/* Display error message if any */}
{error && (
<div className="error-message">
<p>Error: {error}</p>
</div>
)}
{/* Display success message */}
{success && (
<div className="success-message">
<p>Schema created successfully!</p>
{createdSchema && (
<div className="schema-details">
<h3>Created Schema Details:</h3>
<p><strong>Name:</strong> {createdSchema.name}</p>
<p><strong>Slug:</strong> {createdSchema.slug || 'Not provided by API'}</p>
<p><strong>Description:</strong> {createdSchema.description || 'None'}</p>
<p><strong>Database:</strong> {createdSchema.database}</p>
<p><strong>Created At:</strong> {createdSchema.created_at}</p>
</div>
)}
</div>
)}
{/* Example API call code */}
<div className="api-example">
<h3>API Call Example:</h3>
<pre>
{`// Example API call to create a schema
const createNewSchema = async () => {
try {
const newSchema = await createSchema(
'my_dwh', // Database slug
'Schema3', // Schema name
'This is a test schema.' // Schema description
);
console.log('Schema created:', newSchema);
} catch (error) {
console.error('Error creating schema:', error);
}
};`}
</pre>
</div>
</div>
);
};
export default SchemaCreationExample;

View File

@ -0,0 +1,45 @@
// Example script to demonstrate how to use the createSchema function
import { createSchema } from '../components/mockData';
/**
* This example shows how to create a new schema using the createSchema function
* It can be run directly or used as a reference for implementing schema creation
* in your application.
*/
async function createSchemaExample() {
try {
console.log('Starting schema creation example...');
// Example payload from the requirements
const dbSlug = 'my_dwh';
const schemaName = 'Schema3';
const schemaDescription = 'This is a test schema.';
console.log(`Creating schema "${schemaName}" in database "${dbSlug}"...`);
// Call the createSchema function
const newSchema = await createSchema(dbSlug, schemaName, schemaDescription);
console.log('Schema created successfully!');
console.log('New schema details:', newSchema);
return newSchema;
} catch (error) {
console.error('Error in schema creation example:', error);
throw error;
}
}
// Execute the example if this script is run directly
if (typeof require !== 'undefined' && require.main === module) {
createSchemaExample()
.then(result => {
console.log('Example completed successfully');
})
.catch(error => {
console.error('Example failed:', error);
process.exit(1);
});
}
export default createSchemaExample;