import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Alert, Button, Progress, Typography } from 'antd'; import { PlayCircleOutlined, ReloadOutlined, StopOutlined, } from '@ant-design/icons'; import { CancelSQLFileExecution, ImportDatabaseSQL } from '../../wailsjs/go/app/App'; import { t as defaultTranslate } from '../i18n'; import { useOptionalI18n } from '../i18n/provider'; import type { SavedConnection } from '../types'; import { confirmProductionRisk } from '../utils/productionRiskConfirm'; import { formatImportBytes, formatImportDuration } from './importProgressMetrics'; import Modal from './common/ResizableDraggableModal'; import { useSQLFileExecutionRunner, type SQLFileExecutionRunnerStatus, } from './useSQLFileExecutionRunner'; const { Paragraph, Text, Title } = Typography; type DatabaseImportExecutionPanelProps = { connection?: SavedConnection; connectionConfig: Record | null; dbName?: string; filePath: string; fileSizeMB?: string; darkMode: boolean; continueOnError: boolean; onRunningChange?: (running: boolean) => void; }; const getFileName = (filePath: string): string => { const parts = String(filePath || '').split(/[\\/]/); return parts[parts.length - 1] || filePath; }; const resolveProgressStatus = ( status: SQLFileExecutionRunnerStatus, ): 'active' | 'success' | 'exception' | 'normal' => { if (status === 'done') return 'success'; if (status === 'error') return 'exception'; if (status === 'start' || status === 'running' || status === 'stopping') return 'active'; return 'normal'; }; const DatabaseImportExecutionPanel: React.FC = ({ connection, connectionConfig, dbName = '', filePath, fileSizeMB, darkMode, continueOnError, onRunningChange, }) => { const i18n = useOptionalI18n(); const t = i18n?.t ?? defaultTranslate; const [executionPending, setExecutionPending] = useState(false); const [cancelRequested, setCancelRequested] = useState(false); const lastReportedRunningRef = useRef(null); const { state, reset, cancelExecution, runSQLFileExecutionWithProgress, isRunning, } = useSQLFileExecutionRunner({ showToast: false }); const taskRunning = isRunning || executionPending; const progressPercent = Math.max(0, Math.min(100, Number(state.percent) || 0)); const terminal = state.status === 'done' || state.status === 'cancelled' || state.status === 'error'; const completedWithErrors = state.status === 'done' && state.failed > 0; const subtleBackground = `var(--gn-bg-subtle, var(--gn-bg-panel-2, ${darkMode ? 'rgba(255,255,255,0.04)' : '#f8fafc'}))`; const dividerColor = `var(--gn-br-1, ${darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(15,23,42,0.08)'})`; const warningColor = 'var(--gn-warn, #faad14)'; const transferMetrics = useMemo(() => { if (state.bytesRead <= 0 && state.totalBytes <= 0) return ''; const details = [t('data_import.workbench.progress.bytes', { processed: formatImportBytes(state.bytesRead), total: state.totalBytes > 0 ? formatImportBytes(state.totalBytes) : '—', })]; if (state.bytesPerSecond > 0) { details.push(t('data_import.workbench.progress.throughput', { rate: formatImportBytes(state.bytesPerSecond), })); } if (state.etaSeconds > 0) { details.push(t('data_import.workbench.progress.eta', { duration: formatImportDuration(state.etaSeconds, i18n?.language), })); } return details.join(' · '); }, [i18n?.language, state.bytesPerSecond, state.bytesRead, state.etaSeconds, state.totalBytes, t]); useEffect(() => { if (lastReportedRunningRef.current === taskRunning) return; lastReportedRunningRef.current = taskRunning; onRunningChange?.(taskRunning); }, [onRunningChange, taskRunning]); const startImport = useCallback(async () => { if (!connectionConfig || !String(filePath || '').trim() || taskRunning) return; const approved = await confirmProductionRisk({ connection, action: t('connection.production_risk.action.execute_sql'), target: [dbName, getFileName(filePath)].filter(Boolean).join(' / '), translate: t, }); if (!approved) return; setExecutionPending(true); setCancelRequested(false); try { await runSQLFileExecutionWithProgress({ title: getFileName(filePath), filePath, fileSizeMB, run: async (jobId) => { const result = await ImportDatabaseSQL( connectionConfig as any, String(dbName || '').trim(), filePath, jobId, continueOnError, ); // Reaching EOF with recorded statement errors is a completed import, // not a transport/fatal failure. Preserve the counters and render it // as a warning result instead of offering a misleading fatal retry. if (continueOnError && result.data?.completed === true) { return { ...result, success: true }; } return result; }, cancel: async (jobId) => { const result = await CancelSQLFileExecution(jobId); if (!result?.success) { throw new Error(result?.message || t('import_preview.error.stop_failed')); } }, }); } catch { // The shared runner already records and displays the RPC error state. } finally { setExecutionPending(false); } }, [ connectionConfig, connection, continueOnError, dbName, filePath, fileSizeMB, runSQLFileExecutionWithProgress, taskRunning, t, ]); const requestCancel = useCallback(async () => { if (!taskRunning || cancelRequested) return; setCancelRequested(true); try { await cancelExecution(); } catch { setCancelRequested(false); } }, [cancelExecution, cancelRequested, taskRunning]); const resetProgress = useCallback(() => { if (taskRunning) return; setCancelRequested(false); reset(); }, [reset, taskRunning]); const requestStartImport = useCallback(() => { if (!terminal) { void startImport(); return; } Modal.confirm({ title: t('data_import.workbench.confirm.rerun_title'), content: t('data_import.workbench.confirm.rerun_content'), okText: t('data_import.workbench.action.retry_database_import'), cancelText: t('common.cancel'), okButtonProps: { danger: true }, onOk: startImport, }); }, [startImport, t, terminal]); const statusText = useMemo(() => { if (cancelRequested && taskRunning) return t('data_import.workbench.state.cancelling'); switch (state.status) { case 'start': case 'running': return t('data_import.workbench.state.running'); case 'done': return state.failed > 0 ? t('data_import.workbench.state.completed_with_errors') : t('data_import.workbench.state.completed'); case 'error': return t('data_import.workbench.state.failed'); case 'cancelled': return t('data_import.workbench.state.cancelled'); default: return t('data_import.workbench.state.ready_sql_title'); } }, [cancelRequested, state.failed, state.status, t, taskRunning]); const stageText = useMemo(() => { switch (state.stage) { case 'prepare': return t('import_preview.stage.prepare'); case 'preflight': return t('import_preview.stage.preflight'); case 'read': return t('import_preview.stage.read'); case 'parse': return t('import_preview.stage.parse'); case 'write': return t('import_preview.stage.write'); case 'finalize': return t('import_preview.stage.finalize'); default: return state.stage || statusText; } }, [state.stage, statusText, t]); const resultAlertType = state.status === 'error' ? 'error' : state.status === 'cancelled' ? 'warning' : completedWithErrors ? 'warning' : 'success'; return (
{statusText} {state.status === 'idle' ? t('data_import.workbench.state.ready_sql_description') : state.filePath || filePath}
{fileSizeMB ? {fileSizeMB} MB : null}
{state.status !== 'idle' ? (
{stageText} {t('data_import.workbench.progress.statements', { executed: state.executed, failed: state.failed, total: state.total, })}
{transferMetrics ? ( {transferMetrics} ) : null}
) : null} {state.currentSQL ? (
{state.currentSQL}
) : null} {terminal && state.message ? ( {state.message}
} /> ) : null}
{taskRunning ? ( ) : ( )} {terminal ? ( ) : null}
); }; export default DatabaseImportExecutionPanel;