feat: implement database schema discovery and management services with frontend integration
This commit is contained in:
@@ -48,4 +48,34 @@ interface DatabaseDriverInterface
|
|||||||
* Get database metadata (charset, collation, size, etc.)
|
* Get database metadata (charset, collation, size, etc.)
|
||||||
*/
|
*/
|
||||||
public function getDatabaseMetadata(string $database): array;
|
public function getDatabaseMetadata(string $database): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get table metadata.
|
||||||
|
*/
|
||||||
|
public function getTableMetadata(string $database, string $table): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get metadata for all tables in a database.
|
||||||
|
*/
|
||||||
|
public function getTablesMetadata(string $database): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate a table.
|
||||||
|
*/
|
||||||
|
public function truncateTable(string $table): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop a table.
|
||||||
|
*/
|
||||||
|
public function dropTable(string $table): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimize a table.
|
||||||
|
*/
|
||||||
|
public function optimizeTable(string $table): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform batch update on a table.
|
||||||
|
*/
|
||||||
|
public function batchUpdate(string $table, array $changes): bool;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,4 +248,19 @@ class SchemaController extends Controller
|
|||||||
return Response::json(['error' => $e->getMessage()], 400);
|
return Response::json(['error' => $e->getMessage()], 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function batchUpdate(Request $request, $table)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'changes' => 'required|array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->initializeDriver($request);
|
||||||
|
$this->databaseService->batchUpdate($table, $request->changes);
|
||||||
|
return Response::json(['message' => 'Batch update successful']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return Response::json(['error' => $e->getMessage()], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,4 +302,41 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface
|
|||||||
|
|
||||||
return $this->query($sql, [$database]);
|
return $this->query($sql, [$database]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function batchUpdate(string $table, array $changes): bool
|
||||||
|
{
|
||||||
|
$connection = $this->getConnection();
|
||||||
|
|
||||||
|
// Find primary key
|
||||||
|
$schema = $this->getTableSchema($table);
|
||||||
|
$primaryKey = 'id'; // default
|
||||||
|
foreach ($schema as $col) {
|
||||||
|
if (($col->Key ?? '') === 'PRI') {
|
||||||
|
$primaryKey = $col->Field;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection->beginTransaction();
|
||||||
|
try {
|
||||||
|
foreach ($changes as $change) {
|
||||||
|
if (!isset($change[$primaryKey])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $change[$primaryKey];
|
||||||
|
$updateData = $change;
|
||||||
|
unset($updateData[$primaryKey]);
|
||||||
|
|
||||||
|
if (empty($updateData)) continue;
|
||||||
|
|
||||||
|
$connection->table($table)->where($primaryKey, $id)->update($updateData);
|
||||||
|
}
|
||||||
|
$connection->commit();
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$connection->rollBack();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,4 +156,12 @@ class DatabaseService
|
|||||||
{
|
{
|
||||||
return $this->getDriver()->getTablesMetadata($database);
|
return $this->getDriver()->getTablesMetadata($database);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform batch update on a table.
|
||||||
|
*/
|
||||||
|
public function batchUpdate(string $table, array $changes): bool
|
||||||
|
{
|
||||||
|
return $this->getDriver()->batchUpdate($table, $changes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Route::prefix('schema')->group(function () {
|
|||||||
Route::post('/bulk-action', [SchemaController::class, 'bulkAction']);
|
Route::post('/bulk-action', [SchemaController::class, 'bulkAction']);
|
||||||
Route::get('/{table}', [SchemaController::class, 'schema']);
|
Route::get('/{table}', [SchemaController::class, 'schema']);
|
||||||
Route::get('/{table}/data', [SchemaController::class, 'data']);
|
Route::get('/{table}/data', [SchemaController::class, 'data']);
|
||||||
|
Route::post('/{table}/batch-update', [SchemaController::class, 'batchUpdate']);
|
||||||
Route::post('/execute', [SchemaController::class, 'execute']);
|
Route::post('/execute', [SchemaController::class, 'execute']);
|
||||||
Route::post('/export', [SchemaController::class, 'export']);
|
Route::post('/export', [SchemaController::class, 'export']);
|
||||||
Route::post('/import', [SchemaController::class, 'import']);
|
Route::post('/import', [SchemaController::class, 'import']);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -8,17 +8,21 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
AlertTitle,
|
AlertTitle,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab
|
Tab,
|
||||||
|
Button
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
TableChart,
|
TableChart,
|
||||||
Terminal,
|
Terminal,
|
||||||
CloudDownload,
|
CloudDownload,
|
||||||
CloudUpload,
|
CloudUpload,
|
||||||
Info
|
Info,
|
||||||
|
Save,
|
||||||
|
Undo
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { DataGrid, type GridPaginationModel } from '@mui/x-data-grid';
|
import { DataGrid, type GridPaginationModel, type GridRowModel } from '@mui/x-data-grid';
|
||||||
import { useAppStore } from '../store/useAppStore';
|
import { useAppStore } from '../store/useAppStore';
|
||||||
|
import { SchemaService } from '../services/api';
|
||||||
import TransferContent from './TransferContent';
|
import TransferContent from './TransferContent';
|
||||||
import DatabaseTablesGrid from './DatabaseTablesGrid';
|
import DatabaseTablesGrid from './DatabaseTablesGrid';
|
||||||
import TechnicalOverview from './TechnicalOverview';
|
import TechnicalOverview from './TechnicalOverview';
|
||||||
@@ -42,11 +46,180 @@ const MainContent: React.FC = () => {
|
|||||||
severity: 'error'
|
severity: 'error'
|
||||||
});
|
});
|
||||||
|
|
||||||
const { columns, loading: loadingSchema } = useTableSchema(activeTable, activeDatabase);
|
const [pendingChanges, setPendingChanges] = useState<Record<string, any>>({});
|
||||||
const { rows, rowCount, loading: loadingData } = useTableData(activeTable, activeDatabase, paginationModel);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [cellFocus, setCellFocus] = useState<{ id: string | number, field: string } | null>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [dragStart, setDragStart] = useState<{ id: string | number, field: string } | null>(null);
|
||||||
|
const [dragCurrent, setDragCurrent] = useState<{ id: string | number, field: string } | null>(null);
|
||||||
|
|
||||||
|
const { columns, loading: loadingSchema, primaryKey } = useTableSchema(activeTable, activeDatabase);
|
||||||
|
const { rows, rowCount, loading: loadingData, refetch } = useTableData(activeTable, activeDatabase, paginationModel);
|
||||||
|
|
||||||
|
const handleRowUpdate = (newRow: GridRowModel, oldRow: GridRowModel) => {
|
||||||
|
const hasChanged = Object.keys(newRow).some(key => newRow[key] !== oldRow[key]);
|
||||||
|
if (hasChanged) {
|
||||||
|
const rowId = newRow[primaryKey];
|
||||||
|
setPendingChanges(prev => ({
|
||||||
|
...prev,
|
||||||
|
[rowId]: {
|
||||||
|
...(prev[rowId] || {}),
|
||||||
|
...newRow
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return newRow;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBatchSave = async () => {
|
||||||
|
if (!activeTable) return;
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
const changes = Object.values(pendingChanges);
|
||||||
|
await SchemaService.batchUpdate(activeTable, changes);
|
||||||
|
setNotification({
|
||||||
|
open: true,
|
||||||
|
title: 'Batch Update Success',
|
||||||
|
message: `Successfully updated ${changes.length} records.`,
|
||||||
|
severity: 'success'
|
||||||
|
});
|
||||||
|
setPendingChanges({});
|
||||||
|
refetch(); // Refresh data from server to be sure
|
||||||
|
} catch (error: any) {
|
||||||
|
setNotification({
|
||||||
|
open: true,
|
||||||
|
title: 'Update Failed',
|
||||||
|
message: error.response?.data?.error || error.message,
|
||||||
|
severity: 'error'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaste = useCallback((event: React.ClipboardEvent) => {
|
||||||
|
if (!cellFocus || !activeTable) return;
|
||||||
|
|
||||||
|
const pasteData = event.clipboardData.getData('text');
|
||||||
|
const rows_data = pasteData.split(/\r?\n/).filter(line => line.length > 0).map(line => line.split('\t'));
|
||||||
|
|
||||||
|
if (rows_data.length === 0) return;
|
||||||
|
|
||||||
|
const updatedChanges = { ...pendingChanges };
|
||||||
|
const focusedRowIndex = rows.findIndex(r => r[primaryKey] === cellFocus.id);
|
||||||
|
const focusedColIndex = columns.findIndex(c => c.field === cellFocus.field);
|
||||||
|
|
||||||
|
if (focusedRowIndex === -1 || focusedColIndex === -1) return;
|
||||||
|
|
||||||
|
rows_data.forEach((rowData, rIdx) => {
|
||||||
|
const targetRowIndex = focusedRowIndex + rIdx;
|
||||||
|
if (targetRowIndex >= rows.length) return;
|
||||||
|
|
||||||
|
const targetRow = rows[targetRowIndex];
|
||||||
|
const targetRowId = targetRow[primaryKey];
|
||||||
|
|
||||||
|
const newRowData = { ...(updatedChanges[targetRowId] || {}), [primaryKey]: targetRowId };
|
||||||
|
|
||||||
|
rowData.forEach((cellData, cIdx) => {
|
||||||
|
const targetColIndex = focusedColIndex + cIdx;
|
||||||
|
if (targetColIndex >= columns.length) return;
|
||||||
|
|
||||||
|
const targetCol = columns[targetColIndex];
|
||||||
|
if (targetCol.editable !== false && targetCol.field !== primaryKey) {
|
||||||
|
newRowData[targetCol.field] = cellData;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updatedChanges[targetRowId] = newRowData;
|
||||||
|
});
|
||||||
|
|
||||||
|
setPendingChanges(updatedChanges);
|
||||||
|
event.preventDefault();
|
||||||
|
}, [cellFocus, rows, columns, primaryKey, pendingChanges, activeTable]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
|
||||||
|
// Excel Ctrl+D support (Fill Down)
|
||||||
|
if (event.ctrlKey && event.key === 'd') {
|
||||||
|
if (!cellFocus || !activeTable) return;
|
||||||
|
|
||||||
|
const focusedRowIndex = rows.findIndex(r => r[primaryKey] === cellFocus.id);
|
||||||
|
if (focusedRowIndex === -1 || focusedRowIndex + 1 >= rows.length) return;
|
||||||
|
|
||||||
|
const valueToCopy = (pendingChanges[cellFocus.id]?.[cellFocus.field]) ?? rows[focusedRowIndex][cellFocus.field];
|
||||||
|
const nextRow = rows[focusedRowIndex + 1];
|
||||||
|
const nextRowId = nextRow[primaryKey];
|
||||||
|
|
||||||
|
setPendingChanges(prev => ({
|
||||||
|
...prev,
|
||||||
|
[nextRowId]: {
|
||||||
|
...(prev[nextRowId] || {}),
|
||||||
|
[primaryKey]: nextRowId,
|
||||||
|
[cellFocus.field]: valueToCopy
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, [cellFocus, rows, pendingChanges, primaryKey, activeTable]);
|
||||||
|
|
||||||
|
const handleCellMouseDown = useCallback((params: any, event: React.MouseEvent) => {
|
||||||
|
// Only start drag if clicking near the bottom-right corner (simulating fill handle)
|
||||||
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
|
||||||
|
// Check if click is in the bottom-right corner (approx 10x10px)
|
||||||
|
if (x > rect.width - 12 && y > rect.height - 12) {
|
||||||
|
setIsDragging(true);
|
||||||
|
setDragStart({ id: params.id, field: params.field });
|
||||||
|
setDragCurrent({ id: params.id, field: params.field });
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCellMouseEnter = useCallback((params: any) => {
|
||||||
|
if (isDragging) {
|
||||||
|
setDragCurrent({ id: params.id, field: params.field });
|
||||||
|
}
|
||||||
|
}, [isDragging]);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
if (isDragging && dragStart && dragCurrent && dragStart.field === dragCurrent.field) {
|
||||||
|
const startIndex = rows.findIndex(r => r[primaryKey] === dragStart.id);
|
||||||
|
const endIndex = rows.findIndex(r => r[primaryKey] === dragCurrent.id);
|
||||||
|
|
||||||
|
if (startIndex !== -1 && endIndex !== -1) {
|
||||||
|
const start = Math.min(startIndex, endIndex);
|
||||||
|
const end = Math.max(startIndex, endIndex);
|
||||||
|
|
||||||
|
const valueToFill = (pendingChanges[dragStart.id]?.[dragStart.field]) ?? rows[startIndex][dragStart.field];
|
||||||
|
const updatedChanges = { ...pendingChanges };
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
const targetRowId = rows[i][primaryKey];
|
||||||
|
updatedChanges[targetRowId] = {
|
||||||
|
...(updatedChanges[targetRowId] || {}),
|
||||||
|
[primaryKey]: targetRowId,
|
||||||
|
[dragStart.field]: valueToFill
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setPendingChanges(updatedChanges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsDragging(false);
|
||||||
|
setDragStart(null);
|
||||||
|
setDragCurrent(null);
|
||||||
|
}, [isDragging, dragStart, dragCurrent, rows, primaryKey, pendingChanges]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('mouseup', handleMouseUp);
|
||||||
|
return () => window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
}, [handleMouseUp]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPaginationModel({ page: 0, pageSize: 100 });
|
setPaginationModel({ page: 0, pageSize: 100 });
|
||||||
|
setPendingChanges({});
|
||||||
}, [activeTable]);
|
}, [activeTable]);
|
||||||
|
|
||||||
const handleCloseNotification = () => setNotification({ ...notification, open: false });
|
const handleCloseNotification = () => setNotification({ ...notification, open: false });
|
||||||
@@ -116,6 +289,33 @@ const MainContent: React.FC = () => {
|
|||||||
<Typography variant="caption" sx={{ fontWeight: 700 }}>{rowCount} rows found</Typography>
|
<Typography variant="caption" sx={{ fontWeight: 700 }}>{rowCount} rows found</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{Object.keys(pendingChanges).length > 0 && (
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
size="small"
|
||||||
|
startIcon={<Save />}
|
||||||
|
onClick={handleBatchSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
sx={{ borderRadius: 2, fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
{isSaving ? <CircularProgress size={20} color="inherit" /> : `Save ${Object.keys(pendingChanges).length} Changes`}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
startIcon={<Undo />}
|
||||||
|
onClick={() => setPendingChanges({})}
|
||||||
|
disabled={isSaving}
|
||||||
|
sx={{ borderRadius: 2 }}
|
||||||
|
>
|
||||||
|
Discard
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Paper sx={{
|
<Paper sx={{
|
||||||
@@ -135,7 +335,11 @@ const MainContent: React.FC = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows.map(row => ({
|
||||||
|
...row,
|
||||||
|
...(pendingChanges[row[primaryKey]] || {})
|
||||||
|
}))}
|
||||||
|
getRowId={(row) => row[primaryKey]}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
loading={loadingData}
|
loading={loadingData}
|
||||||
@@ -143,14 +347,43 @@ const MainContent: React.FC = () => {
|
|||||||
paginationModel={paginationModel}
|
paginationModel={paginationModel}
|
||||||
onPaginationModelChange={setPaginationModel}
|
onPaginationModelChange={setPaginationModel}
|
||||||
pageSizeOptions={[25, 50, 100]}
|
pageSizeOptions={[25, 50, 100]}
|
||||||
|
processRowUpdate={handleRowUpdate}
|
||||||
|
onProcessRowUpdateError={(error) => {
|
||||||
|
console.error('Row update error:', error);
|
||||||
|
}}
|
||||||
|
onCellFocusIn={(params) => setCellFocus({ id: params.id, field: params.field })}
|
||||||
|
onCellMouseDown={handleCellMouseDown}
|
||||||
|
onCellMouseEnter={handleCellMouseEnter}
|
||||||
|
slotProps={{
|
||||||
|
root: {
|
||||||
|
onPaste: handlePaste,
|
||||||
|
onKeyDown: handleKeyDown,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
density="compact"
|
||||||
sx={{
|
sx={{
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
'& .MuiDataGrid-columnHeaderTitle': {
|
||||||
|
fontWeight: 800,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.5px'
|
||||||
|
},
|
||||||
|
'& .MuiDataGrid-cell': {
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
px: 1,
|
||||||
|
borderRight: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
},
|
||||||
|
'& .MuiDataGrid-row': {
|
||||||
|
minHeight: '32px !important',
|
||||||
|
maxHeight: '32px !important',
|
||||||
|
},
|
||||||
'& .MuiDataGrid-row:nth-of-type(even)': {
|
'& .MuiDataGrid-row:nth-of-type(even)': {
|
||||||
bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.01)',
|
bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.01)',
|
||||||
},
|
},
|
||||||
'& .MuiDataGrid-cell': {
|
'& .MuiDataGrid-cell--editable': {
|
||||||
borderRight: '1px solid',
|
bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(0, 255, 0, 0.02)' : 'rgba(0, 255, 0, 0.01)',
|
||||||
borderColor: 'divider',
|
|
||||||
},
|
},
|
||||||
'& .MuiDataGrid-columnHeader': {
|
'& .MuiDataGrid-columnHeader': {
|
||||||
borderRight: '1px solid',
|
borderRight: '1px solid',
|
||||||
@@ -161,6 +394,37 @@ const MainContent: React.FC = () => {
|
|||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
},
|
},
|
||||||
'& .MuiDataGrid-row:hover': { bgcolor: 'rgba(0, 97, 255, 0.04)' },
|
'& .MuiDataGrid-row:hover': { bgcolor: 'rgba(0, 97, 255, 0.04)' },
|
||||||
|
'& .MuiDataGrid-cell:focus-within': {
|
||||||
|
outline: '2px solid #0061ff',
|
||||||
|
outlineOffset: '-2px',
|
||||||
|
position: 'relative',
|
||||||
|
'&::after': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -1,
|
||||||
|
right: -1,
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
bgcolor: '#0061ff',
|
||||||
|
border: '1px solid white',
|
||||||
|
cursor: 'crosshair',
|
||||||
|
zIndex: 10,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'& .MuiDataGrid-cell--dragging': {
|
||||||
|
bgcolor: 'rgba(0, 97, 255, 0.08) !important',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
getCellClassName={(params) => {
|
||||||
|
if (isDragging && dragStart && dragCurrent && params.field === dragStart.field) {
|
||||||
|
const startIndex = rows.findIndex(r => r[primaryKey] === dragStart.id);
|
||||||
|
const endIndex = rows.findIndex(r => r[primaryKey] === dragCurrent.id);
|
||||||
|
const currentIndex = rows.findIndex(r => r[primaryKey] === params.id);
|
||||||
|
if (currentIndex >= Math.min(startIndex, endIndex) && currentIndex <= Math.max(startIndex, endIndex)) {
|
||||||
|
return 'MuiDataGrid-cell--dragging';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const mapSqlTypeToMuiType = (sqlType: string): 'string' | 'number' | 'date' | 'd
|
|||||||
export const useTableSchema = (activeTable: string | null, activeDatabase: string | null) => {
|
export const useTableSchema = (activeTable: string | null, activeDatabase: string | null) => {
|
||||||
const [columns, setColumns] = useState<GridColDef[]>([]);
|
const [columns, setColumns] = useState<GridColDef[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [primaryKey, setPrimaryKey] = useState<string>('id');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSchema = async () => {
|
const fetchSchema = async () => {
|
||||||
@@ -22,7 +23,9 @@ export const useTableSchema = (activeTable: string | null, activeDatabase: strin
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const schemaRes = await SchemaService.getTableSchema(activeTable, activeDatabase);
|
const schemaRes = await SchemaService.getTableSchema(activeTable, activeDatabase);
|
||||||
|
let pk = 'id';
|
||||||
const cols: GridColDef[] = schemaRes.data.map((col: any) => {
|
const cols: GridColDef[] = schemaRes.data.map((col: any) => {
|
||||||
|
if (col.Key === 'PRI') pk = col.Field;
|
||||||
const type = mapSqlTypeToMuiType(col.Type);
|
const type = mapSqlTypeToMuiType(col.Type);
|
||||||
return {
|
return {
|
||||||
field: col.Field,
|
field: col.Field,
|
||||||
@@ -30,6 +33,7 @@ export const useTableSchema = (activeTable: string | null, activeDatabase: strin
|
|||||||
type: type,
|
type: type,
|
||||||
width: 200,
|
width: 200,
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
|
editable: col.Key !== 'PRI', // Enable editing if not primary key
|
||||||
valueGetter: (value: any) => {
|
valueGetter: (value: any) => {
|
||||||
if ((type === 'date' || type === 'dateTime') && value && typeof value === 'string') {
|
if ((type === 'date' || type === 'dateTime') && value && typeof value === 'string') {
|
||||||
return new Date(value);
|
return new Date(value);
|
||||||
@@ -39,6 +43,7 @@ export const useTableSchema = (activeTable: string | null, activeDatabase: strin
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
setColumns(cols);
|
setColumns(cols);
|
||||||
|
setPrimaryKey(pk);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch table schema', error);
|
console.error('Failed to fetch table schema', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -49,5 +54,5 @@ export const useTableSchema = (activeTable: string | null, activeDatabase: strin
|
|||||||
fetchSchema();
|
fetchSchema();
|
||||||
}, [activeTable, activeDatabase]);
|
}, [activeTable, activeDatabase]);
|
||||||
|
|
||||||
return { columns, loading };
|
return { columns, loading, primaryKey };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const SchemaService = {
|
|||||||
dropTable: (table: string, database?: string) => api.post(`/schema/drop/${table}`, { database }),
|
dropTable: (table: string, database?: string) => api.post(`/schema/drop/${table}`, { database }),
|
||||||
optimizeTable: (table: string, database?: string) => api.post(`/schema/optimize/${table}`, { database }),
|
optimizeTable: (table: string, database?: string) => api.post(`/schema/optimize/${table}`, { database }),
|
||||||
bulkAction: (data: { tables: string[], action: string, database: string }) => api.post('/schema/bulk-action', data),
|
bulkAction: (data: { tables: string[], action: string, database: string }) => api.post('/schema/bulk-action', data),
|
||||||
|
batchUpdate: (table: string, changes: any[]) => api.post(`/schema/${table}/batch-update`, { changes }),
|
||||||
executeQuery: (query: string) => api.post('/schema/execute', { query }),
|
executeQuery: (query: string) => api.post('/schema/execute', { query }),
|
||||||
exportDatabase: (database?: string, table?: string) => api.post('/schema/export', { database, table }, { responseType: 'blob' }),
|
exportDatabase: (database?: string, table?: string) => api.post('/schema/export', { database, table }, { responseType: 'blob' }),
|
||||||
importDatabase: (formData: FormData) => api.post('/schema/import', formData, {
|
importDatabase: (formData: FormData) => api.post('/schema/import', formData, {
|
||||||
|
|||||||
Reference in New Issue
Block a user