diff --git a/backend/app/Contracts/DatabaseDriverInterface.php b/backend/app/Contracts/DatabaseDriverInterface.php index 74445f3..152f6ba 100644 --- a/backend/app/Contracts/DatabaseDriverInterface.php +++ b/backend/app/Contracts/DatabaseDriverInterface.php @@ -22,12 +22,12 @@ interface DatabaseDriverInterface /** * Get table data. */ - public function getTableData(string $table, int $limit = 100, int $offset = 0): array; + public function getTableData(string $table, int $limit = 100, int $offset = 0, array $filters = []): array; /** * Get the count of rows in a table. */ - public function getTableCount(string $table): int; + public function getTableCount(string $table, array $filters = []): int; /** * Get the underlying connection instance. diff --git a/backend/app/Http/Controllers/Api/SchemaController.php b/backend/app/Http/Controllers/Api/SchemaController.php index 58aaa3b..7fe2471 100644 --- a/backend/app/Http/Controllers/Api/SchemaController.php +++ b/backend/app/Http/Controllers/Api/SchemaController.php @@ -68,15 +68,16 @@ class SchemaController extends Controller $skip = $request->get('skip', 0); $take = $request->get('take', 100); + $filters = json_decode($request->get('filters', '[]'), true); - $data = $this->databaseService->getTableData($table, $take, $skip); + $data = $this->databaseService->getTableData($table, $take, $skip, $filters); $response = [ 'data' => $data, ]; if ($request->get('requireTotalCount') === 'true') { - $response['totalCount'] = $this->databaseService->getTableCount($table); + $response['totalCount'] = $this->databaseService->getTableCount($table, $filters); } return Response::json($response); diff --git a/backend/app/Services/Database/MySqlDriver.php b/backend/app/Services/Database/MySqlDriver.php index 181ef1d..62e4eeb 100644 --- a/backend/app/Services/Database/MySqlDriver.php +++ b/backend/app/Services/Database/MySqlDriver.php @@ -64,15 +64,68 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface return $this->query("DESCRIBE `{$table}`"); } - public function getTableData(string $table, int $limit = 100, int $offset = 0): array + public function getTableData(string $table, int $limit = 100, int $offset = 0, array $filters = []): array { - return $this->query("SELECT * FROM `{$table}` LIMIT ? OFFSET ?", [$limit, $offset]); + $query = DB::connection($this->connectionName)->table($table); + $this->applyFilters($query, $filters); + return $query->limit($limit)->offset($offset)->get()->all(); } - public function getTableCount(string $table): int + public function getTableCount(string $table, array $filters = []): int { - $result = $this->query("SELECT COUNT(*) as count FROM `{$table}`"); - return (int) ($result[0]->count ?? 0); + $query = DB::connection($this->connectionName)->table($table); + $this->applyFilters($query, $filters); + return $query->count(); + } + + protected function applyFilters($query, array $filters) + { + if (empty($filters)) return $query; + + foreach ($filters as $filter) { + $field = $filter['field'] ?? ($filter['columnField'] ?? null); + $operator = $filter['operator'] ?? ($filter['operatorValue'] ?? null); + $value = $filter['value'] ?? null; + + if (!$field) continue; + + switch ($operator) { + case 'contains': + $query->where($field, 'LIKE', "%{$value}%"); + break; + case 'equals': + case '=': + $query->where($field, '=', $value); + break; + case 'startsWith': + $query->where($field, 'LIKE', "{$value}%"); + break; + case 'endsWith': + $query->where($field, 'LIKE', "%{$value}"); + break; + case 'isEmpty': + $query->where(function($q) use ($field) { + $q->whereNull($field)->orWhere($field, ''); + }); + break; + case 'isNotEmpty': + $query->whereNotNull($field)->where($field, '!=', ''); + break; + case 'isAnyOf': + if (is_array($value)) { + $query->whereIn($field, $value); + } + break; + case '>': + case '<': + case '>=': + case '<=': + case '!=': + $query->where($field, $operator, $value); + break; + } + } + return $query; } public function getForeignKeys(string $table): array diff --git a/backend/app/Services/DatabaseService.php b/backend/app/Services/DatabaseService.php index 0fe8edc..a582154 100644 --- a/backend/app/Services/DatabaseService.php +++ b/backend/app/Services/DatabaseService.php @@ -78,17 +78,17 @@ class DatabaseService /** * Get table data. */ - public function getTableData(string $table, int $limit = 100, int $offset = 0): array + public function getTableData(string $table, int $limit = 100, int $offset = 0, array $filters = []): array { - return $this->getDriver()->getTableData($table, $limit, $offset); + return $this->getDriver()->getTableData($table, $limit, $offset, $filters); } /** * Get table row count. */ - public function getTableCount(string $table): int + public function getTableCount(string $table, array $filters = []): int { - return $this->getDriver()->getTableCount($table); + return $this->getDriver()->getTableCount($table, $filters); } /** diff --git a/frontend/src/components/MainContent.tsx b/frontend/src/components/MainContent.tsx index b836ee3..f8dfd7e 100644 --- a/frontend/src/components/MainContent.tsx +++ b/frontend/src/components/MainContent.tsx @@ -20,7 +20,13 @@ import { Save, Undo } from '@mui/icons-material'; -import { DataGrid, useGridApiRef, type GridPaginationModel, type GridRowModel } from '@mui/x-data-grid'; +import { + DataGrid, + useGridApiRef, + type GridPaginationModel, + type GridRowModel, + type GridFilterModel +} from '@mui/x-data-grid'; import { useAppStore } from '../store/useAppStore'; import { SchemaService } from '../services/api'; import TransferContent from './TransferContent'; @@ -39,6 +45,7 @@ const MainContent: React.FC = () => { page: 0, pageSize: 100, }); + const [filterModel, setFilterModel] = useState({ items: [] }); const [notification, setNotification] = useState({ open: false, @@ -58,7 +65,7 @@ const MainContent: React.FC = () => { const [selectionEnd, setSelectionEnd] = useState<{ id: string | number, field: string } | null>(null); const { columns, loading: loadingSchema, primaryKey } = useTableSchema(activeTable, activeDatabase); - const { rows, rowCount, loading: loadingData, refetch } = useTableData(activeTable, activeDatabase, paginationModel); + const { rows, rowCount, loading: loadingData, refetch } = useTableData(activeTable, activeDatabase, paginationModel, filterModel); const handleRowUpdate = (newRow: GridRowModel, oldRow: GridRowModel) => { const hasChanged = Object.keys(newRow).some(key => newRow[key] !== oldRow[key]); @@ -101,6 +108,51 @@ const MainContent: React.FC = () => { } }; + const handleExport = async (format: 'sql' | 'csv') => { + if (!activeTable || !activeDatabase) return; + try { + setNotification({ open: true, title: 'Exporting', message: 'Generating export file...', severity: 'success' }); + + // For now, we'll use the existing SQL export for 'sql' + // and implement a client-side CSV export for 'csv' since we have the rows + if (format === 'sql') { + const response = await SchemaService.exportDatabase(activeDatabase, activeTable); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `${activeTable}_${new Date().toISOString().split('T')[0]}.sql`); + document.body.appendChild(link); + link.click(); + link.remove(); + } else { + // Simple CSV Export + const headers = columns.map(c => c.headerName || c.field).join(','); + const csvRows = rows.map(row => + columns.map(c => { + const val = row[c.field]; + return typeof val === 'string' ? `"${val.replace(/"/g, '""')}"` : val; + }).join(',') + ); + const csvContent = [headers, ...csvRows].join('\n'); + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `${activeTable}_export.csv`); + document.body.appendChild(link); + link.click(); + link.remove(); + } + } catch (error: any) { + setNotification({ + open: true, + title: 'Export Failed', + message: error.message, + severity: 'error' + }); + } + }; + const handlePaste = useCallback((event: React.ClipboardEvent) => { if (!cellFocus || !activeTable) return; @@ -385,6 +437,10 @@ const MainContent: React.FC = () => { {rowCount} rows found + + + + {Object.keys(pendingChanges).length > 0 && ( @@ -444,6 +500,9 @@ const MainContent: React.FC = () => { paginationMode="server" paginationModel={paginationModel} onPaginationModelChange={setPaginationModel} + filterMode="server" + filterModel={filterModel} + onFilterModelChange={setFilterModel} pageSizeOptions={[25, 50, 100]} processRowUpdate={handleRowUpdate} onProcessRowUpdateError={(error) => { diff --git a/frontend/src/hooks/useTableData.ts b/frontend/src/hooks/useTableData.ts index 55e21cb..0d4fa65 100644 --- a/frontend/src/hooks/useTableData.ts +++ b/frontend/src/hooks/useTableData.ts @@ -1,11 +1,12 @@ import { useState, useEffect, useCallback } from 'react'; -import type { GridPaginationModel } from '@mui/x-data-grid'; +import type { GridPaginationModel, GridFilterModel } from '@mui/x-data-grid'; import { SchemaService } from '../services/api'; export const useTableData = ( activeTable: string | null, activeDatabase: string | null, - paginationModel: GridPaginationModel + paginationModel: GridPaginationModel, + filterModel?: GridFilterModel ) => { const [rows, setRows] = useState([]); const [rowCount, setRowCount] = useState(0); @@ -16,13 +17,17 @@ export const useTableData = ( setLoading(true); try { - const params = { + const params: any = { skip: paginationModel.page * paginationModel.pageSize, take: paginationModel.pageSize, requireTotalCount: true, database: activeDatabase, }; + if (filterModel && filterModel.items.length > 0) { + params.filters = JSON.stringify(filterModel.items); + } + const response = await SchemaService.getTableData(activeTable, params); const dataWithIds = response.data.data.map((row: any, index: number) => ({ @@ -37,7 +42,7 @@ export const useTableData = ( } finally { setLoading(false); } - }, [activeTable, activeDatabase, paginationModel]); + }, [activeTable, activeDatabase, paginationModel, filterModel]); useEffect(() => { fetchData();