import React, { useEffect, useState, useCallback } from 'react'; import { Box, Paper, Typography, CircularProgress, Button, Divider, IconButton, Tooltip, Snackbar, Alert, AlertTitle, Tabs, Tab, Stack, Checkbox } 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 { 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([]); const [loading, setLoading] = useState(true); const [selectionModel, setSelectionModel] = useState([]); 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: () => ( 0 && selectionModel.length < tableRows.length} checked={tableRows.length > 0 && selectionModel.length === tableRows.length} onChange={toggleAll} /> ), renderCell: (params) => ( { e.stopPropagation(); toggleRow(params.row.name); }} /> ) }, { field: 'name', headerName: 'Table Name', flex: 1, minWidth: 200, renderCell: (params) => ( ) }, { 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 ( {selectionModel.length > 0 && ( {selectionModel.length} tables selected: )} row.name} density="comfortable" sx={{ border: 'none' }} /> ); }; const TechnicalOverview = ({ database, table, setNotification }: { database: string, table: string | null, setNotification: (n: MainNotification) => void }) => { const [meta, setMeta] = useState(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 ; if (!meta) return Could not load metadata; const stats = table ? [ { label: 'Engine', value: meta.engine, icon: }, { label: 'Rows', value: meta.rows, icon: }, { label: 'Collation', value: meta.collation, icon: }, { label: 'Data Size', value: `${(meta.data_length / 1024 / 1024).toFixed(2)} MB`, icon: }, { label: 'Index Size', value: `${(meta.index_length / 1024 / 1024).toFixed(2)} MB`, icon: }, { label: 'Created', value: meta.create_time, icon: }, ] : [ { label: 'Character Set', value: meta.charset, icon: }, { label: 'Collation', value: meta.collation, icon: }, { label: 'Tables', value: meta.table_count, icon: }, { label: 'Views', value: meta.view_count, icon: }, { label: 'Total Size', value: `${(meta.size_bytes / 1024 / 1024).toFixed(2)} MB`, icon: }, ]; return ( Technical Specifications: {table ? `${database}.${table}` : database} {table && ( )} 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} /> {stats.map((stat, i) => ( 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)' } }}> {stat.label} {stat.icon} {stat.value || 'N/A'} ))} ); }; const MainContent: React.FC = () => { const { activeTable, activeDatabase, darkMode, dbTab, setDbTab } = useAppStore(); const [columns, setColumns] = useState([]); const [rows, setRows] = useState([]); 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({ page: 0, pageSize: 100, }); // Custom Notification State const [notification, setNotification] = useState({ open: false, message: '', title: '', severity: 'error' }); 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 ( Select a database to start ); } const handleTabChange = (_: React.SyntheticEvent, newValue: string) => { setDbTab(newValue); }; return ( {/* Database Navigation Tabs */} } iconPosition="start" label="Data Explorer" /> } iconPosition="start" label="SQL Editor" /> } iconPosition="start" label="Import" /> } iconPosition="start" label="Export" /> } iconPosition="start" label="Technical Info" /> {/* Tables View */} {dbTab === 'tables' && ( {!activeTable ? ( Database Tables: {activeDatabase} ) : ( <> Table Results: {activeTable} {rowCount} rows found {loadingSchema ? ( ) : ( )} )} )} {/* SQL View */} {dbTab === 'sql' && ( SQL CONSOLE setSqlQuery(value || '')} options={{ minimap: { enabled: false }, fontSize: 14, automaticLayout: true }} /> )} {/* Import/Export Views */} {dbTab === 'import' && } {dbTab === 'export' && } {/* Technical Info View */} {dbTab === 'info' && } {/* Custom Notification (Snackbar) */} {notification.title} {notification.message} ); }; export default MainContent;