UNPKG

payload-wordpress-migrator

Version:

A PayloadCMS plugin for WordPress migration - migrate and manage WordPress content directly in your Payload admin dashboard

944 lines (943 loc) 54.1 kB
'use client'; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import React, { useEffect, useState } from 'react'; import styles from './MigrationDashboardClient.module.css'; export const MigrationDashboardClient = ({ summary: initialSummary })=>{ const [summary, setSummary] = useState(initialSummary); const [loading, setLoading] = useState(false); const [logs, setLogs] = useState([]); // WordPress site configuration state const [siteConfig, setSiteConfig] = useState({ connectionStatus: 'not-scanned', discoveredContent: [], siteName: '', totalItems: 0, wpPassword: '', wpSiteUrl: '', wpUsername: '' }); const [configState, setConfigState] = useState({ isEditing: true, isSaved: false }); const [showSiteConfig, setShowSiteConfig] = useState(true) // Open by default for better UX ; // Add log entry with timestamp const addLog = (message)=>{ const timestamp = new Date().toLocaleTimeString(); const logEntry = `[${timestamp}] ${message}`; setLogs((prev)=>[ ...prev, logEntry ]); }; // Clear logs (called when starting new migration) const clearLogs = ()=>{ setLogs([]); }; // Reusable function to refresh dashboard data const refreshDashboard = async ()=>{ try { console.trace('Call stack:'); const response = await fetch('/api/wordpress/migration-summary?refresh=true'); const newSummary = await response.json(); setSummary(newSummary); } catch (error) { console.error('Failed to refresh dashboard:', error); } }; // Load initial data on mount useEffect(()=>{ const loadInitialData = async ()=>{ await refreshDashboard(); }; // Load saved WordPress configuration from localStorage const loadSavedConfig = ()=>{ try { const savedConfig = localStorage.getItem('wp-site-config'); if (savedConfig) { const parsed = JSON.parse(savedConfig); setSiteConfig(parsed); setConfigState({ isEditing: false, isSaved: true }); } } catch (error) { console.error('Failed to load saved config:', error); } }; void loadInitialData(); loadSavedConfig(); }, []); // Smart periodic refresh - only when jobs are active useEffect(()=>{ let refreshInterval = null; if (summary.activeJobs > 0) { // Only refresh when there are active jobs refreshInterval = setInterval(()=>{ void refreshDashboard(); }, 15000); // Refresh every 15 seconds when jobs are active } return ()=>{ if (refreshInterval) { clearInterval(refreshInterval); } }; }, [ summary.activeJobs ]); // Re-run when activeJobs count changes // Auto-close site config when scan is successful AND config is saved (with delay to show results) useEffect(()=>{ if (siteConfig.connectionStatus === 'scanned' && configState.isSaved && !configState.isEditing && siteConfig.discoveredContent && siteConfig.discoveredContent.length > 0) { // Wait 5 seconds to show the discovered content before auto-closing const timer = setTimeout(()=>{ setShowSiteConfig(false); }, 5000); return ()=>clearTimeout(timer); } }, [ siteConfig.connectionStatus, configState.isSaved, configState.isEditing, siteConfig.discoveredContent ]); // Only refresh on significant page visibility changes (not micro-focus events) useEffect(()=>{ let lastVisibilityChange = Date.now(); const handleVisibilityChange = ()=>{ // Only refresh if page was hidden for more than 30 seconds (significant absence) if (!document.hidden && Date.now() - lastVisibilityChange > 30000) { void refreshDashboard(); } if (document.hidden) { lastVisibilityChange = Date.now(); } }; document.addEventListener('visibilitychange', handleVisibilityChange); return ()=>{ document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, []); // Listen for localStorage events to immediately refresh when jobs are changed useEffect(()=>{ const handleStorageChange = (e)=>{ if (e.key === 'migration-event' && e.newValue) { try { const event = JSON.parse(e.newValue); if (event.type?.startsWith('migration-job-')) { void refreshDashboard(); } } catch (error) { // Ignore parsing errors } } }; window.addEventListener('storage', handleStorageChange); return ()=>window.removeEventListener('storage', handleStorageChange); }, []); const startMigrationJob = async (jobId)=>{ try { setLoading(true); // Validate that site configuration is saved and content is scanned if (!configState.isSaved || !validateSiteConfig() || siteConfig.connectionStatus !== 'scanned') { alert('Please save a valid WordPress site configuration and scan for content before starting migration jobs.'); return; } // Force refresh to clear any old server-side logs before starting await refreshDashboard(); // Clear logs when starting new migration clearLogs(); // Find job name for logging const job = summary.recentJobs?.find((j)=>j.id === jobId); const jobName = job?.jobName || `Job ${jobId}`; addLog(`Migration "${jobName}" started`); const response = await fetch(`/api/wordpress/migration-jobs`, { body: JSON.stringify({ action: 'start', jobId, siteConfig: { siteName: siteConfig.siteName, wpPassword: siteConfig.wpPassword, wpSiteUrl: siteConfig.wpSiteUrl, wpUsername: siteConfig.wpUsername } }), headers: { 'Content-Type': 'application/json' }, method: 'POST' }); if (!response.ok) { const errorData = await response.json(); addLog(`Error starting migration "${jobName}": ${errorData.error || 'Unknown error'}`); throw new Error(errorData.error || 'Failed to start job'); } // Refresh dashboard immediately after starting job await refreshDashboard(); } catch (error) { console.error('Error starting migration:', error); alert(`Failed to start migration: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally{ setLoading(false); } }; const pauseMigrationJob = async (jobId)=>{ try { // Find job name for logging const job = summary.recentJobs?.find((j)=>j.id === jobId); const jobName = job?.jobName || `Job ${jobId}`; addLog(`Migration "${jobName}" paused`); const response = await fetch(`/api/wordpress/migration-jobs?jobId=${jobId}&action=pause`, { method: 'PUT' }); if (!response.ok) { addLog(`Error pausing migration "${jobName}"`); throw new Error('Failed to pause migration job'); } // Refresh dashboard immediately after pausing job await refreshDashboard(); } catch (error) { console.error('Error pausing migration:', error); alert('Failed to pause migration job'); } }; const resumeMigrationJob = async (jobId)=>{ try { setLoading(true); // Validate that site configuration is saved and content is scanned if (!configState.isSaved || !validateSiteConfig() || siteConfig.connectionStatus !== 'scanned') { alert('Please save a valid WordPress site configuration and scan for content before resuming migration jobs.'); return; } // Find job name for logging const job = summary.recentJobs?.find((j)=>j.id === jobId); const jobName = job?.jobName || `Job ${jobId}`; addLog(`Migration "${jobName}" resumed`); const response = await fetch(`/api/wordpress/migration-jobs?jobId=${jobId}&action=resume`, { method: 'PUT' }); if (!response.ok) { const errorData = await response.json(); addLog(`Error resuming migration "${jobName}": ${errorData.error || 'Unknown error'}`); throw new Error(errorData.error || 'Failed to resume job'); } // Start the migration process again (it will continue from where it left off) const startResponse = await fetch(`/api/wordpress/migration-jobs`, { body: JSON.stringify({ action: 'resume', jobId, siteConfig: { siteName: siteConfig.siteName, wpPassword: siteConfig.wpPassword, wpSiteUrl: siteConfig.wpSiteUrl, wpUsername: siteConfig.wpUsername } }), headers: { 'Content-Type': 'application/json' }, method: 'POST' }); if (!startResponse.ok) { const errorData = await startResponse.json(); addLog(`Error resuming migration "${jobName}": ${errorData.error || 'Unknown error'}`); throw new Error(errorData.error || 'Failed to resume job'); } // Refresh dashboard immediately after resuming job await refreshDashboard(); } catch (error) { console.error('Error resuming migration:', error); alert(`Failed to resume migration: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally{ setLoading(false); } }; const retryMigrationJob = async (jobId)=>{ try { setLoading(true); // Validate that site configuration is saved and content is scanned if (!configState.isSaved || !validateSiteConfig() || siteConfig.connectionStatus !== 'scanned') { alert('Please save a valid WordPress site configuration and scan for content before retrying migration jobs.'); return; } // Find job name for logging const job = summary.recentJobs?.find((j)=>j.id === jobId); const jobName = job?.jobName || `Job ${jobId}`; addLog(`Retrying migration "${jobName}"`); const response = await fetch(`/api/wordpress/migration-jobs`, { body: JSON.stringify({ action: 'retry', jobId, siteConfig: { siteName: siteConfig.siteName, wpPassword: siteConfig.wpPassword, wpSiteUrl: siteConfig.wpSiteUrl, wpUsername: siteConfig.wpUsername } }), headers: { 'Content-Type': 'application/json' }, method: 'POST' }); if (!response.ok) { const errorData = await response.json(); addLog(`Error retrying migration "${jobName}": ${errorData.error || 'Unknown error'}`); throw new Error(errorData.error || 'Failed to retry job'); } // Refresh dashboard immediately after retrying job await refreshDashboard(); } catch (error) { console.error('Error retrying migration:', error); alert(`Failed to retry migration: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally{ setLoading(false); } }; const updateMigrationJob = async (jobId)=>{ try { setLoading(true); // Validate that site configuration is saved and content is scanned if (!configState.isSaved || !validateSiteConfig() || siteConfig.connectionStatus !== 'scanned') { alert('Please save a valid WordPress site configuration and scan for content before updating migration jobs.'); return; } // Find job name for logging const job = summary.recentJobs?.find((j)=>j.id === jobId); const jobName = job?.jobName || `Job ${jobId}`; // Confirm update action const confirmed = confirm(`Are you sure you want to update the "${jobName}" migration? This will update existing items with new field mappings without re-processing all content.`); if (!confirmed) { return; } addLog(`Updating migration "${jobName}" with new field mappings`); const response = await fetch(`/api/wordpress/migration-jobs`, { body: JSON.stringify({ action: 'update', jobId, siteConfig: { siteName: siteConfig.siteName, wpPassword: siteConfig.wpPassword, wpSiteUrl: siteConfig.wpSiteUrl, wpUsername: siteConfig.wpUsername } }), headers: { 'Content-Type': 'application/json' }, method: 'POST' }); if (!response.ok) { const errorData = await response.json(); addLog(`Error updating migration "${jobName}": ${errorData.error || 'Unknown error'}`); throw new Error(errorData.error || 'Failed to update job'); } const result = await response.json(); addLog(`Successfully started update for "${jobName}"`); // Refresh summary to show updated status await refreshDashboard(); } catch (error) { console.error('Error updating migration:', error); alert(`Failed to update migration job: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally{ setLoading(false); } }; const restartMigrationJob = async (jobId)=>{ try { setLoading(true); // Validate that site configuration is saved and content is scanned if (!configState.isSaved || !validateSiteConfig() || siteConfig.connectionStatus !== 'scanned') { alert('Please save a valid WordPress site configuration and scan for content before restarting migration jobs.'); return; } // Find job name for logging const job = summary.recentJobs?.find((j)=>j.id === jobId); const jobName = job?.jobName || `Job ${jobId}`; // Confirm restart action const confirmed = confirm(`Are you sure you want to restart the "${jobName}" migration? This will re-process ALL items, including those that were already successfully migrated.`); if (!confirmed) { return; } addLog(`Restarting migration "${jobName}"`); const response = await fetch(`/api/wordpress/migration-jobs`, { body: JSON.stringify({ action: 'restart', jobId, siteConfig: { siteName: siteConfig.siteName, wpPassword: siteConfig.wpPassword, wpSiteUrl: siteConfig.wpSiteUrl, wpUsername: siteConfig.wpUsername } }), headers: { 'Content-Type': 'application/json' }, method: 'POST' }); if (!response.ok) { const errorData = await response.json(); addLog(`Error restarting migration "${jobName}": ${errorData.error || 'Unknown error'}`); throw new Error(errorData.error || 'Failed to restart job'); } // Refresh dashboard immediately after restarting job await refreshDashboard(); } catch (error) { console.error('Error restarting migration:', error); alert(`Failed to restart migration: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally{ setLoading(false); } }; const scanWordPressContent = async ()=>{ if (!validateSiteConfig()) { return; } setSiteConfig((prev)=>({ ...prev, connectionStatus: 'scanning' })); try { const response = await fetch('/api/wordpress/discover-content', { body: JSON.stringify({ wpPassword: siteConfig.wpPassword, wpSiteUrl: siteConfig.wpSiteUrl, wpUsername: siteConfig.wpUsername }), headers: { 'Content-Type': 'application/json' }, method: 'POST' }); const result = await response.json(); if (response.ok && result.success) { const contentTypes = result.contentTypes || []; const totalItems = result.totalItems || 0; const updatedConfig = { ...siteConfig, connectionStatus: 'scanned', discoveredContent: contentTypes, totalItems }; setSiteConfig(updatedConfig); // Save the updated config with discovered content to localStorage try { localStorage.setItem('wp-site-config', JSON.stringify(updatedConfig)); } catch (error) { console.error('Failed to save discovered content to localStorage:', error); } // Show a message if no content was found if (contentTypes.length === 0) { alert('WordPress scan completed, but no content was found. This could be due to:\n\n' + '• Empty WordPress site (no posts, pages, media, etc.)\n' + '• Insufficient user permissions\n' + '• WordPress REST API restrictions\n\n' + 'Please check your WordPress site and user permissions.'); } } else { const updatedConfig = { ...siteConfig, connectionStatus: 'failed', discoveredContent: [], totalItems: 0 }; setSiteConfig(updatedConfig); // Save the updated config to localStorage try { localStorage.setItem('wp-site-config', JSON.stringify(updatedConfig)); } catch (error) { console.error('Failed to save failed scan state to localStorage:', error); } alert(`WordPress scan failed: ${result.error || 'Unknown error'}`); } } catch (error) { console.error('Content scan failed:', error); const updatedConfig = { ...siteConfig, connectionStatus: 'failed', discoveredContent: [], totalItems: 0 }; setSiteConfig(updatedConfig); // Save the updated config to localStorage try { localStorage.setItem('wp-site-config', JSON.stringify(updatedConfig)); } catch (storageError) { console.error('Failed to save network error state to localStorage:', storageError); } alert('WordPress scan failed: Network error'); } }; const handleSiteConfigChange = (field, value)=>{ // Only reset connection status and content for fields that affect the WordPress connection const connectionFields = [ 'wpSiteUrl', 'wpUsername', 'wpPassword' ]; const shouldReset = connectionFields.includes(field); setSiteConfig((prev)=>({ ...prev, [field]: value, ...shouldReset && { connectionStatus: 'not-scanned', discoveredContent: [], totalItems: 0 } })); }; const validateSiteConfig = ()=>{ const { siteName, wpPassword, wpSiteUrl, wpUsername } = siteConfig; if (!siteName.trim() || !wpSiteUrl.trim() || !wpUsername.trim() || !wpPassword.trim()) { alert('Please fill in all WordPress site configuration fields'); return false; } return true; }; const handleSaveConfig = async ()=>{ if (!validateSiteConfig()) { return; } try { localStorage.setItem('wp-site-config', JSON.stringify(siteConfig)); setConfigState({ isEditing: false, isSaved: true }); // Automatically scan for content after saving await scanWordPressContent(); } catch (error) { console.error('Failed to save configuration:', error); alert('Failed to save configuration. Please try again.'); } }; const handleEditConfig = ()=>{ setConfigState({ isEditing: true, isSaved: true }); }; const handleCancelEdit = ()=>{ // Reload saved config from localStorage try { const savedConfig = localStorage.getItem('wp-site-config'); if (savedConfig) { const parsed = JSON.parse(savedConfig); setSiteConfig((prev)=>({ ...prev, ...parsed, // Preserve discovered content if it exists connectionStatus: parsed.connectionStatus || prev.connectionStatus || 'not-scanned', discoveredContent: parsed.discoveredContent || prev.discoveredContent || [], totalItems: parsed.totalItems || prev.totalItems || 0 })); } setConfigState({ isEditing: false, isSaved: true }); } catch (error) { console.error('Failed to reload saved configuration:', error); } }; // Combine client-side action logs with server-side migration logs const getAllLogs = ()=>{ const combinedLogs = []; // Add client-side action logs (with current timestamp) logs.forEach((log)=>{ // Extract timestamp from log if it exists, otherwise use current time const timestampMatch = log.match(/^\[(\d{1,2}:\d{2}:\d{2}(?:\s?[AP]M)?)\]/); let timestamp = new Date(); if (timestampMatch) { // Use today's date with the extracted time const timeStr = timestampMatch[1]; const today = new Date(); const timeWithDate = `${today.toDateString()} ${timeStr}`; timestamp = new Date(timeWithDate); } combinedLogs.push({ message: log, source: 'client', timestamp }); }); // Add server-side migration logs if (summary.recentLogs) { summary.recentLogs.forEach((log)=>{ const timestamp = new Date(log.timestamp); const levelIcon = log.level === 'error' ? '❌' : log.level === 'warning' ? '⚠️' : 'ℹ️'; const formattedMessage = `[${timestamp.toLocaleTimeString()}] ${levelIcon} [${log.jobName}] ${log.message}`; combinedLogs.push({ level: log.level, message: formattedMessage, source: 'server', timestamp }); }); } // Sort by timestamp (NEWEST FIRST - latest at top) return combinedLogs.sort((a, b)=>b.timestamp.getTime() - a.timestamp.getTime()).slice(0, 100) // Limit to 100 most recent logs .map((log)=>log.message); }; return /*#__PURE__*/ _jsxs("div", { className: "gutter--left gutter--right collection-list__wrap", children: [ /*#__PURE__*/ _jsx("div", { className: styles.migrationDashboardHeader, children: /*#__PURE__*/ _jsx("h1", { children: "WordPress Migration Dashboard" }) }), /*#__PURE__*/ _jsxs("div", { className: styles.siteConfigSection, children: [ /*#__PURE__*/ _jsxs("div", { className: styles.siteConfigHeader, children: [ /*#__PURE__*/ _jsx("h2", { children: "WordPress Site Configuration" }), /*#__PURE__*/ _jsx("button", { className: styles.toggleButton, onClick: ()=>setShowSiteConfig(!showSiteConfig), children: showSiteConfig ? 'Hide Configuration' : 'Show Configuration' }) ] }), showSiteConfig && /*#__PURE__*/ _jsxs("div", { className: styles.siteConfigForm, children: [ /*#__PURE__*/ _jsxs("div", { className: styles.formGrid, children: [ /*#__PURE__*/ _jsxs("div", { className: styles.formGroup, children: [ /*#__PURE__*/ _jsx("label", { htmlFor: "siteName", children: "Site Name" }), /*#__PURE__*/ _jsx("input", { disabled: !configState.isEditing, id: "siteName", onChange: (e)=>handleSiteConfigChange('siteName', e.target.value), placeholder: "My WordPress Site", type: "text", value: siteConfig.siteName }) ] }), /*#__PURE__*/ _jsxs("div", { className: styles.formGroup, children: [ /*#__PURE__*/ _jsx("label", { htmlFor: "wpSiteUrl", children: "WordPress Site URL" }), /*#__PURE__*/ _jsx("input", { disabled: !configState.isEditing, id: "wpSiteUrl", onChange: (e)=>handleSiteConfigChange('wpSiteUrl', e.target.value), placeholder: "https://example.com", type: "url", value: siteConfig.wpSiteUrl }), /*#__PURE__*/ _jsx("small", { children: "Base URL of your WordPress site" }) ] }), /*#__PURE__*/ _jsxs("div", { className: styles.formGroup, children: [ /*#__PURE__*/ _jsx("label", { htmlFor: "wpUsername", children: "WordPress Username" }), /*#__PURE__*/ _jsx("input", { disabled: !configState.isEditing, id: "wpUsername", onChange: (e)=>handleSiteConfigChange('wpUsername', e.target.value), placeholder: "admin", type: "text", value: siteConfig.wpUsername }) ] }), /*#__PURE__*/ _jsxs("div", { className: styles.formGroup, children: [ /*#__PURE__*/ _jsx("label", { htmlFor: "wpPassword", children: "Application Password" }), /*#__PURE__*/ _jsx("input", { disabled: !configState.isEditing, id: "wpPassword", onChange: (e)=>handleSiteConfigChange('wpPassword', e.target.value), placeholder: "xxxx xxxx xxxx xxxx", type: "password", value: siteConfig.wpPassword }), /*#__PURE__*/ _jsx("small", { children: "WordPress Application Password for REST API access" }) ] }) ] }), /*#__PURE__*/ _jsxs("div", { className: styles.configActions, children: [ /*#__PURE__*/ _jsx("div", { className: styles.connectionStatus, children: /*#__PURE__*/ _jsxs("div", { className: styles.statusIndicator, children: [ /*#__PURE__*/ _jsx("span", { className: `${styles.statusDot} ${styles[siteConfig.connectionStatus]}` }), /*#__PURE__*/ _jsxs("span", { className: styles.statusText, children: [ siteConfig.connectionStatus === 'not-scanned' && 'Not Scanned', siteConfig.connectionStatus === 'scanned' && siteConfig.discoveredContent && siteConfig.discoveredContent.length > 0 && /*#__PURE__*/ _jsxs("div", { className: styles.discoveredContent, children: [ /*#__PURE__*/ _jsxs("div", { className: styles.discoveredHeader, children: [ /*#__PURE__*/ _jsxs("h4", { children: [ "Available Content (", siteConfig.totalItems, " total items)" ] }), /*#__PURE__*/ _jsx("small", { children: "Ready for migration" }) ] }), /*#__PURE__*/ _jsx("div", { className: styles.contentList, children: siteConfig.discoveredContent.map((content)=>/*#__PURE__*/ _jsxs("div", { className: styles.contentItem, children: [ /*#__PURE__*/ _jsxs("span", { className: styles.contentLabel, children: [ content.label, ":" ] }), /*#__PURE__*/ _jsxs("span", { className: styles.contentCount, children: [ content.count, " items" ] }), content.custom && /*#__PURE__*/ _jsx("span", { className: styles.customBadge, children: "Custom" }) ] }, content.type)) }) ] }), siteConfig.connectionStatus === 'scanned' && 'Content Discovered', siteConfig.connectionStatus === 'failed' && 'Scan Failed' ] }) ] }) }), /*#__PURE__*/ _jsx("div", { className: styles.formActions, children: configState.isEditing ? /*#__PURE__*/ _jsxs(_Fragment, { children: [ /*#__PURE__*/ _jsx("button", { className: styles.saveButton, disabled: loading, onClick: handleSaveConfig, children: "Save Configuration" }), configState.isSaved && /*#__PURE__*/ _jsx("button", { className: styles.cancelButton, disabled: loading, onClick: handleCancelEdit, children: "Cancel" }) ] }) : /*#__PURE__*/ _jsx("button", { className: styles.editButton, disabled: loading, onClick: handleEditConfig, children: "Edit Configuration" }) }) ] }) ] }) ] }), /*#__PURE__*/ _jsxs("div", { className: styles.statsGrid, children: [ /*#__PURE__*/ _jsxs("div", { className: styles.statCard, children: [ /*#__PURE__*/ _jsx("h3", { children: "Site Configured" }), /*#__PURE__*/ _jsx("p", { className: styles.statValue, children: siteConfig.connectionStatus !== 'scanned' && '✗ No' }), siteConfig.connectionStatus === 'scanned' && siteConfig.discoveredContent && /*#__PURE__*/ _jsxs("small", { className: styles.statDetail, children: [ siteConfig.discoveredContent.length, " content types", /*#__PURE__*/ _jsx("br", {}), siteConfig.totalItems, " items" ] }) ] }), /*#__PURE__*/ _jsxs("div", { className: styles.statCard, children: [ /*#__PURE__*/ _jsx("h3", { children: "Total Jobs" }), /*#__PURE__*/ _jsx("p", { className: styles.statValue, children: summary.totalJobs || 0 }) ] }), /*#__PURE__*/ _jsxs("div", { className: styles.statCard, children: [ /*#__PURE__*/ _jsx("h3", { children: "Active Jobs" }), /*#__PURE__*/ _jsx("p", { className: `${styles.statValue} ${(summary.activeJobs || 0) > 0 ? styles.active : ''}`, children: summary.activeJobs || 0 }) ] }), /*#__PURE__*/ _jsxs("div", { className: styles.statCard, children: [ /*#__PURE__*/ _jsx("h3", { children: "Items Migrated" }), /*#__PURE__*/ _jsx("p", { className: styles.statValue, children: (summary.totalItemsMigrated || 0).toLocaleString() }) ] }) ] }), /*#__PURE__*/ _jsxs("div", { className: styles.recentJobs, children: [ /*#__PURE__*/ _jsx("h3", { children: "Recent Migration Jobs" }), /*#__PURE__*/ _jsx("div", { className: styles.jobsTable, children: /*#__PURE__*/ _jsxs("table", { children: [ /*#__PURE__*/ _jsx("thead", { children: /*#__PURE__*/ _jsxs("tr", { children: [ /*#__PURE__*/ _jsx("th", { children: "Job Name" }), /*#__PURE__*/ _jsx("th", { children: "Status" }), /*#__PURE__*/ _jsx("th", { children: "Items" }), /*#__PURE__*/ _jsx("th", { children: "Success Rate" }), /*#__PURE__*/ _jsx("th", { children: "Actions" }) ] }) }), /*#__PURE__*/ _jsx("tbody", { children: summary.recentJobs?.map((job)=>{ const successRate = job.progress.processedItems > 0 ? job.progress.successfulItems / job.progress.processedItems * 100 : 0; return /*#__PURE__*/ _jsxs("tr", { children: [ /*#__PURE__*/ _jsx("td", { children: job.jobName }), /*#__PURE__*/ _jsx("td", { children: /*#__PURE__*/ _jsx("span", { className: `${styles.statusBadge} ${styles[job.status]}`, children: job.status }) }), /*#__PURE__*/ _jsxs("td", { children: [ job.progress.processedItems, "/", job.progress.totalItems ] }), /*#__PURE__*/ _jsxs("td", { children: [ successRate.toFixed(1), "%" ] }), /*#__PURE__*/ _jsx("td", { children: /*#__PURE__*/ _jsxs("div", { className: styles.jobActions, children: [ job.status === 'ready' && /*#__PURE__*/ _jsx("button", { className: styles.actionButton, disabled: loading, onClick: ()=>startMigrationJob(job.id), children: "Start" }), job.status === 'running' && /*#__PURE__*/ _jsx("button", { className: styles.actionButton, onClick: ()=>pauseMigrationJob(job.id), children: "Pause" }), job.status === 'paused' && /*#__PURE__*/ _jsxs(_Fragment, { children: [ /*#__PURE__*/ _jsx("button", { className: styles.actionButton, disabled: loading, onClick: ()=>resumeMigrationJob(job.id), children: "Resume" }), /*#__PURE__*/ _jsx("button", { className: `${styles.actionButton} ${styles.restartButton}`, disabled: loading, onClick: ()=>restartMigrationJob(job.id), title: "Restart entire migration from beginning", children: "Restart" }) ] }), job.status === 'failed' && /*#__PURE__*/ _jsx("button", { className: `${styles.actionButton} ${styles.retryButton}`, disabled: loading, onClick: ()=>retryMigrationJob(job.id), children: "Retry" }),