feat: implement database export/import functionality and table management UI components
This commit is contained in:
@@ -1,17 +1,17 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Paper,
|
Paper,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
Button,
|
||||||
Stack,
|
Stack,
|
||||||
Checkbox
|
Checkbox
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
TableChart,
|
TableChart,
|
||||||
CleaningServices,
|
CleaningServices,
|
||||||
Close,
|
Close,
|
||||||
Save
|
Save
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
||||||
import { SchemaService } from '../services/api';
|
import { SchemaService } from '../services/api';
|
||||||
@@ -47,7 +47,7 @@ const DatabaseTablesGrid: React.FC<DatabaseTablesGridProps> = ({ database, setNo
|
|||||||
|
|
||||||
const handleBulkAction = async (action: 'truncate' | 'drop' | 'optimize') => {
|
const handleBulkAction = async (action: 'truncate' | 'drop' | 'optimize') => {
|
||||||
if (selectionModel.length === 0) return;
|
if (selectionModel.length === 0) return;
|
||||||
|
|
||||||
const confirmMsg = `Are you sure you want to ${action} ${selectionModel.length} selected tables? This action is irreversible!`;
|
const confirmMsg = `Are you sure you want to ${action} ${selectionModel.length} selected tables? This action is irreversible!`;
|
||||||
if (!window.confirm(confirmMsg)) return;
|
if (!window.confirm(confirmMsg)) return;
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ const DatabaseTablesGrid: React.FC<DatabaseTablesGridProps> = ({ database, setNo
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleRow = (name: string) => {
|
const toggleRow = (name: string) => {
|
||||||
setSelectionModel(prev =>
|
setSelectionModel(prev =>
|
||||||
prev.includes(name) ? prev.filter(n => n !== name) : [...prev, name]
|
prev.includes(name) ? prev.filter(n => n !== name) : [...prev, name]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -100,16 +100,16 @@ const DatabaseTablesGrid: React.FC<DatabaseTablesGridProps> = ({ database, setNo
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
renderHeader: () => (
|
renderHeader: () => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
size="small"
|
size="small"
|
||||||
indeterminate={selectionModel.length > 0 && selectionModel.length < tableRows.length}
|
indeterminate={selectionModel.length > 0 && selectionModel.length < tableRows.length}
|
||||||
checked={tableRows.length > 0 && selectionModel.length === tableRows.length}
|
checked={tableRows.length > 0 && selectionModel.length === tableRows.length}
|
||||||
onChange={toggleAll}
|
onChange={toggleAll}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
renderCell: (params) => (
|
renderCell: (params) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
size="small"
|
size="small"
|
||||||
checked={selectionModel.includes(params.row.name)}
|
checked={selectionModel.includes(params.row.name)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -118,14 +118,14 @@ const DatabaseTablesGrid: React.FC<DatabaseTablesGridProps> = ({ database, setNo
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
headerName: 'Table Name',
|
headerName: 'Table Name',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
renderCell: (params) => (
|
renderCell: (params) => (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<TableChart fontSize="small" />}
|
startIcon={<TableChart fontSize="small" />}
|
||||||
onClick={() => setActiveTable(params.value)}
|
onClick={() => setActiveTable(params.value)}
|
||||||
sx={{ textTransform: 'none', fontWeight: 600, color: '#ffc107', '&:hover': { color: '#ffca28' } }}
|
sx={{ textTransform: 'none', fontWeight: 600, color: '#ffc107', '&:hover': { color: '#ffca28' } }}
|
||||||
@@ -136,15 +136,15 @@ const DatabaseTablesGrid: React.FC<DatabaseTablesGridProps> = ({ database, setNo
|
|||||||
},
|
},
|
||||||
{ field: 'engine', headerName: 'Engine', width: 120 },
|
{ field: 'engine', headerName: 'Engine', width: 120 },
|
||||||
{ field: 'rows', headerName: 'Rows', type: 'number', width: 120 },
|
{ field: 'rows', headerName: 'Rows', type: 'number', width: 120 },
|
||||||
{
|
{
|
||||||
field: 'data_length',
|
field: 'data_length',
|
||||||
headerName: 'Data Size',
|
headerName: 'Data Size',
|
||||||
width: 130,
|
width: 130,
|
||||||
valueFormatter: (value) => `${(Number(value) / 1024 / 1024).toFixed(2)} MB`
|
valueFormatter: (value) => `${(Number(value) / 1024 / 1024).toFixed(2)} MB`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'index_length',
|
field: 'index_length',
|
||||||
headerName: 'Index Size',
|
headerName: 'Index Size',
|
||||||
width: 130,
|
width: 130,
|
||||||
valueFormatter: (value) => `${(Number(value) / 1024 / 1024).toFixed(2)} MB`
|
valueFormatter: (value) => `${(Number(value) / 1024 / 1024).toFixed(2)} MB`
|
||||||
},
|
},
|
||||||
@@ -168,13 +168,13 @@ const DatabaseTablesGrid: React.FC<DatabaseTablesGridProps> = ({ database, setNo
|
|||||||
<Button size="small" onClick={() => setSelectionModel([])}>Clear Selection</Button>
|
<Button size="small" onClick={() => setSelectionModel([])}>Clear Selection</Button>
|
||||||
</Paper>
|
</Paper>
|
||||||
)}
|
)}
|
||||||
<Paper sx={{
|
<Paper sx={{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
borderRadius: 3,
|
borderRadius: 1,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
border: 1,
|
border: 1,
|
||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
|
boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Paper,
|
Paper,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
Button,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Alert
|
Alert,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableRow,
|
||||||
|
Avatar
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Terminal,
|
Terminal,
|
||||||
TableChart,
|
TableChart,
|
||||||
CleaningServices,
|
CleaningServices,
|
||||||
Save,
|
Save,
|
||||||
History,
|
History,
|
||||||
Info
|
Info
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { SchemaService } from '../services/api';
|
import { SchemaService } from '../services/api';
|
||||||
import ConfirmDialog from './ConfirmDialog';
|
import ConfirmDialog from './ConfirmDialog';
|
||||||
@@ -34,7 +40,7 @@ const TechnicalOverview: React.FC<TechnicalOverviewProps> = ({ database, table,
|
|||||||
const fetchMeta = useCallback(async () => {
|
const fetchMeta = useCallback(async () => {
|
||||||
setLoadingMeta(true);
|
setLoadingMeta(true);
|
||||||
try {
|
try {
|
||||||
const res = table
|
const res = table
|
||||||
? await SchemaService.getTableMetadata(database, table)
|
? await SchemaService.getTableMetadata(database, table)
|
||||||
: await SchemaService.getDatabaseMetadata(database);
|
: await SchemaService.getDatabaseMetadata(database);
|
||||||
setMeta(res.data);
|
setMeta(res.data);
|
||||||
@@ -98,9 +104,9 @@ const TechnicalOverview: React.FC<TechnicalOverviewProps> = ({ database, table,
|
|||||||
Technical Specifications: {table ? `${database}.${table}` : database}
|
Technical Specifications: {table ? `${database}.${table}` : database}
|
||||||
</Typography>
|
</Typography>
|
||||||
{table && (
|
{table && (
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="error"
|
color="error"
|
||||||
startIcon={truncating ? <CircularProgress size={16} color="inherit" /> : <CleaningServices />}
|
startIcon={truncating ? <CircularProgress size={16} color="inherit" /> : <CleaningServices />}
|
||||||
onClick={() => setShowConfirm(true)}
|
onClick={() => setShowConfirm(true)}
|
||||||
disabled={truncating}
|
disabled={truncating}
|
||||||
@@ -111,7 +117,7 @@ const TechnicalOverview: React.FC<TechnicalOverviewProps> = ({ database, table,
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={showConfirm}
|
open={showConfirm}
|
||||||
onClose={() => setShowConfirm(false)}
|
onClose={() => setShowConfirm(false)}
|
||||||
onConfirm={handleTruncate}
|
onConfirm={handleTruncate}
|
||||||
@@ -120,28 +126,39 @@ const TechnicalOverview: React.FC<TechnicalOverviewProps> = ({ database, table,
|
|||||||
confirmLabel="Truncate Now"
|
confirmLabel="Truncate Now"
|
||||||
loading={truncating}
|
loading={truncating}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 3 }}>
|
<TableContainer component={Paper} sx={{
|
||||||
{stats.map((stat, i) => (
|
borderRadius: 1,
|
||||||
<Paper key={i} sx={{
|
border: 1,
|
||||||
p: 3,
|
borderColor: 'divider',
|
||||||
borderRadius: 4,
|
bgcolor: 'background.paper',
|
||||||
border: 1,
|
overflow: 'hidden',
|
||||||
borderColor: 'divider',
|
boxShadow: 'none'
|
||||||
display: 'flex',
|
}}>
|
||||||
flexDirection: 'column',
|
<Table>
|
||||||
gap: 1,
|
<TableBody>
|
||||||
bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.01)',
|
{stats.map((stat, i) => (
|
||||||
transition: 'all 0.2s',
|
<TableRow key={i} sx={{ '&:last-child td, &:last-child th': { border: 0 }, '&:hover': { bgcolor: 'rgba(255,255,255,0.02)' } }}>
|
||||||
'&:hover': { borderColor: 'primary.main', transform: 'translateY(-2px)' }
|
<TableCell sx={{ width: 64 }}>
|
||||||
}}>
|
<Avatar sx={{
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
bgcolor: 'rgba(0,123,255,0.1)',
|
||||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary', textTransform: 'uppercase', letterSpacing: 1 }}>{stat.label}</Typography>
|
color: 'primary.main',
|
||||||
<Box sx={{ color: 'primary.main', opacity: 0.5 }}>{stat.icon}</Box>
|
width: 40,
|
||||||
</Box>
|
height: 40
|
||||||
<Typography variant="h5" sx={{ fontWeight: 800 }}>{stat.value || 'N/A'}</Typography>
|
}}>
|
||||||
</Paper>
|
{stat.icon}
|
||||||
))}
|
</Avatar>
|
||||||
</Box>
|
</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 700, color: 'text.secondary', textTransform: 'uppercase', letterSpacing: 1, fontSize: '0.75rem' }}>
|
||||||
|
{stat.label}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 800, fontSize: '1.1rem' }}>
|
||||||
|
{stat.value || 'N/A'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Alert,
|
Alert,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
IconButton
|
IconButton,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableRow,
|
||||||
|
Avatar
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
CloudDownload,
|
CloudDownload,
|
||||||
@@ -111,106 +117,103 @@ const TransferContent: React.FC<TransferContentProps> = ({ mode = 'both' }) => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={3}>
|
<TableContainer component={Paper} sx={{
|
||||||
{/* Export Card */}
|
borderRadius: 4,
|
||||||
{(mode === 'export' || mode === 'both') && (
|
border: 1,
|
||||||
<Paper sx={{
|
borderColor: 'divider',
|
||||||
flex: 1,
|
bgcolor: 'background.paper',
|
||||||
p: 4,
|
overflow: 'hidden',
|
||||||
borderRadius: 4,
|
boxShadow: 'none'
|
||||||
border: 1,
|
}}>
|
||||||
borderColor: 'divider',
|
<Table>
|
||||||
display: 'flex',
|
<TableBody>
|
||||||
flexDirection: 'column',
|
{/* Export Row */}
|
||||||
alignItems: 'center',
|
{(mode === 'export' || mode === 'both') && (
|
||||||
textAlign: 'center',
|
<TableRow sx={{ '&:hover': { bgcolor: 'rgba(255,255,255,0.02)' } }}>
|
||||||
gap: 2,
|
<TableCell sx={{ width: 80, verticalAlign: 'top', pt: 3 }}>
|
||||||
transition: 'all 0.3s ease',
|
<Avatar sx={{ bgcolor: 'primary.main', color: 'white', width: 48, height: 48 }}>
|
||||||
'&:hover': { transform: 'translateY(-4px)', boxShadow: '0 12px 24px rgba(0,0,0,0.1)' }
|
<CloudDownload />
|
||||||
}}>
|
</Avatar>
|
||||||
<Box sx={{ p: 2, borderRadius: '50%', bgcolor: 'primary.main', color: 'white', mb: 1 }}>
|
</TableCell>
|
||||||
<CloudDownload sx={{ fontSize: 40 }} />
|
<TableCell sx={{ py: 3 }}>
|
||||||
</Box>
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 0.5 }}>Export {activeTable ? 'Table' : 'Database'}</Typography>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 700 }}>Export {activeTable ? 'Table' : 'Database'}</Typography>
|
<Typography variant="body2" color="text.secondary">
|
||||||
<Typography variant="body2" color="text.secondary">
|
Create a full backup of the current {activeTable ? 'table' : 'database'}: <br/>
|
||||||
Create a full backup of the current {activeTable ? 'table' : 'database'}: <strong>{activeTable ? `${activeDatabase}.${activeTable}` : (activeDatabase || 'All Databases')}</strong>
|
<Box component="span" sx={{ color: 'primary.main', fontWeight: 600 }}>
|
||||||
</Typography>
|
{activeTable ? `${activeDatabase}.${activeTable}` : (activeDatabase || 'All Databases')}
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
</Box>
|
||||||
<Button
|
</Typography>
|
||||||
variant="contained"
|
</TableCell>
|
||||||
fullWidth
|
<TableCell align="right" sx={{ py: 3 }}>
|
||||||
size="large"
|
<Button
|
||||||
startIcon={<CloudDownload />}
|
variant="contained"
|
||||||
onClick={handleExport}
|
startIcon={<CloudDownload />}
|
||||||
disabled={loading}
|
onClick={handleExport}
|
||||||
sx={{ borderRadius: 2, py: 1.5, fontWeight: 700 }}
|
disabled={loading}
|
||||||
>
|
sx={{ borderRadius: 2, px: 3, py: 1, fontWeight: 700, minWidth: 160 }}
|
||||||
Start Export
|
>
|
||||||
</Button>
|
Start Export
|
||||||
</Paper>
|
</Button>
|
||||||
)}
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Import Card */}
|
{/* Import Row */}
|
||||||
{(mode === 'import' || mode === 'both') && (
|
{(mode === 'import' || mode === 'both') && (
|
||||||
<Paper sx={{
|
<TableRow sx={{ '&:hover': { bgcolor: 'rgba(255,255,255,0.02)' } }}>
|
||||||
flex: 1,
|
<TableCell sx={{ width: 80, verticalAlign: 'top', pt: 3 }}>
|
||||||
p: 4,
|
<Avatar sx={{ bgcolor: 'secondary.main', color: 'white', width: 48, height: 48 }}>
|
||||||
borderRadius: 4,
|
<CloudUpload />
|
||||||
border: 1,
|
</Avatar>
|
||||||
borderColor: 'divider',
|
</TableCell>
|
||||||
display: 'flex',
|
<TableCell sx={{ py: 3 }}>
|
||||||
flexDirection: 'column',
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 0.5 }}>Import {activeTable ? 'Table' : 'Database'}</Typography>
|
||||||
alignItems: 'center',
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
textAlign: 'center',
|
Upload a .sql file to restore or migrate your {activeTable ? 'table' : 'database'}.
|
||||||
gap: 2,
|
</Typography>
|
||||||
transition: 'all 0.3s ease',
|
|
||||||
'&:hover': { transform: 'translateY(-4px)', boxShadow: '0 12px 24px rgba(0,0,0,0.1)' }
|
<Box sx={{
|
||||||
}}>
|
maxWidth: 400,
|
||||||
<Box sx={{ p: 2, borderRadius: '50%', bgcolor: 'secondary.main', color: 'white', mb: 1 }}>
|
p: 1.5,
|
||||||
<CloudUpload sx={{ fontSize: 40 }} />
|
border: '2px dashed',
|
||||||
</Box>
|
borderColor: file ? 'secondary.main' : 'divider',
|
||||||
<Typography variant="h6" sx={{ fontWeight: 700 }}>Import {activeTable ? 'Table' : 'Database'}</Typography>
|
borderRadius: 2,
|
||||||
<Typography variant="body2" color="text.secondary">
|
bgcolor: 'rgba(0,0,0,0.02)',
|
||||||
Upload a .sql file to restore or migrate your {activeTable ? 'table' : 'database'}.
|
cursor: 'pointer',
|
||||||
</Typography>
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
<Box sx={{
|
alignItems: 'center',
|
||||||
width: '100%',
|
gap: 1
|
||||||
p: 2,
|
}}>
|
||||||
border: '2px dashed',
|
<input
|
||||||
borderColor: file ? 'secondary.main' : 'divider',
|
type="file"
|
||||||
borderRadius: 2,
|
accept=".sql"
|
||||||
bgcolor: 'rgba(0,0,0,0.02)',
|
onChange={handleFileChange}
|
||||||
cursor: 'pointer',
|
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer', zIndex: 1 }}
|
||||||
position: 'relative'
|
/>
|
||||||
}}>
|
<CloudUpload fontSize="small" color={file ? "secondary" : "disabled"} />
|
||||||
<input
|
<Typography variant="caption" sx={{ fontWeight: 600 }}>
|
||||||
type="file"
|
{file ? file.name : "Select or drag .sql file here"}
|
||||||
accept=".sql"
|
</Typography>
|
||||||
onChange={handleFileChange}
|
</Box>
|
||||||
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer' }}
|
</TableCell>
|
||||||
/>
|
<TableCell align="right" sx={{ py: 3 }}>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Button
|
||||||
{file ? file.name : "Click or drag .sql file here"}
|
variant="outlined"
|
||||||
</Typography>
|
color="secondary"
|
||||||
</Box>
|
startIcon={<CloudUpload />}
|
||||||
|
onClick={handleImport}
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
disabled={loading || !file}
|
||||||
<Button
|
sx={{ borderRadius: 2, px: 3, py: 1, fontWeight: 700, minWidth: 160 }}
|
||||||
variant="outlined"
|
>
|
||||||
fullWidth
|
Start Import
|
||||||
size="large"
|
</Button>
|
||||||
color="secondary"
|
</TableCell>
|
||||||
startIcon={<CloudUpload />}
|
</TableRow>
|
||||||
onClick={handleImport}
|
)}
|
||||||
disabled={loading || !file}
|
</TableBody>
|
||||||
sx={{ borderRadius: 2, py: 1.5, fontWeight: 700 }}
|
</Table>
|
||||||
>
|
</TableContainer>
|
||||||
Start Import
|
|
||||||
</Button>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<Box sx={{ width: '100%' }}>
|
<Box sx={{ width: '100%' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user