feat: implement database schema management, bulk table operations, and data viewer components
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
Button,
|
||||
Stack,
|
||||
Checkbox
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TableChart,
|
||||
CleaningServices,
|
||||
Close,
|
||||
Save
|
||||
} from '@mui/icons-material';
|
||||
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
||||
import { SchemaService } from '../services/api';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import type { MainNotification } from '../types/database';
|
||||
|
||||
interface DatabaseTablesGridProps {
|
||||
database: string;
|
||||
setNotification: (n: MainNotification) => void;
|
||||
}
|
||||
|
||||
const DatabaseTablesGrid: React.FC<DatabaseTablesGridProps> = ({ database, setNotification }) => {
|
||||
const [tableRows, setTableRows] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectionModel, setSelectionModel] = useState<string[]>([]);
|
||||
const { setActiveTable } = useAppStore();
|
||||
|
||||
const fetchTablesMeta = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await SchemaService.getTablesMetadata(database);
|
||||
setTableRows(res.data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [database]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTablesMeta();
|
||||
}, [fetchTablesMeta]);
|
||||
|
||||
const handleBulkAction = async (action: 'truncate' | 'drop' | 'optimize') => {
|
||||
if (selectionModel.length === 0) return;
|
||||
|
||||
const confirmMsg = `Are you sure you want to ${action} ${selectionModel.length} selected tables? This action is irreversible!`;
|
||||
if (!window.confirm(confirmMsg)) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await SchemaService.bulkAction({
|
||||
tables: selectionModel,
|
||||
action,
|
||||
database
|
||||
});
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'Bulk Action Success',
|
||||
message: `Successfully performed ${action} on ${selectionModel.length} tables.`,
|
||||
severity: 'success'
|
||||
});
|
||||
fetchTablesMeta();
|
||||
setSelectionModel([]);
|
||||
} catch (error: any) {
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'Bulk Action Error',
|
||||
message: error.response?.data?.error || error.message,
|
||||
severity: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRow = (name: string) => {
|
||||
setSelectionModel(prev =>
|
||||
prev.includes(name) ? prev.filter(n => n !== name) : [...prev, name]
|
||||
);
|
||||
};
|
||||
|
||||
const toggleAll = () => {
|
||||
if (selectionModel.length === tableRows.length) {
|
||||
setSelectionModel([]);
|
||||
} else {
|
||||
setSelectionModel(tableRows.map(r => r.name));
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: '__check__',
|
||||
headerName: '',
|
||||
width: 50,
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
renderHeader: () => (
|
||||
<Checkbox
|
||||
size="small"
|
||||
indeterminate={selectionModel.length > 0 && selectionModel.length < tableRows.length}
|
||||
checked={tableRows.length > 0 && selectionModel.length === tableRows.length}
|
||||
onChange={toggleAll}
|
||||
/>
|
||||
),
|
||||
renderCell: (params) => (
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={selectionModel.includes(params.row.name)}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleRow(params.row.name);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
headerName: 'Table Name',
|
||||
flex: 1,
|
||||
minWidth: 200,
|
||||
renderCell: (params) => (
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<TableChart fontSize="small" />}
|
||||
onClick={() => setActiveTable(params.value)}
|
||||
sx={{ textTransform: 'none', fontWeight: 600, color: '#ffc107', '&:hover': { color: '#ffca28' } }}
|
||||
>
|
||||
{params.value}
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
{ field: 'engine', headerName: 'Engine', width: 120 },
|
||||
{ field: 'rows', headerName: 'Rows', type: 'number', width: 120 },
|
||||
{
|
||||
field: 'data_length',
|
||||
headerName: 'Data Size',
|
||||
width: 130,
|
||||
valueFormatter: (value) => `${(Number(value) / 1024 / 1024).toFixed(2)} MB`
|
||||
},
|
||||
{
|
||||
field: 'index_length',
|
||||
headerName: 'Index Size',
|
||||
width: 130,
|
||||
valueFormatter: (value) => `${(Number(value) / 1024 / 1024).toFixed(2)} MB`
|
||||
},
|
||||
{ field: 'collation', headerName: 'Collation', width: 180 },
|
||||
{ field: 'comment', headerName: 'Comment', flex: 1, minWidth: 150 },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{selectionModel.length > 0 && (
|
||||
<Paper sx={{ p: 1.5, borderRadius: 2, display: 'flex', alignItems: 'center', gap: 2, bgcolor: 'rgba(255, 193, 7, 0.05)', border: '1px dashed #ffc107' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#ffc107' }}>
|
||||
{selectionModel.length} tables selected:
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Button size="small" color="warning" variant="outlined" startIcon={<CleaningServices />} onClick={() => handleBulkAction('truncate')}>Truncate</Button>
|
||||
<Button size="small" color="error" variant="outlined" startIcon={<Close />} onClick={() => handleBulkAction('drop')}>Drop</Button>
|
||||
<Button size="small" color="info" variant="outlined" startIcon={<Save />} onClick={() => handleBulkAction('optimize')}>Optimize</Button>
|
||||
</Stack>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button size="small" onClick={() => setSelectionModel([])}>Clear Selection</Button>
|
||||
</Paper>
|
||||
)}
|
||||
<Paper sx={{
|
||||
flexGrow: 1,
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
|
||||
}}>
|
||||
<DataGrid
|
||||
rows={tableRows}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
getRowId={(row: any) => row.name}
|
||||
density="comfortable"
|
||||
sx={{ border: 'none' }}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatabaseTablesGrid;
|
||||
@@ -1,352 +1,40 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
CircularProgress,
|
||||
Button,
|
||||
Divider,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Snackbar,
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Tabs,
|
||||
Tab,
|
||||
Stack,
|
||||
Checkbox
|
||||
Tab
|
||||
} from '@mui/material';
|
||||
import {
|
||||
PlayArrow,
|
||||
History,
|
||||
Save,
|
||||
CleaningServices,
|
||||
Close,
|
||||
TableChart,
|
||||
Terminal,
|
||||
CloudDownload,
|
||||
CloudUpload,
|
||||
Info
|
||||
} from '@mui/icons-material';
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import type { GridColDef, GridPaginationModel } from '@mui/x-data-grid';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { DataGrid, type GridPaginationModel } from '@mui/x-data-grid';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { SchemaService } from '../services/api';
|
||||
import TransferContent from './TransferContent';
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
|
||||
interface MainNotification {
|
||||
open: boolean;
|
||||
message: string;
|
||||
title: string;
|
||||
severity: 'success' | 'error' | 'info' | 'warning';
|
||||
}
|
||||
|
||||
const DatabaseTablesGrid = ({ database, setNotification }: { database: string, setNotification: (n: MainNotification) => void }) => {
|
||||
const [tableRows, setTableRows] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectionModel, setSelectionModel] = useState<string[]>([]);
|
||||
const { setActiveTable } = useAppStore();
|
||||
|
||||
const fetchTablesMeta = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await SchemaService.getTablesMetadata(database);
|
||||
setTableRows(res.data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [database]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTablesMeta();
|
||||
}, [fetchTablesMeta]);
|
||||
|
||||
const handleBulkAction = async (action: 'truncate' | 'drop' | 'optimize') => {
|
||||
if (selectionModel.length === 0) return;
|
||||
|
||||
const confirmMsg = `Are you sure you want to ${action} ${selectionModel.length} selected tables? This action is irreversible!`;
|
||||
if (!window.confirm(confirmMsg)) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await SchemaService.bulkAction({
|
||||
tables: selectionModel,
|
||||
action,
|
||||
database
|
||||
});
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'Bulk Action Success',
|
||||
message: `Successfully performed ${action} on ${selectionModel.length} tables.`,
|
||||
severity: 'success'
|
||||
});
|
||||
fetchTablesMeta();
|
||||
setSelectionModel([]);
|
||||
} catch (error: any) {
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'Bulk Action Error',
|
||||
message: error.response?.data?.error || error.message,
|
||||
severity: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRow = (name: string) => {
|
||||
setSelectionModel(prev =>
|
||||
prev.includes(name) ? prev.filter(n => n !== name) : [...prev, name]
|
||||
);
|
||||
};
|
||||
|
||||
const toggleAll = () => {
|
||||
if (selectionModel.length === tableRows.length) {
|
||||
setSelectionModel([]);
|
||||
} else {
|
||||
setSelectionModel(tableRows.map(r => r.name));
|
||||
}
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: '__check__',
|
||||
headerName: '',
|
||||
width: 50,
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
renderHeader: () => (
|
||||
<Checkbox
|
||||
size="small"
|
||||
indeterminate={selectionModel.length > 0 && selectionModel.length < tableRows.length}
|
||||
checked={tableRows.length > 0 && selectionModel.length === tableRows.length}
|
||||
onChange={toggleAll}
|
||||
/>
|
||||
),
|
||||
renderCell: (params) => (
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={selectionModel.includes(params.row.name)}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleRow(params.row.name);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
headerName: 'Table Name',
|
||||
flex: 1,
|
||||
minWidth: 200,
|
||||
renderCell: (params) => (
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<TableChart fontSize="small" />}
|
||||
onClick={() => setActiveTable(params.value)}
|
||||
sx={{ textTransform: 'none', fontWeight: 600, color: '#ffc107', '&:hover': { color: '#ffca28' } }}
|
||||
>
|
||||
{params.value}
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
{ field: 'engine', headerName: 'Engine', width: 120 },
|
||||
{ field: 'rows', headerName: 'Rows', type: 'number', width: 120 },
|
||||
{
|
||||
field: 'data_length',
|
||||
headerName: 'Data Size',
|
||||
width: 130,
|
||||
valueFormatter: (value) => `${(Number(value) / 1024 / 1024).toFixed(2)} MB`
|
||||
},
|
||||
{
|
||||
field: 'index_length',
|
||||
headerName: 'Index Size',
|
||||
width: 130,
|
||||
valueFormatter: (value) => `${(Number(value) / 1024 / 1024).toFixed(2)} MB`
|
||||
},
|
||||
{ field: 'collation', headerName: 'Collation', width: 180 },
|
||||
{ field: 'comment', headerName: 'Comment', flex: 1, minWidth: 150 },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{selectionModel.length > 0 && (
|
||||
<Paper sx={{ p: 1.5, borderRadius: 2, display: 'flex', alignItems: 'center', gap: 2, bgcolor: 'rgba(255, 193, 7, 0.05)', border: '1px dashed #ffc107' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#ffc107' }}>
|
||||
{selectionModel.length} tables selected:
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Button size="small" color="warning" variant="outlined" startIcon={<CleaningServices />} onClick={() => handleBulkAction('truncate')}>Truncate</Button>
|
||||
<Button size="small" color="error" variant="outlined" startIcon={<Close />} onClick={() => handleBulkAction('drop')}>Drop</Button>
|
||||
<Button size="small" color="info" variant="outlined" startIcon={<Save />} onClick={() => handleBulkAction('optimize')}>Optimize</Button>
|
||||
</Stack>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button size="small" onClick={() => setSelectionModel([])}>Clear Selection</Button>
|
||||
</Paper>
|
||||
)}
|
||||
<Paper sx={{
|
||||
flexGrow: 1,
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
|
||||
}}>
|
||||
<DataGrid
|
||||
rows={tableRows}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
getRowId={(row: any) => row.name}
|
||||
density="comfortable"
|
||||
sx={{ border: 'none' }}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const TechnicalOverview = ({ database, table, setNotification }: { database: string, table: string | null, setNotification: (n: MainNotification) => void }) => {
|
||||
const [meta, setMeta] = useState<any>(null);
|
||||
const [loadingMeta, setLoadingMeta] = useState(true);
|
||||
const [truncating, setTruncating] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
|
||||
const fetchMeta = useCallback(async () => {
|
||||
setLoadingMeta(true);
|
||||
try {
|
||||
const res = table
|
||||
? await SchemaService.getTableMetadata(database, table)
|
||||
: await SchemaService.getDatabaseMetadata(database);
|
||||
setMeta(res.data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoadingMeta(false);
|
||||
}
|
||||
}, [database, table]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMeta();
|
||||
}, [fetchMeta]);
|
||||
|
||||
const handleTruncate = async () => {
|
||||
setTruncating(true);
|
||||
setShowConfirm(false);
|
||||
try {
|
||||
await SchemaService.truncateTable(table!);
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'Success',
|
||||
message: `Table "${table}" has been truncated.`,
|
||||
severity: 'success'
|
||||
});
|
||||
fetchMeta();
|
||||
} catch (error: any) {
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'Truncate Error',
|
||||
message: error.response?.data?.error || error.message,
|
||||
severity: 'error'
|
||||
});
|
||||
} finally {
|
||||
setTruncating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loadingMeta) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 8 }}><CircularProgress /></Box>;
|
||||
if (!meta) return <Alert severity="error">Could not load metadata</Alert>;
|
||||
|
||||
const stats = table ? [
|
||||
{ label: 'Engine', value: meta.engine, icon: <Terminal /> },
|
||||
{ label: 'Rows', value: meta.rows, icon: <TableChart /> },
|
||||
{ label: 'Collation', value: meta.collation, icon: <CleaningServices /> },
|
||||
{ label: 'Data Size', value: `${(meta.data_length / 1024 / 1024).toFixed(2)} MB`, icon: <Save /> },
|
||||
{ label: 'Index Size', value: `${(meta.index_length / 1024 / 1024).toFixed(2)} MB`, icon: <History /> },
|
||||
{ label: 'Created', value: meta.create_time, icon: <Info /> },
|
||||
] : [
|
||||
{ label: 'Character Set', value: meta.charset, icon: <History /> },
|
||||
{ label: 'Collation', value: meta.collation, icon: <CleaningServices /> },
|
||||
{ label: 'Tables', value: meta.table_count, icon: <TableChart /> },
|
||||
{ label: 'Views', value: meta.view_count, icon: <History /> },
|
||||
{ label: 'Total Size', value: `${(meta.size_bytes / 1024 / 1024).toFixed(2)} MB`, icon: <Save /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 800, letterSpacing: -0.5 }}>
|
||||
Technical Specifications: {table ? `${database}.${table}` : database}
|
||||
</Typography>
|
||||
{table && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={truncating ? <CircularProgress size={16} color="inherit" /> : <CleaningServices />}
|
||||
onClick={() => setShowConfirm(true)}
|
||||
disabled={truncating}
|
||||
sx={{ borderRadius: 2, textTransform: 'none', fontWeight: 700 }}
|
||||
>
|
||||
Truncate Table
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ConfirmDialog
|
||||
open={showConfirm}
|
||||
onClose={() => setShowConfirm(false)}
|
||||
onConfirm={handleTruncate}
|
||||
title="Truncate Table"
|
||||
message={`Are you sure you want to truncate table "${table}"? This action will permanently delete all ${meta?.rows || ''} records. This cannot be undone.`}
|
||||
confirmLabel="Truncate Now"
|
||||
loading={truncating}
|
||||
/>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 3 }}>
|
||||
{stats.map((stat, i) => (
|
||||
<Paper key={i} sx={{
|
||||
p: 3,
|
||||
borderRadius: 4,
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.01)',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': { borderColor: 'primary.main', transform: 'translateY(-2px)' }
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary', textTransform: 'uppercase', letterSpacing: 1 }}>{stat.label}</Typography>
|
||||
<Box sx={{ color: 'primary.main', opacity: 0.5 }}>{stat.icon}</Box>
|
||||
</Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 800 }}>{stat.value || 'N/A'}</Typography>
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
import DatabaseTablesGrid from './DatabaseTablesGrid';
|
||||
import TechnicalOverview from './TechnicalOverview';
|
||||
import SqlConsole from './SqlConsole';
|
||||
import { useTableSchema } from '../hooks/useTableSchema';
|
||||
import { useTableData } from '../hooks/useTableData';
|
||||
import type { MainNotification } from '../types/database';
|
||||
|
||||
const MainContent: React.FC = () => {
|
||||
const { activeTable, activeDatabase, darkMode, dbTab, setDbTab } = useAppStore();
|
||||
const [columns, setColumns] = useState<GridColDef[]>([]);
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [rowCount, setRowCount] = useState(0);
|
||||
const [loadingSchema, setLoadingSchema] = useState(false);
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
const [sqlQuery, setSqlQuery] = useState('');
|
||||
const [isCustomQuery, setIsCustomQuery] = useState(false);
|
||||
|
||||
const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
|
||||
page: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
|
||||
// Custom Notification State
|
||||
const [notification, setNotification] = useState<MainNotification>({
|
||||
open: false,
|
||||
message: '',
|
||||
@@ -354,137 +42,15 @@ const MainContent: React.FC = () => {
|
||||
severity: 'error'
|
||||
});
|
||||
|
||||
const { columns, loading: loadingSchema } = useTableSchema(activeTable, activeDatabase);
|
||||
const { rows, rowCount, loading: loadingData } = useTableData(activeTable, activeDatabase, paginationModel);
|
||||
|
||||
useEffect(() => {
|
||||
setPaginationModel({ page: 0, pageSize: 100 });
|
||||
}, [activeTable]);
|
||||
|
||||
const handleCloseNotification = () => setNotification({ ...notification, open: false });
|
||||
|
||||
// Helper to map SQL types to MUI X Data Grid types
|
||||
const mapSqlTypeToMuiType = (sqlType: string): 'string' | 'number' | 'date' | 'dateTime' | 'boolean' => {
|
||||
sqlType = sqlType.toLowerCase();
|
||||
if (sqlType.includes('int') || sqlType.includes('decimal') || sqlType.includes('float') || sqlType.includes('double')) return 'number';
|
||||
if (sqlType.includes('datetime') || sqlType.includes('timestamp')) return 'dateTime';
|
||||
if (sqlType.includes('date')) return 'date';
|
||||
if (sqlType.includes('bool') || sqlType.includes('tinyint(1)')) return 'boolean';
|
||||
return 'string';
|
||||
};
|
||||
|
||||
// Set initial SQL query when table changes
|
||||
useEffect(() => {
|
||||
if (activeTable && activeDatabase) {
|
||||
setSqlQuery(`SELECT * FROM ${activeDatabase}.${activeTable} LIMIT 1000;`);
|
||||
setIsCustomQuery(false);
|
||||
}
|
||||
}, [activeTable, activeDatabase]);
|
||||
|
||||
// Fetch Schema
|
||||
useEffect(() => {
|
||||
const fetchSchema = async () => {
|
||||
if (!activeTable || !activeDatabase || isCustomQuery) return;
|
||||
|
||||
setLoadingSchema(true);
|
||||
try {
|
||||
const schemaRes = await SchemaService.getTableSchema(activeTable, activeDatabase);
|
||||
const cols: GridColDef[] = schemaRes.data.map((col: any) => {
|
||||
const type = mapSqlTypeToMuiType(col.Type);
|
||||
return {
|
||||
field: col.Field,
|
||||
headerName: col.Field,
|
||||
type: type,
|
||||
width: 200,
|
||||
minWidth: 100,
|
||||
valueGetter: (value: any) => {
|
||||
if ((type === 'date' || type === 'dateTime') && value && typeof value === 'string') {
|
||||
return new Date(value);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
});
|
||||
setColumns(cols);
|
||||
setPaginationModel({ page: 0, pageSize: 100 });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch table schema', error);
|
||||
} finally {
|
||||
setLoadingSchema(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSchema();
|
||||
}, [activeTable, activeDatabase, isCustomQuery]);
|
||||
|
||||
// Fetch Data
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!activeTable || !activeDatabase || isCustomQuery) return;
|
||||
|
||||
setLoadingData(true);
|
||||
try {
|
||||
const params = {
|
||||
skip: paginationModel.page * paginationModel.pageSize,
|
||||
take: paginationModel.pageSize,
|
||||
requireTotalCount: true,
|
||||
database: activeDatabase,
|
||||
};
|
||||
|
||||
const response = await SchemaService.getTableData(activeTable, params);
|
||||
|
||||
const dataWithIds = response.data.data.map((row: any, index: number) => ({
|
||||
id: row.id || row.ID || `row-${index}-${paginationModel.page}`,
|
||||
...row,
|
||||
}));
|
||||
|
||||
setRows(dataWithIds);
|
||||
setRowCount(response.data.totalCount || 0);
|
||||
} catch (error) {
|
||||
console.error('Data loading error', error);
|
||||
} finally {
|
||||
setLoadingData(false);
|
||||
}
|
||||
}, [activeTable, activeDatabase, paginationModel, isCustomQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const handleExecute = async () => {
|
||||
if (!sqlQuery) return;
|
||||
setLoadingData(true);
|
||||
setIsCustomQuery(true);
|
||||
try {
|
||||
const response = await SchemaService.executeQuery(sqlQuery);
|
||||
const rawData = response.data.data;
|
||||
|
||||
if (rawData && rawData.length > 0) {
|
||||
const fields = Object.keys(rawData[0]);
|
||||
const newCols: GridColDef[] = fields.map(field => ({
|
||||
field,
|
||||
headerName: field,
|
||||
width: 150,
|
||||
flex: fields.length < 6 ? 1 : 0
|
||||
}));
|
||||
setColumns(newCols);
|
||||
|
||||
const dataWithIds = rawData.map((row: any, index: number) => ({
|
||||
id: row.id || row.ID || `row-${index}`,
|
||||
...row,
|
||||
}));
|
||||
setRows(dataWithIds);
|
||||
setRowCount(response.data.count);
|
||||
} else {
|
||||
setRows([]);
|
||||
setRowCount(0);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Execution error', error);
|
||||
const msg = error.response?.data?.error || 'An unexpected error occurred during SQL execution.';
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'SQL Execution Error',
|
||||
message: msg,
|
||||
severity: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoadingData(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeDatabase) {
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, p: 3, display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: 'background.default' }}>
|
||||
@@ -591,49 +157,10 @@ const MainContent: React.FC = () => {
|
||||
|
||||
{/* SQL View */}
|
||||
{dbTab === 'sql' && (
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', gap: 2, overflow: 'hidden' }}>
|
||||
<Paper elevation={0} sx={{
|
||||
flexShrink: 0,
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: darkMode ? '#111827' : '#ffffff',
|
||||
}}>
|
||||
<Box sx={{ px: 3, py: 1.5, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'rgba(0,0,0,0.02)' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>SQL CONSOLE</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleExecute}
|
||||
disabled={loadingData}
|
||||
startIcon={loadingData ? <CircularProgress size={16} color="inherit" /> : <PlayArrow />}
|
||||
sx={{ borderRadius: 2, textTransform: 'none', fontWeight: 700 }}
|
||||
>
|
||||
Execute Query
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Editor
|
||||
height="200px"
|
||||
defaultLanguage="sql"
|
||||
theme={darkMode ? 'vs-dark' : 'light'}
|
||||
value={sqlQuery}
|
||||
onChange={(value) => setSqlQuery(value || '')}
|
||||
options={{ minimap: { enabled: false }, fontSize: 14, automaticLayout: true }}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ flexGrow: 1, borderRadius: 3, overflow: 'hidden', border: 1, borderColor: 'divider' }}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
loading={loadingData}
|
||||
sx={{ border: 'none' }}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
<SqlConsole
|
||||
initialQuery={activeTable && activeDatabase ? `SELECT * FROM ${activeDatabase}.${activeTable} LIMIT 1000;` : ''}
|
||||
setNotification={setNotification}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Import/Export Views */}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import { PlayArrow } from '@mui/icons-material';
|
||||
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { SchemaService } from '../services/api';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import type { MainNotification } from '../types/database';
|
||||
|
||||
interface SqlConsoleProps {
|
||||
initialQuery: string;
|
||||
setNotification: (n: MainNotification) => void;
|
||||
}
|
||||
|
||||
const SqlConsole: React.FC<SqlConsoleProps> = ({ initialQuery, setNotification }) => {
|
||||
const { darkMode } = useAppStore();
|
||||
const [sqlQuery, setSqlQuery] = useState(initialQuery);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [columns, setColumns] = useState<GridColDef[]>([]);
|
||||
const [rowCount, setRowCount] = useState(0);
|
||||
|
||||
const handleExecute = async () => {
|
||||
if (!sqlQuery) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await SchemaService.executeQuery(sqlQuery);
|
||||
const rawData = response.data.data;
|
||||
|
||||
if (rawData && rawData.length > 0) {
|
||||
const fields = Object.keys(rawData[0]);
|
||||
const newCols: GridColDef[] = fields.map(field => ({
|
||||
field,
|
||||
headerName: field,
|
||||
width: 150,
|
||||
flex: fields.length < 6 ? 1 : 0
|
||||
}));
|
||||
setColumns(newCols);
|
||||
|
||||
const dataWithIds = rawData.map((row: any, index: number) => ({
|
||||
id: row.id || row.ID || `row-${index}`,
|
||||
...row,
|
||||
}));
|
||||
setRows(dataWithIds);
|
||||
setRowCount(response.data.count);
|
||||
} else {
|
||||
setRows([]);
|
||||
setRowCount(0);
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'Query Executed',
|
||||
message: 'Query executed successfully with no results.',
|
||||
severity: 'info'
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Execution error', error);
|
||||
const msg = error.response?.data?.error || 'An unexpected error occurred during SQL execution.';
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'SQL Execution Error',
|
||||
message: msg,
|
||||
severity: 'error'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', gap: 2, overflow: 'hidden' }}>
|
||||
<Paper elevation={0} sx={{
|
||||
flexShrink: 0,
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: darkMode ? '#111827' : '#ffffff',
|
||||
}}>
|
||||
<Box sx={{ px: 3, py: 1.5, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: 'rgba(0,0,0,0.02)' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>SQL CONSOLE</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleExecute}
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={16} color="inherit" /> : <PlayArrow />}
|
||||
sx={{ borderRadius: 2, textTransform: 'none', fontWeight: 700 }}
|
||||
>
|
||||
Execute Query
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Editor
|
||||
height="200px"
|
||||
defaultLanguage="sql"
|
||||
theme={darkMode ? 'vs-dark' : 'light'}
|
||||
value={sqlQuery}
|
||||
onChange={(value) => setSqlQuery(value || '')}
|
||||
options={{ minimap: { enabled: false }, fontSize: 14, automaticLayout: true }}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ flexGrow: 1, borderRadius: 3, overflow: 'hidden', border: 1, borderColor: 'divider' }}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
sx={{ border: 'none' }}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SqlConsole;
|
||||
@@ -0,0 +1,149 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Terminal,
|
||||
TableChart,
|
||||
CleaningServices,
|
||||
Save,
|
||||
History,
|
||||
Info
|
||||
} from '@mui/icons-material';
|
||||
import { SchemaService } from '../services/api';
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
import type { MainNotification } from '../types/database';
|
||||
|
||||
interface TechnicalOverviewProps {
|
||||
database: string;
|
||||
table: string | null;
|
||||
setNotification: (n: MainNotification) => void;
|
||||
}
|
||||
|
||||
const TechnicalOverview: React.FC<TechnicalOverviewProps> = ({ database, table, setNotification }) => {
|
||||
const [meta, setMeta] = useState<any>(null);
|
||||
const [loadingMeta, setLoadingMeta] = useState(true);
|
||||
const [truncating, setTruncating] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
|
||||
const fetchMeta = useCallback(async () => {
|
||||
setLoadingMeta(true);
|
||||
try {
|
||||
const res = table
|
||||
? await SchemaService.getTableMetadata(database, table)
|
||||
: await SchemaService.getDatabaseMetadata(database);
|
||||
setMeta(res.data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoadingMeta(false);
|
||||
}
|
||||
}, [database, table]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMeta();
|
||||
}, [fetchMeta]);
|
||||
|
||||
const handleTruncate = async () => {
|
||||
setTruncating(true);
|
||||
setShowConfirm(false);
|
||||
try {
|
||||
await SchemaService.truncateTable(table!);
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'Success',
|
||||
message: `Table "${table}" has been truncated.`,
|
||||
severity: 'success'
|
||||
});
|
||||
fetchMeta();
|
||||
} catch (error: any) {
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'Truncate Error',
|
||||
message: error.response?.data?.error || error.message,
|
||||
severity: 'error'
|
||||
});
|
||||
} finally {
|
||||
setTruncating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loadingMeta) return <Box sx={{ display: 'flex', justifyContent: 'center', p: 8 }}><CircularProgress /></Box>;
|
||||
if (!meta) return <Alert severity="error">Could not load metadata</Alert>;
|
||||
|
||||
const stats = table ? [
|
||||
{ label: 'Engine', value: meta.engine, icon: <Terminal /> },
|
||||
{ label: 'Rows', value: meta.rows, icon: <TableChart /> },
|
||||
{ label: 'Collation', value: meta.collation, icon: <CleaningServices /> },
|
||||
{ label: 'Data Size', value: `${(meta.data_length / 1024 / 1024).toFixed(2)} MB`, icon: <Save /> },
|
||||
{ label: 'Index Size', value: `${(meta.index_length / 1024 / 1024).toFixed(2)} MB`, icon: <History /> },
|
||||
{ label: 'Created', value: meta.create_time, icon: <Info /> },
|
||||
] : [
|
||||
{ label: 'Character Set', value: meta.charset, icon: <History /> },
|
||||
{ label: 'Collation', value: meta.collation, icon: <CleaningServices /> },
|
||||
{ label: 'Tables', value: meta.table_count, icon: <TableChart /> },
|
||||
{ label: 'Views', value: meta.view_count, icon: <History /> },
|
||||
{ label: 'Total Size', value: `${(meta.size_bytes / 1024 / 1024).toFixed(2)} MB`, icon: <Save /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 800, letterSpacing: -0.5 }}>
|
||||
Technical Specifications: {table ? `${database}.${table}` : database}
|
||||
</Typography>
|
||||
{table && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={truncating ? <CircularProgress size={16} color="inherit" /> : <CleaningServices />}
|
||||
onClick={() => setShowConfirm(true)}
|
||||
disabled={truncating}
|
||||
sx={{ borderRadius: 2, textTransform: 'none', fontWeight: 700 }}
|
||||
>
|
||||
Truncate Table
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ConfirmDialog
|
||||
open={showConfirm}
|
||||
onClose={() => setShowConfirm(false)}
|
||||
onConfirm={handleTruncate}
|
||||
title="Truncate Table"
|
||||
message={`Are you sure you want to truncate table "${table}"? This action will permanently delete all ${meta?.rows || ''} records. This cannot be undone.`}
|
||||
confirmLabel="Truncate Now"
|
||||
loading={truncating}
|
||||
/>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 3 }}>
|
||||
{stats.map((stat, i) => (
|
||||
<Paper key={i} sx={{
|
||||
p: 3,
|
||||
borderRadius: 4,
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.01)',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': { borderColor: 'primary.main', transform: 'translateY(-2px)' }
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary', textTransform: 'uppercase', letterSpacing: 1 }}>{stat.label}</Typography>
|
||||
<Box sx={{ color: 'primary.main', opacity: 0.5 }}>{stat.icon}</Box>
|
||||
</Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 800 }}>{stat.value || 'N/A'}</Typography>
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TechnicalOverview;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import type { GridPaginationModel } from '@mui/x-data-grid';
|
||||
import { SchemaService } from '../services/api';
|
||||
|
||||
export const useTableData = (
|
||||
activeTable: string | null,
|
||||
activeDatabase: string | null,
|
||||
paginationModel: GridPaginationModel
|
||||
) => {
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [rowCount, setRowCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!activeTable || !activeDatabase) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = {
|
||||
skip: paginationModel.page * paginationModel.pageSize,
|
||||
take: paginationModel.pageSize,
|
||||
requireTotalCount: true,
|
||||
database: activeDatabase,
|
||||
};
|
||||
|
||||
const response = await SchemaService.getTableData(activeTable, params);
|
||||
|
||||
const dataWithIds = response.data.data.map((row: any, index: number) => ({
|
||||
id: row.id || row.ID || `row-${index}-${paginationModel.page}`,
|
||||
...row,
|
||||
}));
|
||||
|
||||
setRows(dataWithIds);
|
||||
setRowCount(response.data.totalCount || 0);
|
||||
} catch (error) {
|
||||
console.error('Data loading error', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeTable, activeDatabase, paginationModel]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
return { rows, rowCount, loading, refetch: fetchData };
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { GridColDef } from '@mui/x-data-grid';
|
||||
import { SchemaService } from '../services/api';
|
||||
|
||||
const mapSqlTypeToMuiType = (sqlType: string): 'string' | 'number' | 'date' | 'dateTime' | 'boolean' => {
|
||||
sqlType = sqlType.toLowerCase();
|
||||
if (sqlType.includes('int') || sqlType.includes('decimal') || sqlType.includes('float') || sqlType.includes('double')) return 'number';
|
||||
if (sqlType.includes('datetime') || sqlType.includes('timestamp')) return 'dateTime';
|
||||
if (sqlType.includes('date')) return 'date';
|
||||
if (sqlType.includes('bool') || sqlType.includes('tinyint(1)')) return 'boolean';
|
||||
return 'string';
|
||||
};
|
||||
|
||||
export const useTableSchema = (activeTable: string | null, activeDatabase: string | null) => {
|
||||
const [columns, setColumns] = useState<GridColDef[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSchema = async () => {
|
||||
if (!activeTable || !activeDatabase) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const schemaRes = await SchemaService.getTableSchema(activeTable, activeDatabase);
|
||||
const cols: GridColDef[] = schemaRes.data.map((col: any) => {
|
||||
const type = mapSqlTypeToMuiType(col.Type);
|
||||
return {
|
||||
field: col.Field,
|
||||
headerName: col.Field,
|
||||
type: type,
|
||||
width: 200,
|
||||
minWidth: 100,
|
||||
valueGetter: (value: any) => {
|
||||
if ((type === 'date' || type === 'dateTime') && value && typeof value === 'string') {
|
||||
return new Date(value);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
});
|
||||
setColumns(cols);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch table schema', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSchema();
|
||||
}, [activeTable, activeDatabase]);
|
||||
|
||||
return { columns, loading };
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface MainNotification {
|
||||
open: boolean;
|
||||
message: string;
|
||||
title: string;
|
||||
severity: 'success' | 'error' | 'info' | 'warning';
|
||||
}
|
||||
|
||||
export interface TableMetadata {
|
||||
name: string;
|
||||
engine: string;
|
||||
rows: number;
|
||||
data_length: number;
|
||||
index_length: number;
|
||||
collation: string;
|
||||
comment: string;
|
||||
create_time?: string;
|
||||
}
|
||||
|
||||
export interface DatabaseMetadata {
|
||||
charset: string;
|
||||
collation: string;
|
||||
table_count: number;
|
||||
view_count: number;
|
||||
size_bytes: number;
|
||||
}
|
||||
Reference in New Issue
Block a user