diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 7494b62fc3..035594c5df 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -186,13 +186,15 @@ export enum IpcChannel { // data migration DataMigrate_CheckNeeded = 'data-migrate:check-needed', - DataMigrate_StartMigration = 'data-migrate:start-migration', DataMigrate_GetProgress = 'data-migrate:get-progress', DataMigrate_Cancel = 'data-migrate:cancel', DataMigrate_RequireBackup = 'data-migrate:require-backup', DataMigrate_BackupCompleted = 'data-migrate:backup-completed', DataMigrate_ShowBackupDialog = 'data-migrate:show-backup-dialog', DataMigrate_StartFlow = 'data-migrate:start-flow', + DataMigrate_ProceedToBackup = 'data-migrate:proceed-to-backup', + DataMigrate_StartMigration = 'data-migrate:start-migration', + DataMigrate_RetryMigration = 'data-migrate:retry-migration', DataMigrate_RestartApp = 'data-migrate:restart-app', DataMigrate_CloseWindow = 'data-migrate:close-window', diff --git a/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts b/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts index 0f489b7742..1f4d4f6d46 100644 --- a/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts +++ b/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts @@ -22,11 +22,21 @@ interface DataRefactorMigrationStatus { version?: string } +type MigrationStage = + | 'introduction' // Introduction phase - user can cancel + | 'backup_required' // Backup required - show backup requirement + | 'backup_progress' // Backup in progress - user is backing up + | 'backup_confirmed' // Backup confirmed - ready to migrate + | 'migration' // Migration in progress - cannot cancel + | 'completed' // Completed - restart app + | 'error' // Error - recovery options + interface MigrationProgress { - stage: string + stage: MigrationStage progress: number total: number message: string + error?: string } interface MigrationResult { @@ -39,13 +49,12 @@ class DataRefactorMigrateService { private static instance: DataRefactorMigrateService | null = null private migrateWindow: BrowserWindow | null = null private backupManager: BackupManager - private backupCompletionResolver: ((value: boolean) => void) | null = null private db = dbService.getDb() private currentProgress: MigrationProgress = { - stage: 'idle', + stage: 'introduction', progress: 0, total: 100, - message: 'Ready to migrate' + message: 'Ready to start data migration' } private isMigrating: boolean = false @@ -77,11 +86,32 @@ class DataRefactorMigrateService { } }) + ipcMain.handle(IpcChannel.DataMigrate_ProceedToBackup, async () => { + try { + await this.proceedToBackup() + return true + } catch (error) { + logger.error('IPC handler error: proceedToBackup', error as Error) + throw error + } + }) + ipcMain.handle(IpcChannel.DataMigrate_StartMigration, async () => { try { - return await this.runMigration() + await this.startMigrationProcess() + return true } catch (error) { - logger.error('IPC handler error: runMigration', error as Error) + logger.error('IPC handler error: startMigrationProcess', error as Error) + throw error + } + }) + + ipcMain.handle(IpcChannel.DataMigrate_RetryMigration, async () => { + try { + await this.retryMigration() + return true + } catch (error) { + logger.error('IPC handler error: retryMigration', error as Error) throw error } }) @@ -104,9 +134,9 @@ class DataRefactorMigrateService { } }) - ipcMain.handle(IpcChannel.DataMigrate_BackupCompleted, () => { + ipcMain.handle(IpcChannel.DataMigrate_BackupCompleted, async () => { try { - this.notifyBackupCompleted() + await this.notifyBackupCompleted() return true } catch (error) { logger.error('IPC handler error: notifyBackupCompleted', error as Error) @@ -114,14 +144,55 @@ class DataRefactorMigrateService { } }) - ipcMain.handle(IpcChannel.DataMigrate_ShowBackupDialog, () => { + ipcMain.handle(IpcChannel.DataMigrate_ShowBackupDialog, async () => { try { - // Show the backup dialog/interface - // This could integrate with existing backup UI or create a new backup interface - logger.info('Backup dialog request received') - return true + logger.info('Opening backup dialog for migration') + + // Update progress to indicate backup dialog is opening + await this.updateProgress('backup_progress', 10, 'Opening backup dialog...') + + // Instead of performing backup automatically, let's open the file dialog + // and let the user choose where to save the backup + const { dialog } = await import('electron') + const result = await dialog.showSaveDialog({ + title: 'Save Migration Backup', + defaultPath: `cherry-studio-migration-backup-${new Date().toISOString().split('T')[0]}.zip`, + filters: [ + { name: 'Backup Files', extensions: ['zip'] }, + { name: 'All Files', extensions: ['*'] } + ] + }) + + if (!result.canceled && result.filePath) { + logger.info('User selected backup location', { filePath: result.filePath }) + await this.updateProgress('backup_progress', 50, 'Creating backup file...') + + // Perform the actual backup to the selected location + const backupResult = await this.performBackupToFile(result.filePath) + + if (backupResult.success) { + await this.updateProgress('backup_progress', 100, 'Backup created successfully!') + // Wait a moment to show the success message, then transition to confirmed state + setTimeout(async () => { + await this.updateProgress( + 'backup_confirmed', + 100, + 'Backup completed! Ready to start migration. Click "Start Migration" to continue.' + ) + }, 1000) + } else { + await this.updateProgress('backup_required', 0, `Backup failed: ${backupResult.error}`) + } + + return backupResult + } else { + logger.info('User cancelled backup dialog') + await this.updateProgress('backup_required', 0, 'Backup cancelled. Please create a backup to continue.') + return { success: false, error: 'Backup cancelled by user' } + } } catch (error) { logger.error('IPC handler error: showBackupDialog', error as Error) + await this.updateProgress('backup_required', 0, 'Backup process failed') throw error } }) @@ -135,9 +206,9 @@ class DataRefactorMigrateService { } }) - ipcMain.handle(IpcChannel.DataMigrate_RestartApp, () => { + ipcMain.handle(IpcChannel.DataMigrate_RestartApp, async () => { try { - this.restartApplication() + await this.restartApplication() return true } catch (error) { logger.error('IPC handler error: restartApplication', error as Error) @@ -167,12 +238,14 @@ class DataRefactorMigrateService { try { ipcMain.removeAllListeners(IpcChannel.DataMigrate_CheckNeeded) - ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartMigration) ipcMain.removeAllListeners(IpcChannel.DataMigrate_GetProgress) ipcMain.removeAllListeners(IpcChannel.DataMigrate_Cancel) ipcMain.removeAllListeners(IpcChannel.DataMigrate_BackupCompleted) ipcMain.removeAllListeners(IpcChannel.DataMigrate_ShowBackupDialog) ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartFlow) + ipcMain.removeAllListeners(IpcChannel.DataMigrate_ProceedToBackup) + ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartMigration) + ipcMain.removeAllListeners(IpcChannel.DataMigrate_RetryMigration) ipcMain.removeAllListeners(IpcChannel.DataMigrate_RestartApp) ipcMain.removeAllListeners(IpcChannel.DataMigrate_CloseWindow) @@ -200,6 +273,7 @@ class DataRefactorMigrateService { return true } + logger.info('Data Refactor Migration is needed') return false } catch (error) { logger.error('Failed to check migration status', error as Error) @@ -212,18 +286,40 @@ class DataRefactorMigrateService { */ private async isMigrationCompleted(): Promise { try { + logger.debug('Checking migration completion status in database') + + // First check if the database is available + if (!this.db) { + logger.warn('Database not initialized, assuming migration not completed') + return false + } + const result = await this.db .select() .from(appStateTable) .where(eq(appStateTable.key, DATA_REFACTOR_MIGRATION_STATUS)) .limit(1) - if (result.length === 0) return false + logger.debug('Migration status query result', { resultCount: result.length }) + + if (result.length === 0) { + logger.info('No migration status record found, migration needed') + return false + } const status = result[0].value as DataRefactorMigrationStatus - return status.completed === true + const isCompleted = status.completed === true + + logger.info('Migration status found', { + completed: isCompleted, + completedAt: status.completedAt, + version: status.version + }) + + return isCompleted } catch (error) { - logger.warn('Failed to check migration state', error as Error) + logger.error('Failed to check migration state - treating as not completed', error as Error) + // In case of database errors, assume migration is needed to be safe return false } } @@ -320,7 +416,7 @@ class DataRefactorMigrateService { } /** - * Run the complete migration process + * Show migration window and initialize introduction stage */ async runMigration(): Promise { if (this.isMigrating) { @@ -332,6 +428,9 @@ class DataRefactorMigrateService { this.isMigrating = true logger.info('Showing migration window') + // Initialize introduction stage + await this.updateProgress('introduction', 0, 'Welcome to Cherry Studio data migration') + // Create migration window const window = this.createMigrateWindow() @@ -345,12 +444,45 @@ class DataRefactorMigrateService { }) } + /** + * Start migration flow - simply ensure we're in introduction stage + * This is called when user first opens the migration window + */ async startMigrationFlow(): Promise { if (!this.isMigrating) { logger.warn('Migration not started, cannot execute flow.') return } - logger.info('Starting migration flow from user action') + + logger.info('Confirming introduction stage for migration flow') + await this.updateProgress('introduction', 0, 'Ready to begin migration process. Please read the information below.') + } + + /** + * Proceed from introduction to backup requirement stage + * This is called when user clicks "Next" in introduction + */ + async proceedToBackup(): Promise { + if (!this.isMigrating) { + logger.warn('Migration not started, cannot proceed to backup.') + return + } + + logger.info('Proceeding from introduction to backup stage') + await this.updateProgress('backup_required', 0, 'Data backup is required before migration can proceed') + } + + /** + * Start the actual migration process + * This is called when user confirms backup and clicks "Start Migration" + */ + async startMigrationProcess(): Promise { + if (!this.isMigrating) { + logger.warn('Migration not started, cannot start migration process.') + return + } + + logger.info('Starting actual migration process') try { await this.executeMigrationFlow() } catch (error) { @@ -360,21 +492,12 @@ class DataRefactorMigrateService { } /** - * Execute the complete migration flow + * Execute the actual migration process + * Called after user has confirmed backup completion */ private async executeMigrationFlow(): Promise { try { - // Step 1: Enforce backup - await this.updateProgress('backup', 0, 'Starting backup process...') - const backupSuccess = await this.enforceBackup() - - if (!backupSuccess) { - throw new Error('Backup process failed or was cancelled by user') - } - - await this.updateProgress('backup', 100, 'Backup completed successfully') - - // Step 2: Execute migration + // Start migration await this.updateProgress('migration', 0, 'Starting data migration...') const migrationResult = await this.executeMigration() @@ -388,16 +511,18 @@ class DataRefactorMigrateService { `Migration completed: ${migrationResult.migratedCount} items migrated` ) - // Step 3: Mark as completed + // Mark as completed await this.markMigrationCompleted() - await this.updateProgress('completed', 100, 'Migration completed! Please restart the app.') + await this.updateProgress('completed', 100, 'Migration completed successfully! Click restart to continue.') } catch (error) { logger.error('Migration flow failed', error as Error) + const errorMessage = error instanceof Error ? error.message : String(error) await this.updateProgress( 'error', 0, - `Migration failed: ${error instanceof Error ? error.message : String(error)}. Please close this window and restart the app to try again.` + 'Migration failed. You can close this window and try again, or continue using the previous version.', + errorMessage ) throw error @@ -405,58 +530,113 @@ class DataRefactorMigrateService { } /** - * Enforce backup before migration + * Perform backup to a specific file location */ - private async enforceBackup(): Promise { + private async performBackupToFile(filePath: string): Promise<{ success: boolean; error?: string }> { try { - logger.info('Enforcing backup before migration') + logger.info('Performing backup to file', { filePath }) - await this.updateProgress('backup', 0, 'Backup is required before migration') + // Get backup data from the current application state + const backupData = await this.getBackupData() - // Send backup requirement to renderer - if (this.migrateWindow && !this.migrateWindow.isDestroyed()) { - this.migrateWindow.webContents.send(IpcChannel.DataMigrate_RequireBackup) - } + // Extract directory and filename from the full path + const path = await import('path') + const destinationDir = path.dirname(filePath) + const fileName = path.basename(filePath) - // Wait for user to complete backup - const backupResult = await this.waitForBackupCompletion() + // Use the existing backup manager to create a backup + const backupPath = await this.backupManager.backup( + null as any, // IpcMainInvokeEvent - we're calling directly so pass null + fileName, + backupData, + destinationDir, + false // Don't skip backup files - full backup for migration safety + ) - if (backupResult) { - await this.updateProgress('backup', 100, 'Backup completed successfully') - return true + if (backupPath) { + logger.info('Backup created successfully', { path: backupPath }) + return { success: true } } else { - await this.updateProgress('backup', 0, 'Backup is required to proceed with migration') - return false + return { + success: false, + error: 'Backup process did not return a file path' + } } } catch (error) { - logger.error('Backup enforcement failed', error as Error) - await this.updateProgress('backup', 0, 'Backup process failed') - return false + const errorMessage = error instanceof Error ? error.message : String(error) + logger.error('Backup failed during migration:', error as Error) + return { + success: false, + error: errorMessage + } } } /** - * Wait for user to complete backup + * Get backup data from the current application + * This creates a minimal backup with essential system information */ - private async waitForBackupCompletion(): Promise { - return new Promise((resolve) => { - // Store resolver for later use - this.backupCompletionResolver = resolve + private async getBackupData(): Promise { + try { + const fs = await import('fs-extra') + const path = await import('path') - // The actual completion will be triggered by notifyBackupCompleted() method - }) + // Gather basic system information + const data = { + backup: { + timestamp: new Date().toISOString(), + version: electronApp.getVersion(), + type: 'pre-migration-backup', + note: 'This is a safety backup created before data migration' + }, + system: { + platform: process.platform, + arch: process.arch, + nodeVersion: process.version + }, + // Include basic configuration files if they exist + configs: {} as Record + } + + // Try to read some basic configuration files (non-critical if they fail) + try { + const { getDataPath } = await import('@main/utils') + const dataPath = getDataPath() + + // Check if there are any config files we should backup + const configFiles = ['config.json', 'settings.json', 'preferences.json'] + for (const configFile of configFiles) { + const configPath = path.join(dataPath, configFile) + if (await fs.pathExists(configPath)) { + try { + const configContent = await fs.readJson(configPath) + data.configs[configFile] = configContent + } catch (err) { + logger.warn(`Could not read config file ${configFile}`, err as Error) + } + } + } + } catch (err) { + logger.warn('Could not access data directory for config backup', err as Error) + } + + return JSON.stringify(data, null, 2) + } catch (error) { + logger.error('Failed to get backup data:', error as Error) + throw error + } } /** * Notify that backup has been completed (called from IPC handler) */ - public notifyBackupCompleted(): void { - if (this.backupCompletionResolver) { - logger.info('Backup completed by user') - - this.backupCompletionResolver(true) - this.backupCompletionResolver = null - } + public async notifyBackupCompleted(): Promise { + logger.info('Backup completed by user') + await this.updateProgress( + 'backup_confirmed', + 100, + 'Backup completed! Ready to start migration. Click "Start Migration" to continue.' + ) } /** @@ -494,12 +674,18 @@ class DataRefactorMigrateService { /** * Update migration progress and broadcast to window */ - private async updateProgress(stage: string, progress: number, message: string): Promise { + private async updateProgress( + stage: MigrationStage, + progress: number, + message: string, + error?: string + ): Promise { this.currentProgress = { stage, progress, total: 100, - message + message, + error } if (this.migrateWindow && !this.migrateWindow.isDestroyed()) { @@ -518,18 +704,36 @@ class DataRefactorMigrateService { /** * Cancel migration process + * Only allowed during introduction and backup phases */ async cancelMigration(): Promise { if (!this.isMigrating) { return } + const currentStage = this.currentProgress.stage + if (currentStage === 'migration') { + logger.warn('Cannot cancel migration during migration process') + return + } + logger.info('Cancelling migration process') this.isMigrating = false - await this.updateProgress('cancelled', 0, 'Migration cancelled by user') this.closeMigrateWindow() } + /** + * Retry migration after error + */ + async retryMigration(): Promise { + logger.info('Retrying migration process') + await this.updateProgress( + 'introduction', + 0, + 'Ready to restart migration process. Please read the information below.' + ) + } + /** * Close migration window */ @@ -547,22 +751,73 @@ class DataRefactorMigrateService { /** * Restart the application after successful migration */ - private restartApplication(): void { + private async restartApplication(): Promise { try { - logger.info('Restarting application after migration completion') + logger.info('Preparing to restart application after migration completion') - // Clean up migration window and handlers before restart - this.closeMigrateWindow() + // Ensure migration status is properly saved before restart + await this.verifyMigrationStatus() - // Restart the app using Electron's relaunch mechanism - app.relaunch() - app.exit(0) + // Give some time for database operations to complete + await new Promise((resolve) => setTimeout(resolve, 500)) + + logger.info('Restarting application now') + + // In development mode, relaunch might not work properly + if (process.env.NODE_ENV === 'development' || !app.isPackaged) { + logger.warn('Development mode detected - showing restart instruction instead of auto-restart') + + const { dialog } = await import('electron') + await dialog.showMessageBox({ + type: 'info', + title: 'Migration Complete - Restart Required', + message: + 'Data migration completed successfully!\n\nSince you are in development mode, please manually restart the application to continue.', + buttons: ['Close App'], + defaultId: 0 + }) + + // Clean up migration window and handlers after showing dialog + this.closeMigrateWindow() + app.quit() + } else { + // Production mode - clean up first, then relaunch + this.closeMigrateWindow() + app.relaunch() + app.exit(0) + } } catch (error) { logger.error('Failed to restart application', error as Error) // Fallback: just close migration window and let user manually restart this.closeMigrateWindow() } } + + /** + * Verify that migration status is properly saved + */ + private async verifyMigrationStatus(): Promise { + try { + const isCompleted = await this.isMigrationCompleted() + if (isCompleted) { + logger.info('Migration status verified as completed') + } else { + logger.warn('Migration status not found as completed, attempting to mark again') + await this.markMigrationCompleted() + + // Double-check + const recheck = await this.isMigrationCompleted() + if (recheck) { + logger.info('Migration status successfully marked as completed on retry') + } else { + logger.error('Failed to mark migration as completed even on retry') + } + } + } catch (error) { + logger.error('Failed to verify migration status', error as Error) + // Don't throw - still allow restart + } + } } // Export singleton instance diff --git a/src/main/index.ts b/src/main/index.ts index 27e0d24ae6..52bc54e78e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -113,20 +113,43 @@ if (!app.requestSingleInstanceLock()) { // Data Refactor Migration // Check if data migration is needed BEFORE creating any windows try { + logger.info('Checking if data refactor migration is needed') const isMigrated = await dataRefactorMigrateService.isMigrated() + logger.info('Migration status check result', { isMigrated }) + if (!isMigrated) { logger.info('Data Refactor Migration needed, starting migration process') - await dataRefactorMigrateService.runMigration() - logger.info('Migration completed, app will restart automatically') - // Migration service will handle app restart, no need to continue startup - return + + try { + await dataRefactorMigrateService.runMigration() + logger.info('Migration window created successfully') + // Migration service will handle the migration flow, no need to continue startup + return + } catch (migrationError) { + logger.error('Failed to start migration process', migrationError as Error) + + // Migration is required for this version - show error and exit + await dialog.showErrorBox( + 'Migration Required - Application Cannot Start', + `This version of Cherry Studio requires data migration to function properly.\n\nMigration window failed to start: ${(migrationError as Error).message}\n\nThe application will now exit. Please try starting again or contact support if the problem persists.` + ) + + logger.error('Exiting application due to failed migration startup') + app.quit() + return + } } } catch (error) { - logger.error('Migration process failed', error as Error) - dialog.showErrorBox( - 'Fatal Error: Data Refactor Migration Failed', - `The application could not start due to a critical error during data migration.\n\nPlease contact support or try restoring data from a backup.\n\nError details:\n${(error as Error).message}` + logger.error('Migration status check failed', error as Error) + + // If we can't check migration status, this could indicate a serious database issue + // Since migration may be required, it's safer to exit and let user investigate + await dialog.showErrorBox( + 'Migration Status Check Failed - Application Cannot Start', + `Could not determine if data migration is completed.\n\nThis may indicate a database connectivity issue: ${(error as Error).message}\n\nThe application will now exit. Please check your installation and try again.` ) + + logger.error('Exiting application due to migration status check failure') app.quit() return } @@ -140,7 +163,7 @@ if (!app.requestSingleInstanceLock()) { app.dock?.hide() } - // Only create main window if no migration was needed or migration failed + // Create main window - migration has either completed or was not needed const mainWindow = windowService.createMainWindow() new TrayService() diff --git a/src/renderer/src/windows/dataRefactorMigrate/MigrateApp.tsx b/src/renderer/src/windows/dataRefactorMigrate/MigrateApp.tsx index 27798f7761..fdad3fb9b4 100644 --- a/src/renderer/src/windows/dataRefactorMigrate/MigrateApp.tsx +++ b/src/renderer/src/windows/dataRefactorMigrate/MigrateApp.tsx @@ -6,21 +6,30 @@ import styled from 'styled-components' const { Title, Text } = Typography +type MigrationStage = + | 'introduction' // Introduction phase - user can cancel + | 'backup_required' // Backup required - show backup requirement + | 'backup_progress' // Backup in progress - user is backing up + | 'backup_confirmed' // Backup confirmed - ready to migrate + | 'migration' // Migration in progress - cannot cancel + | 'completed' // Completed - restart app + | 'error' // Error - recovery options + interface MigrationProgress { - stage: string + stage: MigrationStage progress: number total: number message: string + error?: string } const MigrateApp: React.FC = () => { const [progress, setProgress] = useState({ - stage: 'idle', + stage: 'introduction', progress: 0, total: 100, - message: '准备开始迁移...' + message: 'Ready to start data migration' }) - const [showBackupRequired, setShowBackupRequired] = useState(false) useEffect(() => { // Listen for progress updates @@ -28,13 +37,7 @@ const MigrateApp: React.FC = () => { setProgress(progressData) } - // Listen for backup requirement - const handleBackupRequired = () => { - setShowBackupRequired(true) - } - window.electron.ipcRenderer.on(IpcChannel.DataMigrateProgress, handleProgress) - window.electron.ipcRenderer.on(IpcChannel.DataMigrate_RequireBackup, handleBackupRequired) // Request initial progress window.electron.ipcRenderer @@ -47,37 +50,41 @@ const MigrateApp: React.FC = () => { return () => { window.electron.ipcRenderer.removeAllListeners(IpcChannel.DataMigrateProgress) - window.electron.ipcRenderer.removeAllListeners(IpcChannel.DataMigrate_RequireBackup) } }, []) const currentStep = useMemo(() => { switch (progress.stage) { - case 'idle': + case 'introduction': return 0 - case 'backup': + case 'backup_required': + case 'backup_progress': + case 'backup_confirmed': return 1 case 'migration': return 2 case 'completed': - return 4 - case 'error': - case 'cancelled': return 3 + case 'error': + return -1 // Error state - will be handled separately default: return 0 } }, [progress.stage]) const stepStatus = useMemo(() => { - if (progress.stage === 'error' || progress.stage === 'cancelled') { + if (progress.stage === 'error') { return 'error' } return 'process' }, [progress.stage]) + const handleProceedToBackup = () => { + window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_ProceedToBackup) + } + const handleStartMigration = () => { - window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_StartFlow) + window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_StartMigration) } const handleRestartApp = () => { @@ -98,18 +105,22 @@ const MigrateApp: React.FC = () => { } const handleBackupCompleted = () => { - setShowBackupRequired(false) // Notify the main process that backup is completed window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_BackupCompleted) } + const handleRetryMigration = () => { + window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_RetryMigration) + } + const getProgressColor = () => { switch (progress.stage) { case 'completed': return '#52c41a' case 'error': - case 'cancelled': return '#ff4d4f' + case 'backup_confirmed': + return '#52c41a' default: return '#1890ff' } @@ -117,12 +128,36 @@ const MigrateApp: React.FC = () => { const renderActionButtons = () => { switch (progress.stage) { - case 'idle': + case 'introduction': return ( - + + + + ) + case 'backup_required': + return ( + + + + + + ) + case 'backup_confirmed': + return ( + + + + + ) + case 'migration': + return case 'completed': return ( ) case 'error': - case 'cancelled': return ( - + + ) - case 'backup': - case 'migration': - return ( - - ) default: return null } @@ -167,11 +197,11 @@ const MigrateApp: React.FC = () => { - {progress.stage !== 'idle' && ( + {progress.stage !== 'introduction' && progress.stage !== 'error' && ( { {progress.message} + {progress.stage === 'introduction' && ( + + )} + + {progress.stage === 'backup_required' && ( + + )} + + {progress.stage === 'backup_confirmed' && ( + + )} + {progress.stage === 'error' && ( { {progress.stage === 'completed' && ( )} - {showBackupRequired && ( - - - - - } - /> - )} -
{renderActionButtons()}