feat: implement database export/import functionality and table management UI components

This commit is contained in:
Ümit Tunç
2026-04-24 13:33:02 +03:00
parent 6594f639c3
commit 5192d950f0
3 changed files with 198 additions and 178 deletions
+37 -37
View File
@@ -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)'
}}> }}>
+58 -41
View File
@@ -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>
); );
}; };
+103 -100
View File
@@ -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%' }}>