import React, { useEffect, useState, useCallback } from 'react'; import { message } from 'antd'; import { TabData, ColumnDefinition } from '../types'; import { useStore } from '../store'; import { DBQuery, DBGetColumns } from '../../wailsjs/go/app/App'; import DataGrid from './DataGrid'; const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => { const [data, setData] = useState([]); const [columnNames, setColumnNames] = useState([]); const [pkColumns, setPkColumns] = useState([]); const [loading, setLoading] = useState(false); const { connections, addSqlLog } = useStore(); const [pagination, setPagination] = useState({ current: 1, pageSize: 100, total: 0 }); const [sortInfo, setSortInfo] = useState<{ columnKey: string, order: string } | null>(null); const [showFilter, setShowFilter] = useState(false); const [filterConditions, setFilterConditions] = useState([]); const fetchData = useCallback(async (page = pagination.current, size = pagination.pageSize) => { setLoading(true); const conn = connections.find(c => c.id === tab.connectionId); if (!conn) { message.error("Connection not found"); setLoading(false); return; } const config = { ...conn.config, port: Number(conn.config.port), password: conn.config.password || "", database: conn.config.database || "", useSSH: conn.config.useSSH || false, ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" } }; const quoteIdent = (ident: string) => { if (!ident) return ident; if (config.type === 'mysql') return `\`${ident.replace(/`/g, '``')}\``; return `"${ident.replace(/"/g, '""')}"`; }; const escapeLiteral = (val: string) => val.replace(/'/g, "''"); const dbName = tab.dbName || ''; const tableName = tab.tableName || ''; const whereParts: string[] = []; filterConditions.forEach(cond => { if (cond.column && cond.value) { if (cond.op === 'LIKE') { whereParts.push(`${quoteIdent(cond.column)} LIKE '%${escapeLiteral(cond.value)}%'`); } else { whereParts.push(`${quoteIdent(cond.column)} ${cond.op} '${escapeLiteral(cond.value)}'`); } } }); const whereSQL = whereParts.length > 0 ? `WHERE ${whereParts.join(' AND ')}` : ""; const countSql = `SELECT COUNT(*) as total FROM ${quoteIdent(tableName)} ${whereSQL}`; let sql = `SELECT * FROM ${quoteIdent(tableName)} ${whereSQL}`; if (sortInfo && sortInfo.order) { sql += ` ORDER BY ${quoteIdent(sortInfo.columnKey)} ${sortInfo.order === 'ascend' ? 'ASC' : 'DESC'}`; } const offset = (page - 1) * size; sql += ` LIMIT ${size} OFFSET ${offset}`; const startTime = Date.now(); try { const pCount = DBQuery(config as any, dbName, countSql); const pData = DBQuery(config as any, dbName, sql); let pCols = null; if (pkColumns.length === 0) { pCols = DBGetColumns(config as any, dbName, tableName); } const [resCount, resData] = await Promise.all([pCount, pData]); const duration = Date.now() - startTime; // Log Execution addSqlLog({ id: `log-${Date.now()}-count`, timestamp: Date.now(), sql: countSql, status: resCount.success ? 'success' : 'error', duration: duration / 2, // Estimate message: resCount.success ? '' : resCount.message, dbName }); addSqlLog({ id: `log-${Date.now()}-data`, timestamp: Date.now(), sql: sql, status: resData.success ? 'success' : 'error', duration: duration, message: resData.success ? '' : resData.message, affectedRows: Array.isArray(resData.data) ? resData.data.length : undefined, dbName }); if (pCols) { const resCols = await pCols; if (resCols.success) { const pks = (resCols.data as ColumnDefinition[]).filter(c => c.key === 'PRI').map(c => c.name); setPkColumns(pks); } } let totalRecords = 0; if (resCount.success && Array.isArray(resCount.data) && resCount.data.length > 0) { totalRecords = Number(resCount.data[0]['total']); } if (resData.success) { let resultData = resData.data as any[]; if (!Array.isArray(resultData)) resultData = []; let fieldNames = resData.fields || []; if (fieldNames.length === 0 && resultData.length > 0) { fieldNames = Object.keys(resultData[0]); } setColumnNames(fieldNames); setData(resultData.map((row: any, i: number) => ({ ...row, key: `row-${i}` }))); setPagination(prev => ({ ...prev, current: page, pageSize: size, total: totalRecords })); } else { message.error(resData.message); } } catch (e: any) { message.error("Error fetching data: " + e.message); addSqlLog({ id: `log-${Date.now()}-error`, timestamp: Date.now(), sql: sql, status: 'error', duration: Date.now() - startTime, message: e.message, dbName }); } setLoading(false); }, [connections, tab, sortInfo, filterConditions, pkColumns.length]); // Depend on pkColumns.length to avoid loop? No, pkColumns is updated inside. // Actually, 'pkColumns' state shouldn't trigger re-fetch. // The 'if (pkColumns.length === 0)' check is inside. // So adding pkColumns to dependency is safer but might trigger double fetch if not careful? // Only if pkColumns changes. It changes once from [] to [...]. // So it's fine. // Handlers memoized const handleReload = useCallback(() => fetchData(), [fetchData]); const handleSort = useCallback((field: string, order: string) => setSortInfo({ columnKey: field, order }), []); const handlePageChange = useCallback((page: number, size: number) => fetchData(page, size), [fetchData]); const handleToggleFilter = useCallback(() => setShowFilter(prev => !prev), []); const handleApplyFilter = useCallback((conditions: any[]) => setFilterConditions(conditions), []); useEffect(() => { fetchData(1, pagination.pageSize); }, [tab, sortInfo, filterConditions]); // Initial load and re-load on sort/filter return (
); }; export default DataViewer;