diff --git a/frontend/src/components/DatabaseTablesGrid.tsx b/frontend/src/components/DatabaseTablesGrid.tsx new file mode 100644 index 0000000..a8ed772 --- /dev/null +++ b/frontend/src/components/DatabaseTablesGrid.tsx @@ -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 = ({ database, setNotification }) => { + 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' }} + /> + + + ); +}; + +export default DatabaseTablesGrid; diff --git a/frontend/src/components/MainContent.tsx b/frontend/src/components/MainContent.tsx index 90d16f2..482e302 100644 --- a/frontend/src/components/MainContent.tsx +++ b/frontend/src/components/MainContent.tsx @@ -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 + 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([]); - 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'} - - ))} - - - ); -}; +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([]); - 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: '', @@ -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 ( @@ -591,49 +157,10 @@ const MainContent: React.FC = () => { {/* SQL View */} {dbTab === 'sql' && ( - - - - SQL CONSOLE - - - - - setSqlQuery(value || '')} - options={{ minimap: { enabled: false }, fontSize: 14, automaticLayout: true }} - /> - - - - - - + )} {/* Import/Export Views */} diff --git a/frontend/src/components/SqlConsole.tsx b/frontend/src/components/SqlConsole.tsx new file mode 100644 index 0000000..621f628 --- /dev/null +++ b/frontend/src/components/SqlConsole.tsx @@ -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 = ({ initialQuery, setNotification }) => { + const { darkMode } = useAppStore(); + const [sqlQuery, setSqlQuery] = useState(initialQuery); + const [loading, setLoading] = useState(false); + const [rows, setRows] = useState([]); + const [columns, setColumns] = useState([]); + 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 ( + + + + SQL CONSOLE + + + + + setSqlQuery(value || '')} + options={{ minimap: { enabled: false }, fontSize: 14, automaticLayout: true }} + /> + + + + + + + ); +}; + +export default SqlConsole; diff --git a/frontend/src/components/TechnicalOverview.tsx b/frontend/src/components/TechnicalOverview.tsx new file mode 100644 index 0000000..954aee4 --- /dev/null +++ b/frontend/src/components/TechnicalOverview.tsx @@ -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 = ({ database, table, setNotification }) => { + 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'} + + ))} + + + ); +}; + +export default TechnicalOverview; diff --git a/frontend/src/hooks/useTableData.ts b/frontend/src/hooks/useTableData.ts new file mode 100644 index 0000000..55e21cb --- /dev/null +++ b/frontend/src/hooks/useTableData.ts @@ -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([]); + 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 }; +}; diff --git a/frontend/src/hooks/useTableSchema.ts b/frontend/src/hooks/useTableSchema.ts new file mode 100644 index 0000000..7a82c71 --- /dev/null +++ b/frontend/src/hooks/useTableSchema.ts @@ -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([]); + 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 }; +}; diff --git a/frontend/src/types/database.ts b/frontend/src/types/database.ts new file mode 100644 index 0000000..0f33ca1 --- /dev/null +++ b/frontend/src/types/database.ts @@ -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; +}