import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Button, Empty, Progress, Typography } from 'antd'; import { ClockCircleOutlined, FileTextOutlined, ReloadOutlined, StopOutlined, } from '@ant-design/icons'; import { ExecuteSQLFile, CancelSQLFileExecution } from '../../wailsjs/go/app/App'; import { useStore } from '../store'; import type { TabData } from '../types'; import { t } from '../i18n'; import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig'; import { resolveConnectionHostSummary } from '../utils/tabDisplay'; import { formatExportElapsed, resolveExportElapsedMs } from '../utils/exportProgress'; import { useSQLFileExecutionRunner, type SQLFileExecutionRunnerStatus, type SQLFileExecutionState, } from './useSQLFileExecutionRunner'; const { Paragraph, Text, Title } = Typography; type SQLFileExecutionHistoryEntry = SQLFileExecutionState & { requestKey: string; }; const EMPTY_HISTORY: SQLFileExecutionHistoryEntry[] = []; const formatDateTime = (timestamp: number): string => { if (!Number.isFinite(timestamp) || timestamp <= 0) { return '-'; } return new Date(timestamp).toLocaleString('zh-CN', { hour12: false }); }; const resolveStatusMeta = (status: SQLFileExecutionRunnerStatus): { label: string; border: string; bg: string; text: string; } => { const meta: Record = { idle: { label: t('sidebar.sql_file_exec.workbench.empty.not_started'), border: 'rgba(148, 163, 184, 0.35)', bg: 'rgba(148, 163, 184, 0.12)', text: '#475467', }, start: { label: t('sidebar.sql_file_exec.workbench.stage.preparing'), border: 'rgba(59, 130, 246, 0.3)', bg: 'rgba(59, 130, 246, 0.12)', text: '#1d4ed8', }, running: { label: t('sidebar.sql_file_exec.status.running'), border: 'rgba(16, 185, 129, 0.3)', bg: 'rgba(16, 185, 129, 0.14)', text: '#047857', }, done: { label: t('sidebar.sql_file_exec.status.done'), border: 'rgba(34, 197, 94, 0.3)', bg: 'rgba(34, 197, 94, 0.14)', text: '#15803d', }, cancelled: { label: t('sidebar.sql_file_exec.status.cancelled'), border: 'rgba(249, 115, 22, 0.3)', bg: 'rgba(249, 115, 22, 0.12)', text: '#c2410c', }, error: { label: t('sidebar.sql_file_exec.status.error'), border: 'rgba(239, 68, 68, 0.32)', bg: 'rgba(239, 68, 68, 0.12)', text: '#dc2626', }, }; return meta[status]; }; const renderStatusPill = (status: SQLFileExecutionRunnerStatus) => { const meta = resolveStatusMeta(status); return ( {meta.label} ); }; const formatExecutionSummary = (executed: number, failed: number): string => `${t('sidebar.sql_file_exec.executed_label')}${executed.toLocaleString()}${t('sidebar.sql_file_exec.rows_separator')}${failed.toLocaleString()}${t('sidebar.sql_file_exec.rows_suffix')}`; const resolveProgressStatus = (status: SQLFileExecutionRunnerStatus): 'active' | 'success' | 'exception' | 'normal' => { if (status === 'done') return 'success'; if (status === 'error') return 'exception'; if (status === 'start' || status === 'running') return 'active'; return 'normal'; }; const SQLFileExecutionWorkbench: React.FC<{ tab: TabData }> = ({ tab }) => { const connections = useStore((state) => state.connections); const theme = useStore((state) => state.theme); const [nowTick, setNowTick] = useState(() => Date.now()); const [historyEntries, setHistoryEntries] = useState(EMPTY_HISTORY); const lastArchivedJobIdRef = useRef(''); const lastRequestKeyRef = useRef(''); const darkMode = theme === 'dark'; const shellBg = darkMode ? '#101319' : '#f5f7fb'; const panelBg = darkMode ? '#161b22' : '#ffffff'; const panelBorder = darkMode ? '1px solid rgba(255,255,255,0.08)' : '1px solid rgba(15,23,42,0.08)'; const dividerColor = darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(15,23,42,0.08)'; const headingColor = darkMode ? 'rgba(255,255,255,0.96)' : '#101828'; const secondaryTextColor = darkMode ? 'rgba(255,255,255,0.68)' : '#667085'; const subtleBg = darkMode ? 'rgba(255,255,255,0.04)' : '#f8fafc'; const connection = useMemo( () => connections.find((item) => item.id === String(tab.connectionId || '').trim()), [connections, tab.connectionId], ); const connectionConfig = useMemo( () => (connection ? buildRpcConnectionConfig(connection.config) : null), [connection], ); const hostSummary = useMemo( () => resolveConnectionHostSummary(connection?.config), [connection?.config], ); const { state, reset, cancelExecution, runSQLFileExecutionWithProgress, isRunning } = useSQLFileExecutionRunner({ showToast: true, }); useEffect(() => { if (!state.startedAt || state.finishedAt > 0) { return undefined; } const timer = globalThis.setInterval(() => { setNowTick(Date.now()); }, 1000); return () => { globalThis.clearInterval(timer); }; }, [state.finishedAt, state.startedAt]); useEffect(() => { if (!state.jobId) { return; } if (state.status !== 'done' && state.status !== 'cancelled' && state.status !== 'error') { return; } if (lastArchivedJobIdRef.current === state.jobId) { return; } lastArchivedJobIdRef.current = state.jobId; setHistoryEntries((prev) => [ { ...state, requestKey: String(tab.sqlFileExecutionRequestKey || '').trim(), }, ...prev.filter((entry) => entry.jobId !== state.jobId), ].slice(0, 10)); }, [state, tab.sqlFileExecutionRequestKey]); const startExecution = React.useCallback(async () => { const filePath = String(tab.filePath || '').trim(); if (!connectionConfig || !filePath) { return; } await runSQLFileExecutionWithProgress({ title: tab.title || t('sidebar.sql_file_exec.title'), filePath, fileSizeMB: tab.sqlFileExecutionFileSizeMB, run: (jobId) => ExecuteSQLFile(connectionConfig as any, tab.dbName || '', filePath, jobId), cancel: (jobId) => { CancelSQLFileExecution(jobId); }, }); }, [ connectionConfig, runSQLFileExecutionWithProgress, tab.dbName, tab.filePath, tab.sqlFileExecutionFileSizeMB, tab.title, ]); useEffect(() => { const requestKey = String(tab.sqlFileExecutionRequestKey || '').trim(); if (!requestKey || requestKey === lastRequestKeyRef.current) { return; } if (!connectionConfig || !String(tab.filePath || '').trim()) { return; } lastRequestKeyRef.current = requestKey; void startExecution(); }, [connectionConfig, startExecution, tab.filePath, tab.sqlFileExecutionRequestKey]); const currentElapsedMs = useMemo( () => resolveExportElapsedMs(state.startedAt, state.finishedAt, nowTick), [nowTick, state.finishedAt, state.startedAt], ); const progressPercent = Math.max(0, Math.min(100, Number(state.percent) || 0)); const currentSummary = formatExecutionSummary(state.executed, state.failed); const historyList = useMemo( () => historyEntries.filter((entry) => entry.jobId !== state.jobId), [historyEntries, state.jobId], ); return (
{t('sidebar.sql_file_exec.title')}
{t('sidebar.sql_file_exec.workbench.helper.auto_run')}
{t('sidebar.sql_file_exec.workbench.section.config')}
{t('data_export.label.connection')} {connection?.name || '-'} {t('data_export.label.database')} {tab.dbName || '-'} {t('sidebar.sql_file_exec.workbench.label.file_path')} {tab.filePath || '-'} {t('sidebar.sql_file_exec.file_size').replace(/[::]\s*$/, '')} {tab.sqlFileExecutionFileSizeMB ? `${tab.sqlFileExecutionFileSizeMB} MB` : '-'} {t('data_export.label.host')} {hostSummary || '-'}
{!connectionConfig ? ( ) : null}
{t('sidebar.sql_file_exec.workbench.helper.reuse')}
{isRunning ? ( ) : ( )} {(state.status === 'done' || state.status === 'cancelled' || state.status === 'error') ? ( ) : null}
{t('data_export.workbench.section.current_task')}
{renderStatusPill(state.status)}
{state.title || tab.title || t('sidebar.sql_file_exec.title')}
{state.jobId ? `${state.filePath || tab.filePath || '-'} · ${currentSummary}` : t('sidebar.sql_file_exec.workbench.description.current_task_empty')}
{t('sidebar.sql_file_exec.workbench.label.elapsed')}
{state.startedAt ? formatExportElapsed(currentElapsedMs) : '--:--'}
{t('sidebar.sql_file_exec.workbench.label.started_at')}
{formatDateTime(state.startedAt)}
{t('sidebar.sql_file_exec.workbench.label.file_path')}
{state.filePath || tab.filePath || '-'}
{t('sidebar.sql_file_exec.workbench.label.progress_summary')}
{state.jobId ? currentSummary : '-'}
{state.jobId ? ( <>
{t('sidebar.sql_file_exec.workbench.label.current_stage')}
{state.stage || resolveStatusMeta(state.status).label}
{t('sidebar.sql_file_exec.workbench.label.file_path')}
{state.filePath || '-'}
{state.currentSQL ? (
{t('sidebar.sql_file_exec.workbench.label.current_sql')}
{state.currentSQL}
) : null} {state.message ? ( ) : null} ) : (
)}
{t('data_export.workbench.section.history')}
{t('sidebar.sql_file_exec.workbench.empty.history')}
{historyList.length.toLocaleString()}
{historyList.length > 0 ? ( ) : null}
{historyList.length > 0 ? (
{historyList.map((entry, index) => { const elapsed = formatExportElapsed(resolveExportElapsedMs(entry.startedAt, entry.finishedAt, nowTick)); return (
{entry.title || tab.title} {renderStatusPill(entry.status)} {formatExecutionSummary(entry.executed, entry.failed)}
{entry.stage || resolveStatusMeta(entry.status).label}
{entry.message ? (
{entry.message}
) : null}
{t('sidebar.sql_file_exec.workbench.label.started_at')} {formatDateTime(entry.startedAt)} {t('sidebar.sql_file_exec.workbench.label.elapsed')} {elapsed} {t('sidebar.sql_file_exec.workbench.label.file_path')} {entry.filePath || '-'}
); })}
) : (
{t('sidebar.sql_file_exec.workbench.empty.history')}
)}
); }; export default SQLFileExecutionWorkbench;