diff --git a/App.jsx b/App.jsx new file mode 100644 index 0000000..a8a6231 --- /dev/null +++ b/App.jsx @@ -0,0 +1,2432 @@ +import React, { useState, useEffect } from 'react'; +import { + postgresApi, + s3Api, + postgresToS3Api, + injectEnv, + getCurrentEnv, + healthCheck, + formatEnvVars, + copyToClipboard, + formatMigrationDetails, + formatFileSize, + formatPostgresConfig, + formatS3Config, + extractPostgresInfo, + extractS3Info, + buildPostgresConnectionString, + buildS3Url, + groupTablesBySchema, + groupObjectsByPrefix, + // إضافة دوال التقدم المفقودة + streamProgress, + streamProgressAsync, + createProgressStream, + formatProgressDisplay, + createProgressBar, + estimateRemainingTime, + formatTimeEstimate +} from './unifiedApi'; +import './App.css'; + +function App() { + // ==================== PostgreSQL Source Configuration ==================== + const [postgresConfig, setPostgresConfig] = useState({ + host: '', + user: '', + password: '', + port: 5432, + database: '', + uri: '' + }); + + // ==================== PostgreSQL Destination Configuration ==================== + const [destPostgresConfig, setDestPostgresConfig] = useState({ + host: '', + user: '', + password: '', + port: 5432, + database: '', + uri: '' + }); + + // ==================== Progress Tracking States ==================== + const [progressData, setProgressData] = useState(null); + const [progressStream, setProgressStream] = useState(null); + const [showProgressModal, setShowProgressModal] = useState(false); + const [progressHistory, setProgressHistory] = useState([]); + + // ==================== Source S3 Configuration ==================== + const [sourceS3Config, setSourceS3Config] = useState({ + accessKeyId: '', + secretAccessKey: '', + region: 'us-east-1', + endpointUrl: '', + sessionToken: '', + bucket: '', + prefix: '' + }); + + // ==================== Destination S3 Configuration ==================== + const [destS3Config, setDestS3Config] = useState({ + accessKeyId: '', + secretAccessKey: '', + region: 'us-east-1', + endpointUrl: '', + sessionToken: '', + bucket: '', + prefix: '' + }); + + // ==================== PostgreSQL to S3 Configuration ==================== + const [pgToS3Config, setPgToS3Config] = useState({ + postgresUri: '', + s3Bucket: '', + s3Prefix: '', + compress: true, + format: 'csv' + }); + + // ==================== State Variables ==================== + const [postgresSchemas, setPostgresSchemas] = useState([]); + const [postgresTables, setPostgresTables] = useState([]); + const [sourceBuckets, setSourceBuckets] = useState([]); + const [destBuckets, setDestBuckets] = useState([]); + const [sourceObjects, setSourceObjects] = useState([]); + const [destObjects, setDestObjects] = useState([]); + const [selectedPostgresDb, setSelectedPostgresDb] = useState(''); + const [selectedSourceBucket, setSelectedSourceBucket] = useState(''); + const [selectedDestBucket, setSelectedDestBucket] = useState(''); + const [selectedPrefix, setSelectedPrefix] = useState(''); + const [selectedSchemas, setSelectedSchemas] = useState([]); + const [selectedTables, setSelectedTables] = useState([]); + const [selectedObjects, setSelectedObjects] = useState([]); + const [migrations, setMigrations] = useState([]); + const [activeMigration, setActiveMigration] = useState(null); + const [migrationLogs, setMigrationLogs] = useState([]); + const [parsedPostgresUri, setParsedPostgresUri] = useState(null); + const [parsedS3Uri, setParsedS3Uri] = useState(null); + const [currentEnv, setCurrentEnv] = useState({}); + const [loading, setLoading] = useState(false); + const [activeTab, setActiveTab] = useState('postgres'); + const [envFormat, setEnvFormat] = useState('dotenv'); + const [notifications, setNotifications] = useState([]); + const [postgresConnectionStatus, setPostgresConnectionStatus] = useState(null); + const [destPostgresConnectionStatus, setDestPostgresConnectionStatus] = useState(null); + const [sourceS3ConnectionStatus, setSourceS3ConnectionStatus] = useState(null); + const [destS3ConnectionStatus, setDestS3ConnectionStatus] = useState(null); + const [showSourceBuckets, setShowSourceBuckets] = useState(false); + const [showDestBuckets, setShowDestBuckets] = useState(false); + const [showPostgresSchemas, setShowPostgresSchemas] = useState(false); + const [migrationOptions, setMigrationOptions] = useState({ + createDestBucket: true, + maxConcurrent: 5, + preserveMetadata: true, + storageClass: 'STANDARD', + includePatterns: '', + excludePatterns: '' + }); + const [objectGroups, setObjectGroups] = useState({}); + const [tableGroups, setTableGroups] = useState({}); + const [totalSize, setTotalSize] = useState(0); + const [totalRows, setTotalRows] = useState(0); + + // ==================== Notification System ==================== + const addNotification = (message, type = 'info') => { + const id = Date.now(); + setNotifications(prev => [...prev, { id, message, type }]); + setTimeout(() => { + setNotifications(prev => prev.filter(n => n.id !== id)); + }, 5000); + }; + + // ==================== Load Initial Data ==================== + useEffect(() => { + loadCurrentEnv(); + }, []); + + const loadCurrentEnv = async () => { + try { + const result = await getCurrentEnv(); + if (result.success) { + setCurrentEnv(result.environment_variables); + } + } catch (error) { + console.error('Error loading current env:', error); + } + }; + + // ============================================================================ + // PostgreSQL Functions + // ============================================================================ + + const testPostgresConnection = async (isSource = true) => { + setLoading(true); + try { + const config = isSource ? postgresConfig : destPostgresConfig; + let uri = config.uri; + + if (!uri && config.host && config.user && config.password && config.database) { + uri = `postgresql://${config.user}:${config.password}@${config.host}:${config.port}/${config.database}`; + } + + const result = await postgresApi.testConnection({ + useEnvVars: false, + uri + }); + + if (result.success) { + addNotification(`✅ PostgreSQL ${isSource ? 'source' : 'destination'} connection successful!`, 'success'); + if (isSource) { + setPostgresConnectionStatus({ + success: true, + host: config.host, + port: config.port, + version: result.version, + database: result.database + }); + } else { + setDestPostgresConnectionStatus({ + success: true, + host: config.host, + port: config.port, + version: result.version, + database: result.database + }); + } + loadCurrentEnv(); + } else { + addNotification(`❌ PostgreSQL ${isSource ? 'source' : 'destination'} connection failed: ${result.error}`, 'error'); + if (isSource) { + setPostgresConnectionStatus({ + success: false, + error: result.error, + diagnostic: result.diagnostic + }); + } else { + setDestPostgresConnectionStatus({ + success: false, + error: result.error, + diagnostic: result.diagnostic + }); + } + } + } catch (error) { + addNotification(`❌ Error testing PostgreSQL connection: ${error.message}`, 'error'); + if (isSource) { + setPostgresConnectionStatus({ + success: false, + error: error.message + }); + } else { + setDestPostgresConnectionStatus({ + success: false, + error: error.message + }); + } + } + setLoading(false); + }; + + const getPostgresSchemas = async (isSource = true) => { + const config = isSource ? postgresConfig : destPostgresConfig; + let uri = config.uri || buildPostgresConnectionString( + config.host, + config.database, + config.user, + config.port + ); + + if (!uri) { + addNotification('Please enter PostgreSQL connection details', 'warning'); + return; + } + + setLoading(true); + try { + const result = await postgresApi.getSchemas(uri); + if (result.success) { + setPostgresSchemas(result.schemas || []); + setShowPostgresSchemas(true); + addNotification(`✅ Found ${result.count} schema(s)`, 'success'); + } else { + addNotification(`❌ Failed to get schemas: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error getting schemas: ${error.message}`, 'error'); + } + setLoading(false); + }; + + const getPostgresTables = async (schema = '', isSource = true) => { + const config = isSource ? postgresConfig : destPostgresConfig; + let uri = config.uri || buildPostgresConnectionString( + config.host, + config.database, + config.user, + config.port + ); + + if (!uri) { + addNotification('Please enter PostgreSQL connection details', 'warning'); + return; + } + + setLoading(true); + try { + const result = await postgresApi.getTables(uri, schema); + if (result.success) { + setPostgresTables(result.tables || []); + + const total = (result.tables || []).reduce((sum, table) => sum + (table.rows || 0), 0); + setTotalRows(total); + + const groups = groupTablesBySchema(result.tables || []); + setTableGroups(groups); + + addNotification(`✅ Found ${result.count} table(s)`, 'success'); + } else { + addNotification(`❌ Failed to get tables: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error getting tables: ${error.message}`, 'error'); + } + setLoading(false); + }; + + const parsePostgresUri = async (uri, isSource = true) => { + try { + const result = await postgresApi.parseUri(uri); + if (result.success) { + setParsedPostgresUri(result); + if (result.parsed) { + if (isSource) { + setPostgresConfig({ + host: result.parsed.host || '', + user: result.parsed.user || '', + password: result.parsed.password || '', + port: result.parsed.port || 5432, + database: result.parsed.database || '', + uri: uri + }); + } else { + setDestPostgresConfig({ + host: result.parsed.host || '', + user: result.parsed.user || '', + password: result.parsed.password || '', + port: result.parsed.port || 5432, + database: result.parsed.database || '', + uri: uri + }); + } + } + addNotification('✅ PostgreSQL URI parsed successfully', 'success'); + } else { + setParsedPostgresUri(null); + addNotification(`❌ Failed to parse PostgreSQL URI: ${result.error}`, 'error'); + } + } catch (error) { + console.error('Error parsing PostgreSQL URI:', error); + addNotification(`❌ Error parsing PostgreSQL URI: ${error.message}`, 'error'); + } + }; + + // ============================================================================ + // S3 Functions + // ============================================================================ + + const testSourceS3Connection = async () => { + setLoading(true); + try { + const result = await s3Api.testSourceConnection({ + useEnvVars: false, + accessKeyId: sourceS3Config.accessKeyId, + secretAccessKey: sourceS3Config.secretAccessKey, + region: sourceS3Config.region, + endpointUrl: sourceS3Config.endpointUrl, + sessionToken: sourceS3Config.sessionToken + }); + + if (result.success) { + addNotification(`✅ Source S3 connection successful!`, 'success'); + setSourceS3ConnectionStatus({ + success: true, + endpoint: sourceS3Config.endpointUrl || 'AWS S3 (default)', + region: sourceS3Config.region, + bucketCount: result.bucket_count + }); + loadCurrentEnv(); + } else { + addNotification(`❌ Source S3 connection failed: ${result.error}`, 'error'); + setSourceS3ConnectionStatus({ + success: false, + error: result.error, + diagnostic: result.diagnostic + }); + } + } catch (error) { + addNotification(`❌ Error testing source S3 connection: ${error.message}`, 'error'); + setSourceS3ConnectionStatus({ + success: false, + error: error.message + }); + } + setLoading(false); + }; + + const testDestinationS3Connection = async () => { + setLoading(true); + try { + const result = await s3Api.testDestinationConnection({ + useEnvVars: false, + accessKeyId: destS3Config.accessKeyId, + secretAccessKey: destS3Config.secretAccessKey, + region: destS3Config.region, + endpointUrl: destS3Config.endpointUrl, + sessionToken: destS3Config.sessionToken + }); + + if (result.success) { + addNotification(`✅ Destination S3 connection successful!`, 'success'); + setDestS3ConnectionStatus({ + success: true, + endpoint: destS3Config.endpointUrl || 'AWS S3 (default)', + region: destS3Config.region, + bucketCount: result.bucket_count + }); + loadCurrentEnv(); + } else { + addNotification(`❌ Destination S3 connection failed: ${result.error}`, 'error'); + setDestS3ConnectionStatus({ + success: false, + error: result.error, + diagnostic: result.diagnostic + }); + } + } catch (error) { + addNotification(`❌ Error testing destination S3 connection: ${error.message}`, 'error'); + setDestS3ConnectionStatus({ + success: false, + error: error.message + }); + } + setLoading(false); + }; + + const listSourceBuckets = async () => { + setLoading(true); + try { + const result = await s3Api.listSourceBuckets( + sourceS3Config.accessKeyId, + sourceS3Config.secretAccessKey, + sourceS3Config.region, + sourceS3Config.endpointUrl, + sourceS3Config.sessionToken + ); + + if (result.success) { + setSourceBuckets(result.buckets || []); + setShowSourceBuckets(true); + addNotification(`✅ Found ${result.count} source bucket(s)`, 'success'); + } else { + addNotification(`❌ Failed to list source buckets: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error listing source buckets: ${error.message}`, 'error'); + } + setLoading(false); + }; + + const listDestinationBuckets = async () => { + setLoading(true); + try { + const result = await s3Api.listDestinationBuckets( + destS3Config.accessKeyId, + destS3Config.secretAccessKey, + destS3Config.region, + destS3Config.endpointUrl, + destS3Config.sessionToken + ); + + if (result.success) { + setDestBuckets(result.buckets || []); + setShowDestBuckets(true); + addNotification(`✅ Found ${result.count} destination bucket(s)`, 'success'); + } else { + addNotification(`❌ Failed to list destination buckets: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error listing destination buckets: ${error.message}`, 'error'); + } + setLoading(false); + }; + + const selectSourceBucket = (bucketName) => { + setSelectedSourceBucket(bucketName); + setSourceS3Config(prev => ({ ...prev, bucket: bucketName })); + setShowSourceBuckets(false); + addNotification(`✅ Selected source bucket: ${bucketName}`, 'success'); + }; + + const selectDestBucket = (bucketName) => { + setSelectedDestBucket(bucketName); + setDestS3Config(prev => ({ ...prev, bucket: bucketName })); + setShowDestBuckets(false); + addNotification(`✅ Selected destination bucket: ${bucketName}`, 'success'); + }; + + const listSourceObjects = async () => { + if (!selectedSourceBucket) { + addNotification('Please select source bucket first', 'warning'); + return; + } + + setLoading(true); + try { + const result = await s3Api.listObjects( + selectedSourceBucket, + selectedPrefix, + true, + { + accessKeyId: sourceS3Config.accessKeyId, + secretAccessKey: sourceS3Config.secretAccessKey, + region: sourceS3Config.region, + endpointUrl: sourceS3Config.endpointUrl, + sessionToken: sourceS3Config.sessionToken, + maxKeys: 1000 + } + ); + + if (result.success) { + setSourceObjects(result.objects || []); + setTotalSize(result.total_size || 0); + + const groups = groupObjectsByPrefix(result.objects || [], 2); + setObjectGroups(groups); + + addNotification(`✅ Found ${result.count} object(s) in ${selectedSourceBucket}`, 'success'); + } else { + addNotification(`❌ Failed to list objects: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error listing objects: ${error.message}`, 'error'); + } + setLoading(false); + }; + + const createDestinationBucket = async () => { + if (!selectedDestBucket) { + addNotification('Please enter destination bucket name', 'warning'); + return; + } + + setLoading(true); + try { + const result = await s3Api.createBucket( + selectedDestBucket, + destS3Config.region, + { + accessKeyId: destS3Config.accessKeyId, + secretAccessKey: destS3Config.secretAccessKey, + endpointUrl: destS3Config.endpointUrl, + sessionToken: destS3Config.sessionToken + } + ); + + if (result.success) { + if (result.created) { + addNotification(`✅ Bucket created successfully: ${selectedDestBucket}`, 'success'); + } else { + addNotification(`ℹ️ Bucket already exists: ${selectedDestBucket}`, 'info'); + } + } else { + addNotification(`❌ Failed to create bucket: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error creating bucket: ${error.message}`, 'error'); + } + setLoading(false); + }; + + const migrateSingleObject = async (sourceKey) => { + if (!selectedSourceBucket || !selectedDestBucket) { + addNotification('Please select source and destination buckets', 'warning'); + return; + } + + setLoading(true); + try { + const result = await s3Api.migrateObject( + selectedSourceBucket, + sourceKey, + selectedDestBucket, + `${selectedPrefix}${sourceKey}`, + { + sourceAccessKeyId: sourceS3Config.accessKeyId, + sourceSecretAccessKey: sourceS3Config.secretAccessKey, + sourceRegion: sourceS3Config.region, + sourceEndpointUrl: sourceS3Config.endpointUrl, + sourceSessionToken: sourceS3Config.sessionToken, + + destAccessKeyId: destS3Config.accessKeyId, + destSecretAccessKey: destS3Config.secretAccessKey, + destRegion: destS3Config.region, + destEndpointUrl: destS3Config.endpointUrl, + destSessionToken: destS3Config.sessionToken, + + preserveMetadata: migrationOptions.preserveMetadata, + storageClass: migrationOptions.storageClass + } + ); + + if (result.success) { + addNotification(`✅ Migrated: ${sourceKey}`, 'success'); + listSourceObjects(); + } else { + addNotification(`❌ Migration failed: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error migrating object: ${error.message}`, 'error'); + } + setLoading(false); + }; + + const migrateSelectedObjects = async () => { + if (selectedObjects.length === 0) { + addNotification('Please select objects to migrate', 'warning'); + return; + } + + setLoading(true); + try { + const result = await s3Api.migrateBatch( + selectedObjects.map(key => ({ key })), + selectedSourceBucket, + selectedDestBucket, + { + sourceAccessKeyId: sourceS3Config.accessKeyId, + sourceSecretAccessKey: sourceS3Config.secretAccessKey, + sourceRegion: sourceS3Config.region, + sourceEndpointUrl: sourceS3Config.endpointUrl, + sourceSessionToken: sourceS3Config.sessionToken, + + destAccessKeyId: destS3Config.accessKeyId, + destSecretAccessKey: destS3Config.secretAccessKey, + destRegion: destS3Config.region, + destEndpointUrl: destS3Config.endpointUrl, + destSessionToken: destS3Config.sessionToken, + + preserveMetadata: migrationOptions.preserveMetadata, + storageClass: migrationOptions.storageClass, + maxConcurrent: migrationOptions.maxConcurrent + } + ); + + if (result.success) { + addNotification(`✅ Migrated ${result.results?.successful?.length || 0} objects`, 'success'); + setSelectedObjects([]); + listSourceObjects(); + } else { + addNotification(`❌ Batch migration failed: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error in batch migration: ${error.message}`, 'error'); + } + setLoading(false); + }; + + const parseS3Uri = async (uri) => { + try { + const result = await s3Api.parseUri(uri); + if (result.success) { + setParsedS3Uri(result); + if (result.parsed) { + setSelectedSourceBucket(result.parsed.bucket); + setSelectedPrefix(result.parsed.key || ''); + } + addNotification('✅ S3 URI parsed successfully', 'success'); + } else { + setParsedS3Uri(null); + addNotification(`❌ Failed to parse S3 URI: ${result.error}`, 'error'); + } + } catch (error) { + console.error('Error parsing S3 URI:', error); + addNotification(`❌ Error parsing S3 URI: ${error.message}`, 'error'); + } + }; + + // ============================================================================ + // PostgreSQL to S3 Functions + // ============================================================================ + + const testPgToS3PostgresConnection = async () => { + if (!pgToS3Config.postgresUri) { + addNotification('Please enter PostgreSQL URI', 'warning'); + return; + } + + setLoading(true); + try { + const result = await postgresToS3Api.testPostgresConnection(pgToS3Config.postgresUri); + + if (result.success) { + addNotification(`✅ PostgreSQL connection successful!`, 'success'); + } else { + addNotification(`❌ PostgreSQL connection failed: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error testing PostgreSQL connection: ${error.message}`, 'error'); + } + setLoading(false); + }; + + const testPgToS3S3Connection = async () => { + setLoading(true); + try { + const result = await postgresToS3Api.testS3Connection( + sourceS3Config.accessKeyId, + sourceS3Config.secretAccessKey, + sourceS3Config.region, + sourceS3Config.endpointUrl + ); + + if (result.success) { + addNotification(`✅ S3 connection successful!`, 'success'); + } else { + addNotification(`❌ S3 connection failed: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error testing S3 connection: ${error.message}`, 'error'); + } + setLoading(false); + }; + + // ============================================================================ + // Progress Tracking Functions + // ============================================================================ + + const startProgressTracking = (migrationId, type = 's3') => { + setShowProgressModal(true); + setProgressData({ + migrationId, + type, + status: 'connecting', + percentage: 0, + startTime: new Date().toISOString(), + logs: [{ + timestamp: new Date().toISOString(), + message: `🔄 Connecting to migration stream for ${migrationId}...`, + level: 'info' + }] + }); + + const stream = createProgressStream(migrationId, type, { + onProgress: (progress) => { + setProgressData(prev => { + const percentage = progress.percentage || progress.percentages?.size || 0; + + const newLogs = [...(prev?.logs || [])]; + if (prev?.percentage < 25 && percentage >= 25) { + newLogs.push({ + timestamp: new Date().toISOString(), + message: `✅ 25% complete - ${progress.processed_size_formatted || progress.processed?.size_formatted} transferred`, + level: 'success' + }); + } else if (prev?.percentage < 50 && percentage >= 50) { + newLogs.push({ + timestamp: new Date().toISOString(), + message: `✅ 50% complete - halfway there!`, + level: 'success' + }); + } else if (prev?.percentage < 75 && percentage >= 75) { + newLogs.push({ + timestamp: new Date().toISOString(), + message: `✅ 75% complete - almost done!`, + level: 'success' + }); + } + + return { + ...prev, + ...progress, + percentage, + status: 'running', + logs: newLogs + }; + }); + + updateProgressInUI(migrationId, progress); + }, + onComplete: (completion) => { + setProgressData(prev => ({ + ...prev, + status: 'completed', + percentage: 100, + completedAt: new Date().toISOString(), + logs: [...(prev?.logs || []), { + timestamp: new Date().toISOString(), + message: '✅ Migration completed successfully!', + level: 'success' + }] + })); + + addNotification(`✅ Migration ${migrationId} completed!`, 'success'); + + setProgressHistory(prev => [...prev, { + migrationId, + type, + completedAt: new Date().toISOString(), + status: 'completed', + totalTime: progressData?.elapsed_time || 0 + }]); + + loadMigrations(type); + + setTimeout(() => { + if (progressStream === stream) { + setShowProgressModal(false); + } + }, 3000); + }, + onError: (error) => { + setProgressData(prev => ({ + ...prev, + status: 'error', + error: error.error, + logs: [...(prev?.logs || []), { + timestamp: new Date().toISOString(), + message: `❌ Error: ${error.error}`, + level: 'error' + }] + })); + + addNotification(`❌ Migration error: ${error.error}`, 'error'); + + setProgressHistory(prev => [...prev, { + migrationId, + type, + completedAt: new Date().toISOString(), + status: 'failed', + error: error.error + }]); + }, + reconnectInterval: 3000, + maxReconnectAttempts: 5 + }); + + setProgressStream(stream); + return stream; + }; + + const stopProgressTracking = () => { + if (progressStream) { + progressStream.stop(); + setProgressStream(null); + } + setShowProgressModal(false); + setProgressData(null); + }; + + const updateProgressInUI = (migrationId, progress) => { + const percentage = progress.percentage || progress.percentages?.size || 0; + + setTimeout(() => { + const migrationCards = document.querySelectorAll('.migration-card'); + migrationCards.forEach(card => { + const idElement = card.querySelector('.migration-id'); + if (idElement && idElement.textContent === migrationId) { + let progressBar = card.querySelector('.progress-bar-fill'); + let progressText = card.querySelector('.progress-text'); + + if (!progressBar) { + const progressContainer = document.createElement('div'); + progressContainer.className = 'progress-bar-container'; + progressContainer.innerHTML = ` +
+ ${percentage.toFixed(1)}% + `; + card.querySelector('.migration-body')?.appendChild(progressContainer); + } else { + progressBar.style.width = `${percentage}%`; + if (progressText) { + progressText.textContent = `${percentage.toFixed(1)}%`; + } + } + } + }); + }, 100); + }; + + // ============================================================================ + // Migration Start Functions (using streaming) + // ============================================================================ + + const startPostgresMigration = async () => { + let sourceUri = postgresConfig.uri || buildPostgresConnectionString( + postgresConfig.host, + postgresConfig.database, + postgresConfig.user, + postgresConfig.port + ); + + let destUri = destPostgresConfig.uri || buildPostgresConnectionString( + destPostgresConfig.host, + destPostgresConfig.database, + destPostgresConfig.user, + destPostgresConfig.port + ); + + if (!sourceUri || !destUri) { + addNotification('Please enter source and destination URIs', 'warning'); + return; + } + + setLoading(true); + + try { + const result = await postgresApi.startMigration( + sourceUri, + destUri, + selectedSchemas.length > 0 ? selectedSchemas : null, + selectedTables.length > 0 ? selectedTables : null + ); + + if (result.success) { + const migrationId = result.migration_id; + setActiveMigration(migrationId); + addNotification(`✅ PostgreSQL migration ${migrationId} started!`, 'success'); + startProgressTracking(migrationId, 'postgres'); + } else { + addNotification(`❌ Failed to start migration: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error starting migration: ${error.message}`, 'error'); + } + setLoading(false); + }; + + const startS3Migration = async () => { + if (!selectedSourceBucket) { + addNotification('Please select source bucket', 'warning'); + return; + } + + if (!selectedDestBucket) { + addNotification('Please select destination bucket', 'warning'); + return; + } + + setLoading(true); + + try { + const includePatterns = migrationOptions.includePatterns + ? migrationOptions.includePatterns.split(',').map(p => p.trim()) + : null; + + const excludePatterns = migrationOptions.excludePatterns + ? migrationOptions.excludePatterns.split(',').map(p => p.trim()) + : null; + + const result = await s3Api.startMigration( + selectedSourceBucket, + selectedDestBucket, + selectedPrefix, + { + sourceAccessKeyId: sourceS3Config.accessKeyId, + sourceSecretAccessKey: sourceS3Config.secretAccessKey, + sourceRegion: sourceS3Config.region, + sourceEndpointUrl: sourceS3Config.endpointUrl, + sourceSessionToken: sourceS3Config.sessionToken, + + destAccessKeyId: destS3Config.accessKeyId, + destSecretAccessKey: destS3Config.secretAccessKey, + destRegion: destS3Config.region, + destEndpointUrl: destS3Config.endpointUrl, + destSessionToken: destS3Config.sessionToken, + + includePatterns, + excludePatterns, + preserveMetadata: migrationOptions.preserveMetadata, + storageClass: migrationOptions.storageClass, + createDestBucket: migrationOptions.createDestBucket, + maxConcurrent: migrationOptions.maxConcurrent + } + ); + + if (result.success) { + const migrationId = result.migration_id; + setActiveMigration(migrationId); + addNotification(`✅ S3 to S3 migration ${migrationId} started!`, 'success'); + startProgressTracking(migrationId, 's3'); + } else { + addNotification(`❌ Failed to start migration: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error starting migration: ${error.message}`, 'error'); + } + setLoading(false); + }; + + const startPgToS3Migration = async () => { + if (!pgToS3Config.postgresUri) { + addNotification('Please enter PostgreSQL URI', 'warning'); + return; + } + + if (!pgToS3Config.s3Bucket) { + addNotification('Please enter S3 bucket name', 'warning'); + return; + } + + setLoading(true); + + try { + const result = await postgresToS3Api.startMigration( + pgToS3Config.postgresUri, + pgToS3Config.s3Bucket, + pgToS3Config.s3Prefix, + { + schemas: selectedSchemas.length > 0 ? selectedSchemas : null, + tables: selectedTables.length > 0 ? selectedTables : null, + compress: pgToS3Config.compress, + format: pgToS3Config.format, + accessKeyId: sourceS3Config.accessKeyId, + secretAccessKey: sourceS3Config.secretAccessKey, + region: sourceS3Config.region, + endpointUrl: sourceS3Config.endpointUrl + } + ); + + if (result.success) { + const migrationId = result.migration_id; + setActiveMigration(migrationId); + addNotification(`✅ PostgreSQL to S3 migration ${migrationId} started!`, 'success'); + startProgressTracking(migrationId, 'pg-to-s3'); + } else { + addNotification(`❌ Failed to start migration: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error starting migration: ${error.message}`, 'error'); + } + setLoading(false); + }; + + // ============================================================================ + // Common Functions + // ============================================================================ + + const loadMigrations = async (type = 'postgres') => { + try { + let result; + if (type === 'postgres') result = await postgresApi.listMigrations(); + else if (type === 's3') result = await s3Api.listMigrations(); + else if (type === 'pg-to-s3') result = await postgresToS3Api.listMigrations(); + + if (result?.success) { + setMigrations(result.migrations || []); + } + } catch (error) { + console.error('Error loading migrations:', error); + } + }; + + const cancelMigration = async (migrationId, type = 's3') => { + try { + let result; + if (type === 's3') result = await s3Api.cancelMigration(migrationId); + + if (result?.success) { + addNotification(`✅ Migration ${migrationId} cancelled`, 'success'); + loadMigrations(type); + } else { + addNotification(`❌ Failed to cancel migration: ${result?.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error cancelling migration: ${error.message}`, 'error'); + } + }; + + const injectEnvironment = async () => { + setLoading(true); + try { + const envVars = { + PG_HOST: postgresConfig.host, + PG_PORT: postgresConfig.port.toString(), + PG_USER: postgresConfig.user, + PG_PASSWORD: postgresConfig.password, + PG_DATABASE: postgresConfig.database, + + DEST_PG_HOST: destPostgresConfig.host, + DEST_PG_PORT: destPostgresConfig.port.toString(), + DEST_PG_USER: destPostgresConfig.user, + DEST_PG_PASSWORD: destPostgresConfig.password, + DEST_PG_DATABASE: destPostgresConfig.database, + + SOURCE_AWS_ACCESS_KEY_ID: sourceS3Config.accessKeyId, + SOURCE_AWS_SECRET_ACCESS_KEY: sourceS3Config.secretAccessKey, + SOURCE_AWS_REGION: sourceS3Config.region, + SOURCE_AWS_ENDPOINT_URL: sourceS3Config.endpointUrl, + SOURCE_S3_BUCKET: selectedSourceBucket, + SOURCE_S3_PREFIX: selectedPrefix, + + DEST_AWS_ACCESS_KEY_ID: destS3Config.accessKeyId, + DEST_AWS_SECRET_ACCESS_KEY: destS3Config.secretAccessKey, + DEST_AWS_REGION: destS3Config.region, + DEST_AWS_ENDPOINT_URL: destS3Config.endpointUrl, + DEST_S3_BUCKET: selectedDestBucket, + DEST_S3_PREFIX: selectedPrefix + }; + + const result = await injectEnv(envVars); + if (result.success) { + addNotification(`✅ Injected ${result.injected_variables?.length || 0} environment variables`, 'success'); + loadCurrentEnv(); + } else { + addNotification(`❌ Failed to inject environment: ${result.error}`, 'error'); + } + } catch (error) { + addNotification(`❌ Error injecting environment: ${error.message}`, 'error'); + } + setLoading(false); + }; + + const clearEnvironment = async () => { + try { + const result = await injectEnv({}); + if (result.success) { + addNotification('✅ Environment cleared', 'success'); + loadCurrentEnv(); + } + } catch (error) { + addNotification(`❌ Error clearing environment: ${error.message}`, 'error'); + } + }; + + const copyEnvToClipboard = async () => { + if (!currentEnv || Object.keys(currentEnv).length === 0) { + addNotification('No environment variables to copy', 'warning'); + return; + } + + const formatted = formatEnvVars(currentEnv, envFormat); + const success = await copyToClipboard(formatted); + if (success) { + addNotification(`✅ Copied ${Object.keys(currentEnv).length} variables to clipboard as ${envFormat} format`, 'success'); + } else { + addNotification(`❌ Failed to copy to clipboard`, 'error'); + } + }; + + const toggleObjectSelection = (key) => { + setSelectedObjects(prev => + prev.includes(key) + ? prev.filter(k => k !== key) + : [...prev, key] + ); + }; + + const toggleTableSelection = (tableName) => { + setSelectedTables(prev => + prev.includes(tableName) + ? prev.filter(t => t !== tableName) + : [...prev, tableName] + ); + }; + + const toggleSchemaSelection = (schemaName) => { + setSelectedSchemas(prev => + prev.includes(schemaName) + ? prev.filter(s => s !== schemaName) + : [...prev, schemaName] + ); + }; + + const selectAllObjects = () => { + setSelectedObjects(sourceObjects.map(obj => obj.key)); + }; + + const selectAllTables = () => { + setSelectedTables(postgresTables.map(table => table.name)); + }; + + const clearSelectedObjects = () => { + setSelectedObjects([]); + }; + + const clearSelectedTables = () => { + setSelectedTables([]); + }; + + // ============================================================================ + // Render Functions + // ============================================================================ + + const renderProgressModal = () => { + if (!showProgressModal || !progressData) return null; + + const progress = progressData; + const percentage = progress.percentage || 0; + const bar = createProgressBar(percentage); + + return ( +
+
+
+

+ + Migration Progress: {progress.migrationId} +

+ +
+ +
+
+
+
+ {percentage.toFixed(1)}% +
+ +
{bar}
+ +
+ {progress.processed_size_formatted && progress.total_size_formatted && ( +
+ Size + {progress.processed_size_formatted} / {progress.total_size_formatted} +
+ )} + + {progress.processed_objects !== undefined && progress.total_objects !== undefined && ( +
+ Objects + {progress.processed_objects} / {progress.total_objects} +
+ )} + + {progress.processed?.tables !== undefined && progress.total?.tables !== undefined && ( +
+ Tables + {progress.processed.tables} / {progress.total.tables} +
+ )} + + {progress.current_speed_formatted && ( +
+ Speed + {progress.current_speed_formatted} +
+ )} + + {progress.eta_formatted && ( +
+ ETA + {progress.eta_formatted} +
+ )} + + {progress.elapsed_time_formatted && ( +
+ Elapsed + {progress.elapsed_time_formatted} +
+ )} +
+ + {(progress.current_object || progress.current_table) && ( +
+

Currently Processing:

+ {progress.current_object && ( +
+ 📄 + {progress.current_object} +
+ )} + {progress.current_table && ( +
+ 📊 + {progress.current_table.name} + {progress.current_table.rows && ( + + {progress.current_table.rows_processed} / {progress.current_table.rows} rows + + )} +
+ )} +
+ )} + + {progress.logs && progress.logs.length > 0 && ( +
+

Activity Log

+
+ {progress.logs.slice(-10).map((log, index) => ( +
+ + {new Date(log.timestamp).toLocaleTimeString()} + + {log.message} +
+ ))} +
+
+ )} + +
+ {progress.status === 'running' && ( + + )} + {progress.status === 'completed' && ( + + )} +
+
+
+
+
+ ); + }; + + const renderProgressHistory = () => { + if (progressHistory.length === 0) return null; + + return ( +
+

Recent Migrations

+
+ {progressHistory.slice(-5).reverse().map((item, index) => ( +
+ {item.migrationId} + {item.type} + {item.status} + + {new Date(item.completedAt).toLocaleTimeString()} + + {item.totalTime && ( + {formatTimeEstimate(item.totalTime)} + )} +
+ ))} +
+
+ ); + }; + + // ============================================================================ + // Render + // ============================================================================ + return ( +
+ {/* Notifications */} +
+ {notifications.map(notification => ( +
+ {notification.message} +
+ ))} +
+ +
+

🔄 Unified Migration Tool

+

PostgreSQL, S3 to S3, and PostgreSQL to S3 migrations

+
+ +
+ {/* Tabs */} +
+ + + + + + + +
+ + {/* ==================== PostgreSQL Tab ==================== */} + {activeTab === 'postgres' && ( +
+

🐘 PostgreSQL Configuration

+ +
+
+
+

Source PostgreSQL

+
+
+ + setPostgresConfig(prev => ({ ...prev, host: e.target.value, uri: '' }))} + placeholder="localhost" + /> +
+
+ + setPostgresConfig(prev => ({ ...prev, port: parseInt(e.target.value) || 5432, uri: '' }))} + placeholder="5432" + /> +
+
+ + setPostgresConfig(prev => ({ ...prev, user: e.target.value, uri: '' }))} + placeholder="postgres" + /> +
+
+ + setPostgresConfig(prev => ({ ...prev, password: e.target.value, uri: '' }))} + placeholder="••••••••" + /> +
+
+ + setPostgresConfig(prev => ({ ...prev, database: e.target.value, uri: '' }))} + placeholder="postgres" + /> +
+
+ +
+ + setPostgresConfig(prev => ({ ...prev, uri: e.target.value }))} + placeholder="postgresql://user:pass@host:5432/database" + className="uri-input" + /> + +
+ +
+ + + +
+ + {postgresConnectionStatus && ( +
+

Source Connection Status:

+

Success: {postgresConnectionStatus.success ? '✅ Yes' : '❌ No'}

+ {postgresConnectionStatus.host &&

Host: {postgresConnectionStatus.host}:{postgresConnectionStatus.port}

} + {postgresConnectionStatus.version &&

Version: {postgresConnectionStatus.version}

} + {postgresConnectionStatus.database &&

Database: {postgresConnectionStatus.database}

} + {postgresConnectionStatus.error &&

Error: {postgresConnectionStatus.error}

} + {postgresConnectionStatus.diagnostic && ( +
+

Diagnostic: {postgresConnectionStatus.diagnostic.message}

+

Reason: {postgresConnectionStatus.diagnostic.reason}

+
+ )} +
+ )} +
+
+ +
+
+

Destination PostgreSQL

+
+
+ + setDestPostgresConfig(prev => ({ ...prev, host: e.target.value, uri: '' }))} + placeholder="localhost" + /> +
+
+ + setDestPostgresConfig(prev => ({ ...prev, port: parseInt(e.target.value) || 5432, uri: '' }))} + placeholder="5432" + /> +
+
+ + setDestPostgresConfig(prev => ({ ...prev, user: e.target.value, uri: '' }))} + placeholder="postgres" + /> +
+
+ + setDestPostgresConfig(prev => ({ ...prev, password: e.target.value, uri: '' }))} + placeholder="••••••••" + /> +
+
+ + setDestPostgresConfig(prev => ({ ...prev, database: e.target.value, uri: '' }))} + placeholder="postgres" + /> +
+
+ +
+ + setDestPostgresConfig(prev => ({ ...prev, uri: e.target.value }))} + placeholder="postgresql://user:pass@host:5432/database" + className="uri-input" + /> + +
+ +
+ +
+ + {destPostgresConnectionStatus && ( +
+

Destination Connection Status:

+

Success: {destPostgresConnectionStatus.success ? '✅ Yes' : '❌ No'}

+ {destPostgresConnectionStatus.host &&

Host: {destPostgresConnectionStatus.host}:{destPostgresConnectionStatus.port}

} + {destPostgresConnectionStatus.version &&

Version: {destPostgresConnectionStatus.version}

} + {destPostgresConnectionStatus.database &&

Database: {destPostgresConnectionStatus.database}

} + {destPostgresConnectionStatus.error &&

Error: {destPostgresConnectionStatus.error}

} + {destPostgresConnectionStatus.diagnostic && ( +
+

Diagnostic: {destPostgresConnectionStatus.diagnostic.message}

+

Reason: {destPostgresConnectionStatus.diagnostic.reason}

+
+ )} +
+ )} +
+
+
+ + {postgresSchemas.length > 0 && ( +
+

Available Schemas

+
+ {postgresSchemas.map((schema, index) => ( +
+ + +
+ ))} +
+
+ )} + + {postgresTables.length > 0 && ( +
+

Tables in Source Database

+
+
+ + +
+
+ + {Object.entries(tableGroups).map(([schema, group]) => ( +
+

📚 Schema: {schema}

+
+ {group.count} tables +
+ + + + + + + + + + {group.tables.map((table, index) => ( + + + + + + ))} + +
+ { + const groupTables = group.tables.map(t => t.name); + if (e.target.checked) { + setSelectedTables([...new Set([...selectedTables, ...groupTables])]); + } else { + setSelectedTables(selectedTables.filter(name => !groupTables.includes(name))); + } + }} + checked={group.tables.every(t => selectedTables.includes(t.name))} + /> + Table NameType
+ toggleTableSelection(table.name)} + /> + {table.name}{table.type}
+
+ ))} +
+ )} +
+ )} + + {/* ==================== Source S3 Tab ==================== */} + {activeTab === 's3-source' && ( +
+

📤 Source S3 Configuration

+ +
+

AWS Credentials

+
+
+ + setSourceS3Config(prev => ({ ...prev, accessKeyId: e.target.value }))} + placeholder="AKIAIOSFODNN7EXAMPLE" + /> +
+
+ + setSourceS3Config(prev => ({ ...prev, secretAccessKey: e.target.value }))} + placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + /> +
+
+ + setSourceS3Config(prev => ({ ...prev, region: e.target.value }))} + placeholder="us-east-1" + /> +
+
+ + setSourceS3Config(prev => ({ ...prev, endpointUrl: e.target.value }))} + placeholder="https://s3.amazonaws.com (optional)" + /> +
+
+ + setSourceS3Config(prev => ({ ...prev, sessionToken: e.target.value }))} + placeholder="Optional for temporary credentials" + /> +
+
+ +
+ + +
+ + {sourceS3ConnectionStatus && ( +
+

Source Connection Status:

+

Success: {sourceS3ConnectionStatus.success ? '✅ Yes' : '❌ No'}

+ {sourceS3ConnectionStatus.endpoint &&

Endpoint: {sourceS3ConnectionStatus.endpoint}

} + {sourceS3ConnectionStatus.region &&

Region: {sourceS3ConnectionStatus.region}

} + {sourceS3ConnectionStatus.bucketCount !== undefined &&

Buckets Found: {sourceS3ConnectionStatus.bucketCount}

} + {sourceS3ConnectionStatus.error &&

Error: {sourceS3ConnectionStatus.error}

} + {sourceS3ConnectionStatus.diagnostic && ( +
+

Diagnostic: {sourceS3ConnectionStatus.diagnostic.message}

+

Reason: {sourceS3ConnectionStatus.diagnostic.reason}

+
+ )} +
+ )} +
+ +
+

Source Bucket Selection

+ +
+ setSelectedSourceBucket(e.target.value)} + className="bucket-input" + /> +
+ + {showSourceBuckets && sourceBuckets.length > 0 && ( +
+

Available Source Buckets:

+
+ {sourceBuckets.map((bucket, index) => ( +
selectSourceBucket(bucket.name)}> +
{bucket.name}
+
+ Region: {bucket.region} + Created: {new Date(bucket.creation_date).toLocaleDateString()} + Objects: {bucket.object_count} + Size: {formatFileSize(bucket.total_size)} +
+
+ ))} +
+ +
+ )} +
+
+ )} + + {/* ==================== Destination S3 Tab ==================== */} + {activeTab === 's3-dest' && ( +
+

📥 Destination S3 Configuration

+ +
+

AWS Credentials

+
+
+ + setDestS3Config(prev => ({ ...prev, accessKeyId: e.target.value }))} + placeholder="AKIAIOSFODNN7EXAMPLE" + /> +
+
+ + setDestS3Config(prev => ({ ...prev, secretAccessKey: e.target.value }))} + placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + /> +
+
+ + setDestS3Config(prev => ({ ...prev, region: e.target.value }))} + placeholder="us-east-1" + /> +
+
+ + setDestS3Config(prev => ({ ...prev, endpointUrl: e.target.value }))} + placeholder="https://s3.amazonaws.com (optional)" + /> +
+
+ + setDestS3Config(prev => ({ ...prev, sessionToken: e.target.value }))} + placeholder="Optional for temporary credentials" + /> +
+
+ +
+ + +
+ + {destS3ConnectionStatus && ( +
+

Destination Connection Status:

+

Success: {destS3ConnectionStatus.success ? '✅ Yes' : '❌ No'}

+ {destS3ConnectionStatus.endpoint &&

Endpoint: {destS3ConnectionStatus.endpoint}

} + {destS3ConnectionStatus.region &&

Region: {destS3ConnectionStatus.region}

} + {destS3ConnectionStatus.bucketCount !== undefined &&

Buckets Found: {destS3ConnectionStatus.bucketCount}

} + {destS3ConnectionStatus.error &&

Error: {destS3ConnectionStatus.error}

} + {destS3ConnectionStatus.diagnostic && ( +
+

Diagnostic: {destS3ConnectionStatus.diagnostic.message}

+

Reason: {destS3ConnectionStatus.diagnostic.reason}

+
+ )} +
+ )} +
+ +
+

Destination Bucket

+ +
+ setSelectedDestBucket(e.target.value)} + className="bucket-input" + /> + +
+ + {showDestBuckets && destBuckets.length > 0 && ( +
+

Available Destination Buckets:

+
+ {destBuckets.map((bucket, index) => ( +
selectDestBucket(bucket.name)}> +
{bucket.name}
+
+ Region: {bucket.region} + Created: {new Date(bucket.creation_date).toLocaleDateString()} + Objects: {bucket.object_count} + Size: {formatFileSize(bucket.total_size)} +
+
+ ))} +
+ +
+ )} +
+
+ )} + + {/* ==================== Browse S3 Tab ==================== */} + {activeTab === 's3-browse' && ( +
+

📁 Browse Source S3 Objects

+ +
+
+ + setSelectedSourceBucket(e.target.value)} + className="bucket-input" + placeholder="source-bucket-name" + /> + + + setSelectedPrefix(e.target.value)} + className="prefix-input" + placeholder="folder/subfolder/" + /> + + +
+ +
+ + parseS3Uri(e.target.value)} + className="uri-input" + /> +
+
+ + {parsedS3Uri && parsedS3Uri.success && ( +
+

Parsed S3 URI:

+
+
Bucket: {parsedS3Uri.parsed?.bucket}
+
Key: {parsedS3Uri.parsed?.key || '/'}
+
+
+ )} + + {sourceObjects.length > 0 && ( +
+
+

+ Objects in s3://{selectedSourceBucket}/{selectedPrefix} + {sourceObjects.length} files + {formatFileSize(totalSize)} +

+ +
+ + + +
+
+ + {/* Object Groups */} + {Object.entries(objectGroups).map(([prefix, group]) => ( +
+

📁 {prefix}

+
+ {group.count} objects + {formatFileSize(group.totalSize)} +
+ + + + + + + + + + + + + {group.objects.map((obj, index) => ( + + + + + + + + + ))} + +
+ { + const groupKeys = group.objects.map(obj => obj.key); + if (e.target.checked) { + setSelectedObjects([...new Set([...selectedObjects, ...groupKeys])]); + } else { + setSelectedObjects(selectedObjects.filter(key => !groupKeys.includes(key))); + } + }} + checked={group.objects.every(obj => selectedObjects.includes(obj.key))} + /> + KeySizeLast ModifiedETagActions
+ toggleObjectSelection(obj.key)} + /> + {obj.key}{formatFileSize(obj.size)} + {new Date(obj.last_modified).toLocaleString()} + {obj.etag?.substring(0, 8)}... + +
+
+ ))} +
+ )} + + {sourceObjects.length === 0 && selectedSourceBucket && ( +
+

📭 No objects found in s3://{selectedSourceBucket}/{selectedPrefix}

+
+ )} +
+ )} + + {/* ==================== PostgreSQL to S3 Tab ==================== */} + {activeTab === 'pg-to-s3' && ( +
+

🔄 PostgreSQL to S3 Migration

+ +
+

PostgreSQL Configuration

+
+
+ + setPgToS3Config(prev => ({ ...prev, postgresUri: e.target.value }))} + placeholder="postgresql://user:pass@host:5432/database" + className="uri-input" + /> +
+
+ +
+ +
+

S3 Configuration

+
+
+ + setSourceS3Config(prev => ({ ...prev, accessKeyId: e.target.value }))} + placeholder="AKIAIOSFODNN7EXAMPLE" + /> +
+
+ + setSourceS3Config(prev => ({ ...prev, secretAccessKey: e.target.value }))} + placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + /> +
+
+ + setSourceS3Config(prev => ({ ...prev, region: e.target.value }))} + placeholder="us-east-1" + /> +
+
+ + setSourceS3Config(prev => ({ ...prev, endpointUrl: e.target.value }))} + placeholder="https://s3.amazonaws.com (optional)" + /> +
+
+ +
+ +
+

Migration Settings

+
+
+ + setPgToS3Config(prev => ({ ...prev, s3Bucket: e.target.value }))} + placeholder="my-s3-bucket" + /> +
+
+ + setPgToS3Config(prev => ({ ...prev, s3Prefix: e.target.value }))} + placeholder="backups/" + /> +
+
+ +
+
+ + +
+
+
+ +
+ +
+
+ )} + + {/* ==================== Migration Tab ==================== */} + {activeTab === 'migration' && ( +
+

🚀 Migration Status

+ +
+

Migration Options

+
+ + + + +
+ + +
+ +
+ + setMigrationOptions({...migrationOptions, maxConcurrent: parseInt(e.target.value) || 5})} + /> +
+ +
+ + setMigrationOptions({...migrationOptions, includePatterns: e.target.value})} + placeholder="*.csv, *.json, important/*" + /> +
+ +
+ + setMigrationOptions({...migrationOptions, excludePatterns: e.target.value})} + placeholder="*.tmp, *.log, temp/*" + /> +
+
+
+ +
+ + +
+ + {postgresTables.length > 0 && ( +
+

Migration Statistics

+
+
+
Total Tables
+
{postgresTables.length}
+
+
+
Total Rows
+
{totalRows.toLocaleString()}
+
+
+
Selected Tables
+
{selectedTables.length}
+
+
+
+ )} + + {sourceObjects.length > 0 && ( +
+

Migration Statistics

+
+
+
Total Objects
+
{sourceObjects.length}
+
+
+
Total Size
+
{formatFileSize(totalSize)}
+
+
+
Selected Objects
+
{selectedObjects.length}
+
+
+
+ )} + + {migrations.length > 0 && ( +
+

Previous Migrations ({migrations.length})

+
+ {migrations.map((migration, index) => ( +
+
+ {migration.id} + + {migration.status} + +
+
+

Status: {migration.status}

+

Started: {migration.started_at ? new Date(migration.started_at * 1000).toLocaleString() : 'N/A'}

+
+
+ + {migration.status === 'running' && ( + + )} +
+
+ ))} +
+
+ )} + + {activeMigration && migrationLogs.length > 0 && ( +
+

Migration Logs ({activeMigration})

+
+ {migrationLogs.map((log, index) => ( +
+ + {log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : ''} + + {log.message} +
+ ))} +
+
+ )} +
+ )} + + {/* ==================== Environment Tab ==================== */} + {activeTab === 'environment' && ( +
+

⚙️ Environment Variables Management

+ +
+ + + +
+ +
+

Current Configuration

+
+
+

🐘 PostgreSQL Source

+
{formatPostgresConfig(postgresConfig)}
+
+
+

🐘 PostgreSQL Destination

+
{formatPostgresConfig(destPostgresConfig)}
+
+
+

📤 Source S3

+
{formatS3Config(sourceS3Config, 'source')}
+
+
+

📥 Destination S3

+
{formatS3Config(destS3Config, 'destination')}
+
+
+
+ +
+ + + +
+ +
+

Current Environment Variables ({Object.keys(currentEnv).length})

+ {Object.keys(currentEnv).length > 0 ? ( +
{formatEnvVars(currentEnv, envFormat)}
+ ) : ( +

No environment variables found. Test connections or inject environment first.

+ )} +
+
+ )} + + {/* Status Bar */} +
+ {loading &&
⏳ Processing...
} +
+ {selectedSourceBucket && 📤 Source: {selectedSourceBucket}} + {selectedDestBucket && 📥 Destination: {selectedDestBucket}} + {selectedPostgresDb && 🐘 DB: {selectedPostgresDb}} + {selectedObjects.length > 0 && 📁 {selectedObjects.length} objects selected} + {selectedTables.length > 0 && 📊 {selectedTables.length} tables selected} + {activeMigration && 🚀 Active migration: {activeMigration}} + ⚡ {Object.keys(currentEnv).length} env vars +
+
+
+ + {/* Progress Modal */} + {renderProgressModal()} + + {/* Progress History */} + {renderProgressHistory()} +
+ ); +} + +export default App; \ No newline at end of file diff --git a/api.js b/api.js new file mode 100644 index 0000000..801dbce --- /dev/null +++ b/api.js @@ -0,0 +1,1153 @@ +// API service for Unified Migration Tool with Environment Variables +const API_URL = ''; + +// ============================================================================ +// PostgreSQL API +// ============================================================================ + +export const postgresApi = { + // ==================== PostgreSQL Connection ==================== + + // Test PostgreSQL connection + testConnection: async (options = {}) => { + const { + useEnvVars = false, + uri, + host, + user, + password, + database, + port = 5432 + } = options; + + const body = { + use_env_vars: useEnvVars + }; + + if (uri) { + body.uri = uri; + } else if (host && user && password && database) { + body.uri = `postgresql://${user}:${password}@${host}:${port}/${database}`; + } + + const response = await fetch(`${API_URL}/api/postgres/test-connection`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return response.json(); + }, + + // Get PostgreSQL schemas + getSchemas: async (uri) => { + const response = await fetch(`${API_URL}/api/postgres/get-schemas`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uri }) + }); + return response.json(); + }, + + // Get PostgreSQL tables + getTables: async (uri, schema = '') => { + const response = await fetch(`${API_URL}/api/postgres/get-tables`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uri, schema }) + }); + return response.json(); + }, + + // Get table counts + getTableCounts: async (uri, schema = '') => { + const response = await fetch(`${API_URL}/api/postgres/get-table-counts`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uri, schema }) + }); + return response.json(); + }, + + // Parse PostgreSQL URI + parseUri: async (uri) => { + const response = await fetch(`${API_URL}/api/postgres/parse-uri`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uri }) + }); + return response.json(); + }, + + // Start PostgreSQL to PostgreSQL migration + startMigration: async (sourceUri, destUri, schemas = null, tables = null) => { + const response = await fetch(`${API_URL}/api/postgres/start-migration`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source_uri: sourceUri, + dest_uri: destUri, + schemas, + tables + }) + }); + return response.json(); + }, + + // Get migration status + getMigrationStatus: async (migrationId) => { + const response = await fetch(`${API_URL}/api/postgres/migration-status/${migrationId}`); + return response.json(); + }, + + // List all migrations + listMigrations: async () => { + const response = await fetch(`${API_URL}/api/postgres/list-migrations`); + return response.json(); + } +}; + + +// ============================================================================ +// S3 to S3 API +// ============================================================================ + +export const s3Api = { + // ==================== Source S3 Connection ==================== + + // Test source S3 connection + testSourceConnection: async (options = {}) => { + const { + useEnvVars = false, + accessKeyId, + secretAccessKey, + region = 'us-east-1', + endpointUrl, + sessionToken + } = options; + + const body = { + use_env_vars: useEnvVars, + access_key_id: accessKeyId, + secret_access_key: secretAccessKey, + region, + endpoint_url: endpointUrl, + session_token: sessionToken + }; + + const response = await fetch(`${API_URL}/api/s3-source/test-connection`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return response.json(); + }, + + // Test destination S3 connection + testDestinationConnection: async (options = {}) => { + const { + useEnvVars = false, + accessKeyId, + secretAccessKey, + region = 'us-east-1', + endpointUrl, + sessionToken + } = options; + + const body = { + use_env_vars: useEnvVars, + access_key_id: accessKeyId, + secret_access_key: secretAccessKey, + region, + endpoint_url: endpointUrl, + session_token: sessionToken + }; + + const response = await fetch(`${API_URL}/api/s3-destination/test-connection`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return response.json(); + }, + + // List source buckets + listSourceBuckets: async (accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl = null, sessionToken = null) => { + const response = await fetch(`${API_URL}/api/s3-source/list-buckets`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + access_key_id: accessKeyId, + secret_access_key: secretAccessKey, + region, + endpoint_url: endpointUrl, + session_token: sessionToken + }) + }); + return response.json(); + }, + + // List destination buckets + listDestinationBuckets: async (accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl = null, sessionToken = null) => { + const response = await fetch(`${API_URL}/api/s3-destination/list-buckets`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + access_key_id: accessKeyId, + secret_access_key: secretAccessKey, + region, + endpoint_url: endpointUrl, + session_token: sessionToken + }) + }); + return response.json(); + }, + + // List objects in a bucket + listObjects: async (bucket, prefix = '', isSource = true, credentials = {}) => { + const body = { + bucket, + prefix, + is_source: isSource, + access_key_id: credentials.accessKeyId, + secret_access_key: credentials.secretAccessKey, + region: credentials.region || 'us-east-1', + endpoint_url: credentials.endpointUrl, + session_token: credentials.sessionToken, + max_keys: credentials.maxKeys || 1000 + }; + + const response = await fetch(`${API_URL}/api/s3/list-objects`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return response.json(); + }, + + // Create bucket in destination + createBucket: async (bucket, region = 'us-east-1', options = {}) => { + const body = { + bucket, + region, + access_key_id: options.accessKeyId, + secret_access_key: options.secretAccessKey, + endpoint_url: options.endpointUrl, + session_token: options.sessionToken + }; + + const response = await fetch(`${API_URL}/api/s3-destination/create-bucket`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return response.json(); + }, + + // Migrate single object + migrateObject: async (sourceBucket, sourceKey, destBucket, destKey = null, options = {}) => { + const body = { + source_bucket: sourceBucket, + source_key: sourceKey, + dest_bucket: destBucket, + dest_key: destKey || sourceKey, + source_access_key_id: options.sourceAccessKeyId, + source_secret_access_key: options.sourceSecretAccessKey, + source_region: options.sourceRegion || 'us-east-1', + source_endpoint_url: options.sourceEndpointUrl, + source_session_token: options.sourceSessionToken, + dest_access_key_id: options.destAccessKeyId, + dest_secret_access_key: options.destSecretAccessKey, + dest_region: options.destRegion || 'us-east-1', + dest_endpoint_url: options.destEndpointUrl, + dest_session_token: options.destSessionToken, + preserve_metadata: options.preserveMetadata !== undefined ? options.preserveMetadata : true, + storage_class: options.storageClass || 'STANDARD' + }; + + const response = await fetch(`${API_URL}/api/s3/migrate-object`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return response.json(); + }, + + // Migrate multiple objects in batch + migrateBatch: async (objects, sourceBucket, destBucket, options = {}) => { + const body = { + objects, + source_bucket: sourceBucket, + dest_bucket: destBucket, + source_access_key_id: options.sourceAccessKeyId, + source_secret_access_key: options.sourceSecretAccessKey, + source_region: options.sourceRegion || 'us-east-1', + source_endpoint_url: options.sourceEndpointUrl, + source_session_token: options.sourceSessionToken, + dest_access_key_id: options.destAccessKeyId, + dest_secret_access_key: options.destSecretAccessKey, + dest_region: options.destRegion || 'us-east-1', + dest_endpoint_url: options.destEndpointUrl, + dest_session_token: options.destSessionToken, + preserve_metadata: options.preserveMetadata !== undefined ? options.preserveMetadata : true, + storage_class: options.storageClass || 'STANDARD', + max_concurrent: options.maxConcurrent || 5 + }; + + const response = await fetch(`${API_URL}/api/s3/migrate-batch`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return response.json(); + }, + + // Start full S3 to S3 migration + startMigration: async (sourceBucket, destBucket, prefix = '', options = {}) => { + const body = { + source_bucket: sourceBucket, + dest_bucket: destBucket, + prefix, + source_access_key_id: options.sourceAccessKeyId, + source_secret_access_key: options.sourceSecretAccessKey, + source_region: options.sourceRegion || 'us-east-1', + source_endpoint_url: options.sourceEndpointUrl, + source_session_token: options.sourceSessionToken, + dest_access_key_id: options.destAccessKeyId, + dest_secret_access_key: options.destSecretAccessKey, + dest_region: options.destRegion || 'us-east-1', + dest_endpoint_url: options.destEndpointUrl, + dest_session_token: options.destSessionToken, + include_patterns: options.includePatterns, + exclude_patterns: options.excludePatterns, + preserve_metadata: options.preserveMetadata !== undefined ? options.preserveMetadata : true, + storage_class: options.storageClass || 'STANDARD', + create_dest_bucket: options.createDestBucket !== undefined ? options.createDestBucket : true, + max_concurrent: options.maxConcurrent || 5 + }; + + const response = await fetch(`${API_URL}/api/s3/start-migration`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return response.json(); + }, + + // Get migration status + getMigrationStatus: async (migrationId) => { + const response = await fetch(`${API_URL}/api/s3/migration-status/${migrationId}`); + return response.json(); + }, + + // List all migrations + listMigrations: async () => { + const response = await fetch(`${API_URL}/api/s3/list-migrations`); + return response.json(); + }, + + // Cancel migration + cancelMigration: async (migrationId) => { + const response = await fetch(`${API_URL}/api/s3/cancel-migration/${migrationId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + return response.json(); + }, + + // Parse S3 URI + parseUri: async (s3Uri) => { + const response = await fetch(`${API_URL}/api/s3/parse-uri`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ s3_uri: s3Uri }) + }); + return response.json(); + }, + + // Generate presigned URL + generatePresignedUrl: async (bucket, key, isSource = true, expiration = 3600, credentials = {}) => { + const body = { + bucket, + key, + is_source: isSource, + expiration, + access_key_id: credentials.accessKeyId, + secret_access_key: credentials.secretAccessKey, + region: credentials.region || 'us-east-1', + endpoint_url: credentials.endpointUrl, + session_token: credentials.sessionToken + }; + + const response = await fetch(`${API_URL}/api/s3/generate-presigned-url`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return response.json(); + } +}; + + +// ============================================================================ +// PostgreSQL to S3 API +// ============================================================================ + +export const postgresToS3Api = { + // Test PostgreSQL connection + testPostgresConnection: async (uri) => { + const response = await fetch(`${API_URL}/api/postgres-s3/test-postgres-connection`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uri }) + }); + return response.json(); + }, + + // Test S3 connection + testS3Connection: async (accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl = null) => { + const response = await fetch(`${API_URL}/api/postgres-s3/test-s3-connection`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + access_key_id: accessKeyId, + secret_access_key: secretAccessKey, + region, + endpoint_url: endpointUrl + }) + }); + return response.json(); + }, + + // Get PostgreSQL schemas + getSchemas: async (uri) => { + const response = await fetch(`${API_URL}/api/postgres-s3/get-schemas`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uri }) + }); + return response.json(); + }, + + // Get PostgreSQL tables + getTables: async (uri, schema = '') => { + const response = await fetch(`${API_URL}/api/postgres-s3/get-tables`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uri, schema }) + }); + return response.json(); + }, + + // Export single table to S3 + exportTable: async (postgresUri, schema, table, s3Bucket, s3Key, options = {}) => { + const body = { + postgres_uri: postgresUri, + schema, + table, + s3_bucket: s3Bucket, + s3_key: s3Key, + compress: options.compress !== undefined ? options.compress : true, + format: options.format || 'csv', + access_key_id: options.accessKeyId, + secret_access_key: options.secretAccessKey, + region: options.region || 'us-east-1', + endpoint_url: options.endpointUrl + }; + + const response = await fetch(`${API_URL}/api/postgres-s3/export-table`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return response.json(); + }, + + // Start full PostgreSQL to S3 migration + startMigration: async (postgresUri, s3Bucket, s3Prefix = '', options = {}) => { + const body = { + postgres_uri: postgresUri, + s3_bucket: s3Bucket, + s3_prefix: s3Prefix, + schemas: options.schemas, + tables: options.tables, + compress: options.compress !== undefined ? options.compress : true, + format: options.format || 'csv', + access_key_id: options.accessKeyId, + secret_access_key: options.secretAccessKey, + region: options.region || 'us-east-1', + endpoint_url: options.endpointUrl + }; + + const response = await fetch(`${API_URL}/api/postgres-s3/start-migration`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + return response.json(); + }, + + // Get migration status + getMigrationStatus: async (migrationId) => { + const response = await fetch(`${API_URL}/api/postgres-s3/migration-status/${migrationId}`); + return response.json(); + }, + + // List all migrations + listMigrations: async () => { + const response = await fetch(`${API_URL}/api/postgres-s3/list-migrations`); + return response.json(); + } +}; + + +// ============================================================================ +// Common Environment and Utility Functions +// ============================================================================ + +// Inject environment variables +export const injectEnv = async (envVars) => { + const response = await fetch(`${API_URL}/api/inject-env`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ environment_variables: envVars }) + }); + return response.json(); +}; + +// Get current environment +export const getCurrentEnv = async () => { + const response = await fetch(`${API_URL}/api/get-current-env`); + return response.json(); +}; + +// Health check +export const healthCheck = async () => { + const response = await fetch(`${API_URL}/api/health`); + return response.json(); +}; + + +// ============================================================================ +// Helper Functions +// ============================================================================ + +// Copy to clipboard +export const copyToClipboard = async (text) => { + try { + await navigator.clipboard.writeText(text); + return true; + } catch (err) { + console.error('Failed to copy:', err); + return false; + } +}; + +// Format environment variables +export const formatEnvVars = (envVars, format = 'dotenv') => { + if (!envVars) return ''; + + switch(format) { + case 'dotenv': + return Object.entries(envVars) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); + + case 'json': + return JSON.stringify(envVars, null, 2); + + case 'shell_export': + return Object.entries(envVars) + .map(([key, value]) => `export ${key}="${value}"`) + .join('\n'); + + case 'docker_env': + return Object.entries(envVars) + .map(([key, value]) => `-e ${key}="${value}"`) + .join(' '); + + case 'docker_compose': + return Object.entries(envVars) + .map(([key, value]) => ` ${key}: ${value}`) + .join('\n'); + + default: + return Object.entries(envVars) + .map(([key, value]) => `${key}: ${value}`) + .join('\n'); + } +}; + +// Format file size +export const formatFileSize = (bytes) => { + if (bytes === 0 || !bytes) return '0 Bytes'; + if (typeof bytes !== 'number') bytes = parseInt(bytes); + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +}; + +// Format PostgreSQL configuration for display +export const formatPostgresConfig = (config) => { + if (!config) return ''; + + const details = [ + `📍 PostgreSQL Configuration:`, + `━━━━━━━━━━━━━━━━━━━━━━━━━━━`, + `Host: ${config.host || 'Not set'}`, + `Port: ${config.port || 5432}`, + `User: ${config.user || 'Not set'}`, + `Database: ${config.database || 'Not set'}`, + `━━━━━━━━━━━━━━━━━━━━━━━━━━━` + ]; + + if (config.host && config.user && config.password) { + details.push(`✅ Credentials: Configured`); + } else { + details.push(`❌ Credentials: Not configured`); + } + + return details.join('\n'); +}; + +// Format S3 configuration for display +export const formatS3Config = (config, type = 'source') => { + if (!config) return ''; + + const details = [ + `📍 ${type.toUpperCase()} S3 Configuration:`, + `━━━━━━━━━━━━━━━━━━━━━━━━━━━`, + `Endpoint: ${config.endpoint_url || 'AWS S3 (default)'}`, + `Region: ${config.region || 'us-east-1'}`, + `Bucket: ${config.bucket || 'Not set'}`, + `━━━━━━━━━━━━━━━━━━━━━━━━━━━` + ]; + + if (config.access_key_id && config.secret_access_key) { + details.push(`✅ Credentials: Configured`); + } else { + details.push(`❌ Credentials: Not configured`); + } + + return details.join('\n'); +}; + +// Format migration details for display +export const formatMigrationDetails = (migrationData) => { + if (!migrationData) return ''; + + const details = [ + `🔄 Migration ID: ${migrationData.migration_id || migrationData.id || 'N/A'}`, + `━━━━━━━━━━━━━━━━━━━━━━━━━━━`, + `Status: ${migrationData.status || 'N/A'}`, + `Started: ${migrationData.started_at ? new Date(migrationData.started_at * 1000).toLocaleString() : 'N/A'}` + ]; + + if (migrationData.stats) { + details.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━`); + details.push(`📊 Statistics:`); + Object.entries(migrationData.stats).forEach(([key, value]) => { + details.push(` ${key}: ${value}`); + }); + } + + if (migrationData.message) { + details.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━`); + details.push(`📝 Message: ${migrationData.message}`); + } + + return details.join('\n'); +}; + +// Extract PostgreSQL info from URI +export const extractPostgresInfo = (uri) => { + try { + const match = uri.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/); + if (match) { + return { + user: match[1], + password: match[2], + host: match[3], + port: parseInt(match[4]), + database: match[5], + isValid: true + }; + } + return { isValid: false }; + } catch (error) { + console.error('Error extracting PostgreSQL info:', error); + return { isValid: false, error: error.message }; + } +}; +// ============================================================================ +// Stream Progress API (Server-Sent Events) +// ============================================================================ + +/** + * Stream migration progress using Server-Sent Events + * @param {string} migrationId - The migration ID to track + * @param {string} type - Migration type: 's3', 'postgres', or 'postgres-s3' + * @param {function} onProgress - Callback function for progress updates + * @param {function} onComplete - Callback function when migration completes + * @param {function} onError - Callback function for errors + * @returns {EventSource} - The EventSource object for the stream + */ +export const streamProgress = (migrationId, type = 's3', onProgress, onComplete, onError) => { + const url = new URL(`${API_URL}/api/stream-progress/${migrationId}`); + url.searchParams.append('type', type); + + const eventSource = new EventSource(url.toString()); + + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + + // التحقق من الإكمال بعدة طرق + if (data.type === 'completion' || + data.status === 'completed' || + data.status === 'success' || + data.success === true) { + if (onComplete) onComplete(data); + eventSource.close(); + } + // التحقق من الخطأ بعدة طرق + else if (data.type === 'error' || + data.status === 'failed' || + data.status === 'error' || + data.error) { + if (onError) onError(data); + eventSource.close(); + } + else { + if (onProgress) onProgress(data); + } + } catch (error) { + console.error('Error parsing stream data:', error); + if (onError) onError({ error: error.message }); + } + }; + + eventSource.onerror = (error) => { + console.error('EventSource error:', error); + if (onError) onError({ error: 'Connection error', details: error }); + eventSource.close(); + }; + + return eventSource; +}; + +/** + * Stream progress with Promise-based wrapper (easier to use with async/await) + * @param {string} migrationId - The migration ID to track + * @param {string} type - Migration type + * @param {function} onProgress - Optional progress callback + * @returns {Promise} - Promise that resolves when migration completes + */ +export const streamProgressAsync = (migrationId, type = 's3', onProgress = null) => { + return new Promise((resolve, reject) => { + const eventSource = streamProgress( + migrationId, + type, + (progress) => { + // تمرير التقدم إلى callback إذا وجد + if (onProgress) onProgress(progress); + }, + (completion) => { + // إكمال الوعد عند الانتهاء + resolve(completion); + eventSource.close(); + }, + (error) => { + // رفض الوعد عند الخطأ + reject(error); + eventSource.close(); + } + ); + }); +}; + +/** + * Stream progress with automatic reconnection + * @param {string} migrationId - The migration ID to track + * @param {string} type - Migration type + * @param {object} options - Options for the stream + * @returns {object} - Control object with start, stop, and pause methods + */ +export const createProgressStream = (migrationId, type = 's3', options = {}) => { + const { + onProgress = () => {}, + onComplete = () => {}, + onError = () => {}, + reconnectInterval = 3000, // إعادة الاتصال كل 3 ثواني + maxReconnectAttempts = 10 // أقصى عدد محاولات إعادة الاتصال + } = options; + + let eventSource = null; + let reconnectAttempts = 0; + let isActive = true; + let reconnectTimer = null; + + const connect = () => { + if (!isActive) return; + + // إغلاق الاتصال القديم إذا كان موجوداً + if (eventSource) { + eventSource.close(); + } + + // إنشاء اتصال جديد + eventSource = streamProgress( + migrationId, + type, + (progress) => { + // إعادة تعيين محاولات إعادة الاتصال عند استلام تقدم + reconnectAttempts = 0; + onProgress(progress); + }, + (completion) => { + isActive = false; + onComplete(completion); + if (eventSource) eventSource.close(); + }, + (error) => { + onError(error); + + // محاولة إعادة الاتصال إذا كان لا يزال نشطاً + if (isActive && reconnectAttempts < maxReconnectAttempts) { + reconnectAttempts++; + console.log(`Reconnecting... Attempt ${reconnectAttempts}/${maxReconnectAttempts}`); + + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(connect, reconnectInterval); + } else if (reconnectAttempts >= maxReconnectAttempts) { + isActive = false; + onError({ error: 'Max reconnection attempts reached' }); + } + } + ); + }; + + // بدء الاتصال + connect(); + + // إرجاع كائن التحكم + return { + // إيقاف البث نهائياً + stop: () => { + isActive = false; + if (reconnectTimer) clearTimeout(reconnectTimer); + if (eventSource) { + eventSource.close(); + eventSource = null; + } + }, + + // إيقاف البث مؤقتاً + pause: () => { + isActive = false; + if (reconnectTimer) clearTimeout(reconnectTimer); + if (eventSource) { + eventSource.close(); + eventSource = null; + } + }, + + // استئناف البث + resume: () => { + if (!isActive) { + isActive = true; + reconnectAttempts = 0; + connect(); + } + }, + + // التحقق من حالة الاتصال + isConnected: () => eventSource !== null && eventSource.readyState === EventSource.OPEN, + + // الحصول على حالة الاتصال + getState: () => { + if (!eventSource) return 'disconnected'; + switch (eventSource.readyState) { + case EventSource.CONNECTING: return 'connecting'; + case EventSource.OPEN: return 'connected'; + case EventSource.CLOSED: return 'closed'; + default: return 'unknown'; + } + } + }; +}; + +/** + * Format progress data for display + * @param {object} progress - Progress data from stream + * @returns {string} - Formatted progress string + */ +export const formatProgressDisplay = (progress) => { + if (!progress) return 'No progress data'; + + const lines = []; + + // إضافة عنوان + if (progress.migration_id) { + lines.push(`🚀 Migration: ${progress.migration_id}`); + } + + // إضافة الحالة + if (progress.status) { + const statusEmoji = { + 'running': '🟢', + 'completed': '✅', + 'failed': '❌', + 'cancelled': '⏹️' + }[progress.status] || '📊'; + + lines.push(`${statusEmoji} Status: ${progress.status}`); + } + + // إضافة النسبة المئوية + if (progress.percentage !== undefined) { + lines.push(`📊 Progress: ${progress.percentage.toFixed(1)}%`); + } else if (progress.percentages?.size !== undefined) { + lines.push(`📊 Progress: ${progress.percentages.size.toFixed(1)}%`); + } + + // إضافة إحصائيات S3 + if (progress.processed_objects !== undefined && progress.total_objects !== undefined) { + lines.push(`📦 Objects: ${progress.processed_objects}/${progress.total_objects}`); + } + + // إضافة إحصائيات PostgreSQL + if (progress.processed?.tables !== undefined && progress.total?.tables !== undefined) { + lines.push(`📋 Tables: ${progress.processed.tables}/${progress.total.tables}`); + } + + // إضافة حجم البيانات + if (progress.processed_size_formatted && progress.total_size_formatted) { + lines.push(`💾 Size: ${progress.processed_size_formatted}/${progress.total_size_formatted}`); + } else if (progress.processed?.size_formatted && progress.total?.size_formatted) { + lines.push(`💾 Size: ${progress.processed.size_formatted}/${progress.total.size_formatted}`); + } + + // إضافة السرعة + if (progress.current_speed_formatted) { + lines.push(`⚡ Speed: ${progress.current_speed_formatted}`); + } else if (progress.speed?.current_formatted) { + lines.push(`⚡ Speed: ${progress.speed.current_formatted}`); + } + + // إضافة الوقت + if (progress.elapsed_time_formatted) { + lines.push(`⏱️ Elapsed: ${progress.elapsed_time_formatted}`); + } + + if (progress.eta_formatted) { + lines.push(`⏳ ETA: ${progress.eta_formatted}`); + } else if (progress.time?.eta_formatted) { + lines.push(`⏳ ETA: ${progress.time.eta_formatted}`); + } + + // إضافة العنصر الحالي + if (progress.current_object) { + lines.push(`📄 Current: ${progress.current_object}`); + } else if (progress.current_table?.name) { + const ct = progress.current_table; + lines.push(`📄 Current Table: ${ct.name}`); + if (ct.size_formatted) { + lines.push(` Size: ${ct.size_formatted}`); + } + if (ct.rows_percentage !== undefined) { + lines.push(` Rows: ${ct.rows_processed}/${ct.rows} (${ct.rows_percentage}%)`); + } + } + + return lines.join('\n'); +}; + +/** + * Create a progress bar string + * @param {number} percentage - Progress percentage (0-100) + * @param {number} width - Width of the progress bar + * @returns {string} - Progress bar string + */ +export const createProgressBar = (percentage, width = 30) => { + const filled = Math.floor(width * (percentage / 100)); + const empty = width - filled; + const bar = '█'.repeat(filled) + '░'.repeat(empty); + return `[${bar}] ${percentage.toFixed(1)}%`; +}; + +/** + * Estimate remaining time based on progress and speed + * @param {object} progress - Progress data + * @returns {string} - Estimated remaining time + */ +export const estimateRemainingTime = (progress) => { + if (!progress) return 'Unknown'; + + // استخدام ETA من البيانات إذا كان موجوداً + if (progress.eta_formatted) return progress.eta_formatted; + if (progress.time?.eta_formatted) return progress.time.eta_formatted; + + // حساب يدوي إذا لم يكن موجوداً + const remaining = progress.remaining_size || progress.remaining?.size; + const speed = progress.current_speed || progress.speed?.current; + + if (remaining && speed && speed > 0) { + const seconds = remaining / speed; + return formatTimeEstimate(seconds); + } + + return 'Calculating...'; +}; + +/** + * Format time estimate + * @param {number} seconds - Time in seconds + * @returns {string} - Formatted time string + */ +export const formatTimeEstimate = (seconds) => { + if (seconds < 60) { + return `${Math.round(seconds)}s`; + } else if (seconds < 3600) { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.round(seconds % 60); + return `${minutes}m ${remainingSeconds}s`; + } else if (seconds < 86400) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + return `${hours}h ${minutes}m`; + } else { + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + return `${days}d ${hours}h`; + } +}; + + +// ============================================================================ +// إضافة الدوال إلى التصدير +// ============================================================================ + +// Extract S3 info from URI +export const extractS3Info = (s3Uri) => { + try { + if (s3Uri.startsWith('s3://')) { + const uri = s3Uri.substring(5); + const parts = uri.split('/', 1); + const bucket = parts[0]; + const key = uri.substring(bucket.length + 1); + return { + bucket, + key: key || '', + fullUri: s3Uri, + isValid: true + }; + } + return { isValid: false }; + } catch (error) { + console.error('Error parsing S3 URI:', error); + return { isValid: false, error: error.message }; + } +}; + +// Build PostgreSQL connection string +export const buildPostgresConnectionString = (host, database, user, port = 5432) => { + if (!host || !database) return ''; + return `postgresql://${user ? user + '@' : ''}${host}:${port}/${database}`; +}; + +// Build S3 URL +export const buildS3Url = (bucket, key, endpointUrl = null) => { + if (!bucket) return ''; + if (!key) return `s3://${bucket}`; + + if (endpointUrl) { + return `${endpointUrl}/${bucket}/${key}`; + } + return `s3://${bucket}/${key}`; +}; + +// Group tables by schema +export const groupTablesBySchema = (tables) => { + const groups = {}; + + tables.forEach(table => { + const schema = table.schema || 'public'; + + if (!groups[schema]) { + groups[schema] = { + count: 0, + tables: [] + }; + } + + groups[schema].count++; + groups[schema].tables.push(table); + }); + + return groups; +}; + +// Group objects by prefix +export const groupObjectsByPrefix = (objects, depth = 1) => { + const groups = {}; + + objects.forEach(obj => { + const parts = obj.key.split('/'); + let prefix = ''; + + for (let i = 0; i < Math.min(depth, parts.length - 1); i++) { + prefix += parts[i] + '/'; + } + + if (!prefix) prefix = '/'; + + if (!groups[prefix]) { + groups[prefix] = { + count: 0, + totalSize: 0, + objects: [] + }; + } + + groups[prefix].count++; + groups[prefix].totalSize += obj.size; + groups[prefix].objects.push(obj); + }); + + return groups; +}; + + +// ============================================================================ +// Export all APIs +// ============================================================================ + +export default { + postgresApi, + s3Api, + postgresToS3Api, + injectEnv, + getCurrentEnv, + healthCheck, + copyToClipboard, + formatEnvVars, + formatFileSize, + formatPostgresConfig, + formatS3Config, + formatMigrationDetails, + extractPostgresInfo, + extractS3Info, + buildPostgresConnectionString, + buildS3Url, + groupTablesBySchema, + groupObjectsByPrefix, + streamProgress, + streamProgressAsync, + createProgressStream, + formatProgressDisplay, + createProgressBar, + estimateRemainingTime, + formatTimeEstimate +}; \ No newline at end of file diff --git a/index.js b/index.js index b8fe2de..5851d84 100644 --- a/index.js +++ b/index.js @@ -1,3366 +1,10 @@ -const API_BASE = ""; - -// ============================================================================ -// تعريف دوال التدفق (Stream) محلياً - حل المشكلة -// ============================================================================ - -function createProgressStream(migrationId, type = 's3', options = {}) { - const { - onProgress = () => {}, - onComplete = () => {}, - onError = () => {}, - reconnectInterval = 3000, - maxReconnectAttempts = 5 - } = options; - - let eventSource = null; - let reconnectAttempts = 0; - let isActive = true; - let reconnectTimer = null; - - const connect = () => { - if (!isActive) return; - - if (eventSource) { - eventSource.close(); - } - - try { - const safeMigrationId = encodeURIComponent(migrationId); - const safeType = encodeURIComponent(type); - const url = `${API_BASE}/api/stream-progress/${safeMigrationId}?type=${safeType}`; - eventSource = new EventSource(url); - - eventSource.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - - // التحقق الموحد من الإكمال - if (data.type === 'completion' || - data.status === 'completed' || - data.status === 'success' || - data.success === true) { - onComplete(data); - if (eventSource) eventSource.close(); - } - // التحقق الموحد من الخطأ - else if (data.type === 'error' || - data.status === 'failed' || - data.status === 'error' || - data.error) { - onError(data); - if (eventSource) eventSource.close(); - } - else { - onProgress(data); - } - } catch (error) { - console.error('Error parsing stream data:', error); - } - }; - - eventSource.onerror = (error) => { - console.error('EventSource error:', error); - - if (isActive && reconnectAttempts < maxReconnectAttempts) { - reconnectAttempts++; - console.log(`Reconnecting... Attempt ${reconnectAttempts}/${maxReconnectAttempts}`); - - if (reconnectTimer) clearTimeout(reconnectTimer); - reconnectTimer = setTimeout(connect, reconnectInterval); - } else if (reconnectAttempts >= maxReconnectAttempts) { - onError({ error: 'Max reconnection attempts reached' }); - } - - if (eventSource) { - eventSource.close(); - eventSource = null; - } - }; - - eventSource.onopen = () => { - console.log(`Stream connected for ${migrationId}`); - reconnectAttempts = 0; - }; - - } catch (error) { - console.error('Error creating EventSource:', error); - onError({ error: error.message }); - } - }; - - connect(); - - return { - stop: () => { - isActive = false; - if (reconnectTimer) clearTimeout(reconnectTimer); - if (eventSource) { - eventSource.close(); - eventSource = null; - } - }, - pause: () => { - isActive = false; - if (reconnectTimer) clearTimeout(reconnectTimer); - if (eventSource) { - eventSource.close(); - eventSource = null; - } - }, - resume: () => { - if (!isActive) { - isActive = true; - reconnectAttempts = 0; - connect(); - } - } - }; -} - -function formatProgressDisplay(progress) { - if (!progress) return 'Connecting...'; - - const lines = []; - - if (progress.migration_id) { - lines.push(`🚀 Migration: ${progress.migration_id}`); - } - - if (progress.status) { - const statusEmoji = { - 'running': '🟢', - 'completed': '✅', - 'failed': '❌', - 'cancelled': '⏹️', - 'success': '✅', - 'error': '❌' - }[progress.status] || '📊'; - - lines.push(`${statusEmoji} Status: ${progress.status}`); - } - - if (progress.percentage !== undefined) { - lines.push(`📊 Progress: ${progress.percentage.toFixed(1)}%`); - } - - if (progress.processed_objects !== undefined && progress.total_objects !== undefined) { - lines.push(`📦 Objects: ${progress.processed_objects}/${progress.total_objects}`); - } - - if (progress.processed?.tables !== undefined && progress.total?.tables !== undefined) { - lines.push(`📋 Tables: ${progress.processed.tables}/${progress.total.tables}`); - } - - if (progress.processed_size_formatted && progress.total_size_formatted) { - lines.push(`💾 Size: ${progress.processed_size_formatted}/${progress.total_size_formatted}`); - } - - if (progress.current_speed_formatted) { - lines.push(`⚡ Speed: ${progress.current_speed_formatted}`); - } - - if (progress.elapsed_time_formatted) { - lines.push(`⏱️ Elapsed: ${progress.elapsed_time_formatted}`); - } - - if (progress.eta_formatted) { - lines.push(`⏳ ETA: ${progress.eta_formatted}`); - } - - if (progress.current_object) { - const objName = progress.current_object.length > 50 - ? '...' + progress.current_object.slice(-50) - : progress.current_object; - lines.push(`📄 Current: ${objName}`); - } - - return lines.join('\n'); -} -// Copy to clipboard -async function copyToClipboard(text) { - try { - await navigator.clipboard.writeText(text); - return true; - } catch (err) { - console.error('Failed to copy:', err); - return false; - } -} - -// Show notification -function showNotification(message, type = 'info') { - const notification = document.createElement('div'); - notification.className = `notification ${type}`; - notification.innerHTML = ` - ${type === 'success' ? '✅' : type === 'error' ? '❌' : type === 'warning' ? '⚠️' : 'ℹ️'} - ${message} - - `; - - document.body.appendChild(notification); - - setTimeout(() => { - if (notification.parentElement) { - notification.style.animation = 'slideOut 0.3s ease'; - setTimeout(() => notification.remove(), 300); - } - }, 3000); -} - -// Format environment variables -function formatEnvVars(envVars, format = 'dotenv') { - if (!envVars) return ''; - - switch(format) { - case 'dotenv': - return Object.entries(envVars) - .map(([key, value]) => `${key}=${value}`) - .join('\n'); - - case 'json': - return JSON.stringify(envVars, null, 2); - - case 'shell_export': - return Object.entries(envVars) - .map(([key, value]) => `export ${key}="${value}"`) - .join('\n'); - - case 'docker_env': - return Object.entries(envVars) - .map(([key, value]) => `-e ${key}="${value}"`) - .join(' '); - - case 'docker_compose': - return Object.entries(envVars) - .map(([key, value]) => ` ${key}: ${value}`) - .join('\n'); - - default: - return Object.entries(envVars) - .map(([key, value]) => `${key}: ${value}`) - .join('\n'); - } -} - -// Format file size -function formatFileSize(bytes) { - if (bytes === 0 || !bytes) return '0 Bytes'; - if (typeof bytes !== 'number') bytes = parseInt(bytes); - - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -} - -// Format PostgreSQL configuration for display -function formatPostgresConfig(config) { - if (!config) return ''; - - const details = [ - `📍 PostgreSQL Configuration:`, - `━━━━━━━━━━━━━━━━━━━━━━━━━━━`, - `Host: ${config.host || 'Not set'}`, - `Port: ${config.port || 5432}`, - `User: ${config.user || 'Not set'}`, - `Database: ${config.database || 'Not set'}`, - `━━━━━━━━━━━━━━━━━━━━━━━━━━━` - ]; - - if (config.host && config.user && config.password) { - details.push(`✅ Credentials: Configured`); - } else { - details.push(`❌ Credentials: Not configured`); - } - - return details.join('\n'); -} - -// Format S3 configuration for display -function formatS3Config(config, type = 'source') { - if (!config) return ''; - - const details = [ - `📍 ${type.toUpperCase()} S3 Configuration:`, - `━━━━━━━━━━━━━━━━━━━━━━━━━━━`, - `Endpoint: ${config.endpoint_url || 'AWS S3 (default)'}`, - `Region: ${config.region || 'us-east-1'}`, - `Bucket: ${config.bucket || 'Not set'}`, - `━━━━━━━━━━━━━━━━━━━━━━━━━━━` - ]; - - if (config.access_key_id && config.secret_access_key) { - details.push(`✅ Credentials: Configured`); - } else { - details.push(`❌ Credentials: Not configured`); - } - - return details.join('\n'); -} - -// Format migration details for display -function formatMigrationDetails(migrationData) { - if (!migrationData) return ''; - - const details = [ - `🔄 Migration ID: ${migrationData.migration_id || migrationData.id || 'N/A'}`, - `━━━━━━━━━━━━━━━━━━━━━━━━━━━`, - `Status: ${migrationData.status || 'N/A'}`, - `Started: ${migrationData.started_at ? new Date(migrationData.started_at * 1000).toLocaleString() : 'N/A'}` - ]; - - if (migrationData.stats) { - details.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━`); - details.push(`📊 Statistics:`); - Object.entries(migrationData.stats).forEach(([key, value]) => { - details.push(` ${key}: ${value}`); - }); - } - - if (migrationData.message) { - details.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━`); - details.push(`📝 Message: ${migrationData.message}`); - } - - return details.join('\n'); -} - -// Extract PostgreSQL info from URI -function extractPostgresInfo(uri) { - try { - const match = uri.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/); - if (match) { - return { - user: match[1], - password: match[2], - host: match[3], - port: parseInt(match[4]), - database: match[5], - isValid: true - }; - } - return { isValid: false }; - } catch (error) { - console.error('Error extracting PostgreSQL info:', error); - return { isValid: false, error: error.message }; - } -} - -// Extract S3 info from URI -function extractS3Info(s3Uri) { - try { - if (s3Uri.startsWith('s3://')) { - const uri = s3Uri.substring(5); - const parts = uri.split('/', 1); - const bucket = parts[0]; - const key = uri.substring(bucket.length + 1); - return { - bucket, - key: key || '', - fullUri: s3Uri, - isValid: true - }; - } - return { isValid: false }; - } catch (error) { - console.error('Error parsing S3 URI:', error); - return { isValid: false, error: error.message }; - } -} - -// Build PostgreSQL connection string -function buildPostgresConnectionString(host, database, user, port = 5432) { - if (!host || !database) return ''; - return `postgresql://${user ? user + '@' : ''}${host}:${port}/${database}`; -} - -// Build S3 URL -function buildS3Url(bucket, key, endpointUrl = null) { - if (!bucket) return ''; - if (!key) return `s3://${bucket}`; - - if (endpointUrl) { - return `${endpointUrl}/${bucket}/${key}`; - } - return `s3://${bucket}/${key}`; -} - -// Group tables by schema -function groupTablesBySchema(tables) { - const groups = {}; - - tables.forEach(table => { - const schema = table.schema || 'public'; - - if (!groups[schema]) { - groups[schema] = { - count: 0, - tables: [] - }; - } - - groups[schema].count++; - groups[schema].tables.push(table); - }); - - return groups; -} - -// Group objects by prefix -function groupObjectsByPrefix(objects, depth = 1) { - const groups = {}; - - objects.forEach(obj => { - const parts = obj.key.split('/'); - let prefix = ''; - - for (let i = 0; i < Math.min(depth, parts.length - 1); i++) { - prefix += parts[i] + '/'; - } - - if (!prefix) prefix = '/'; - - if (!groups[prefix]) { - groups[prefix] = { - count: 0, - totalSize: 0, - objects: [] - }; - } - - groups[prefix].count++; - groups[prefix].totalSize += obj.size; - groups[prefix].objects.push(obj); - }); - - return groups; -} - -// Estimate migration time -function estimateMigrationTime(totalItems, averageItemsPerSecond = 100) { - if (!totalItems) return 'Unknown'; - - const seconds = totalItems / averageItemsPerSecond; - - if (seconds < 60) return `${Math.ceil(seconds)} seconds`; - if (seconds < 3600) return `${Math.ceil(seconds / 60)} minutes`; - if (seconds < 86400) return `${(seconds / 3600).toFixed(1)} hours`; - return `${(seconds / 86400).toFixed(1)} days`; -} - -// ============================================================================ -// PostgreSQL API Functions -// ============================================================================ - -// Test PostgreSQL connection -async function testPostgresConnection(options = {}) { - const { - useEnvVars = false, - uri, - host, - user, - password, - database, - port = 5432 - } = options; - - const body = { - use_env_vars: useEnvVars - }; - - if (uri) { - body.uri = uri; - } else if (host && user && password && database) { - body.uri = `postgresql://${user}:${password}@${host}:${port}/${database}`; - } - - const r = await fetch(`${API_BASE}/api/postgres/test-connection`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// Get PostgreSQL schemas -async function getPostgresSchemas(options = {}) { - const { - useEnvVars = false, - uri, - host, - user, - password, - database, - port = 5432 - } = options; - - const body = { - use_env_vars: useEnvVars - }; - - // بناء الرابط إذا لم يتم توفيره - if (uri) { - body.uri = uri; - } else if (host && user && password && database) { - body.uri = `postgresql://${user}:${password}@${host}:${port}/${database}`; - } - - const r = await fetch(`${API_BASE}/api/postgres/get-schemas`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) // ✅ الآن يرسل الجسم كاملاً - }); - return r.json(); -} -// Get PostgreSQL tables -async function getPostgresTables(options = {}){ - const { - useEnvVars = false, - uri, - host, - user, - password, - database, - port = 5432, - schema = '' - } = options; - - const body = { - use_env_vars: useEnvVars - }; - - // بناء الرابط إذا لم يتم توفيره - if (uri) { - body.uri = uri; - } else if (host && user && password && database) { - body.uri = `postgresql://${user}:${password}@${host}:${port}/${database}`; - } - - const r = await fetch(`${API_BASE}/api/postgres/get-tables`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// Get PostgreSQL table counts -async function getPostgresTableCounts(uri, schema = '') { - const r = await fetch(`${API_BASE}/api/postgres/get-table-counts`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// Parse PostgreSQL URI -async function parsePostgresUri(uri) { - const r = await fetch(`${API_BASE}/api/postgres/parse-uri`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ uri }) - }); - return r.json(); -} - -// Start PostgreSQL to PostgreSQL migration -async function startPostgresMigration(sourceUri, destUri, schemas = null, tables = null) { - const r = await fetch(`${API_BASE}/api/postgres/start-migration`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - source_uri: sourceUri, - dest_uri: destUri, - schemas, - tables - }) - }); - return r.json(); -} - -// Get PostgreSQL migration status -async function getPostgresMigrationStatus(migrationId) { - const r = await fetch(`${API_BASE}/api/postgres/migration-status/${migrationId}`); - return r.json(); -} - -// List PostgreSQL migrations -async function listPostgresMigrations() { - const r = await fetch(`${API_BASE}/api/postgres/list-migrations`); - return r.json(); -} - -// ============================================================================ -// S3 API Functions -// ============================================================================ - -// Test source S3 connection -async function testSourceS3Connection(options = {}) { - const { - useEnvVars = false, - accessKeyId, - secretAccessKey, - region = 'us-east-1', - endpointUrl, - sessionToken - } = options; - - const body = { - use_env_vars: useEnvVars, - access_key_id: accessKeyId, - secret_access_key: secretAccessKey, - region, - endpoint_url: endpointUrl, - session_token: sessionToken - }; - - const r = await fetch(`${API_BASE}/api/s3-source/test-connection`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// Test destination S3 connection -async function testDestinationS3Connection(options = {}) { - const { - useEnvVars = false, - accessKeyId, - secretAccessKey, - region = 'us-east-1', - endpointUrl, - sessionToken - } = options; - - const body = { - use_env_vars: useEnvVars, - access_key_id: accessKeyId, - secret_access_key: secretAccessKey, - region, - endpoint_url: endpointUrl, - session_token: sessionToken - }; - - const r = await fetch(`${API_BASE}/api/s3-destination/test-connection`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// List source S3 buckets -async function listSourceS3Buckets(accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl = null, sessionToken = null) { - const r = await fetch(`${API_BASE}/api/s3-source/list-buckets`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - access_key_id: accessKeyId, - secret_access_key: secretAccessKey, - region, - endpoint_url: endpointUrl, - session_token: sessionToken - }) - }); - return r.json(); -} - -// List destination S3 buckets -async function listDestinationS3Buckets(accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl = null, sessionToken = null) { - const r = await fetch(`${API_BASE}/api/s3-destination/list-buckets`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - access_key_id: accessKeyId, - secret_access_key: secretAccessKey, - region, - endpoint_url: endpointUrl, - session_token: sessionToken - }) - }); - return r.json(); -} - -// List objects in S3 bucket -async function listS3Objects(bucket, prefix = '', isSource = true, credentials = {}) { - const body = { - bucket, - prefix, - is_source: isSource, - access_key_id: credentials.accessKeyId, - secret_access_key: credentials.secretAccessKey, - region: credentials.region || 'us-east-1', - endpoint_url: credentials.endpointUrl, - session_token: credentials.sessionToken, - max_keys: credentials.maxKeys || 1000 - }; - - const r = await fetch(`${API_BASE}/api/s3/list-objects`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// Create S3 bucket in destination -async function createS3Bucket(bucket, region = 'us-east-1', options = {}) { - const body = { - bucket, - region, - access_key_id: options.accessKeyId, - secret_access_key: options.secretAccessKey, - endpoint_url: options.endpointUrl, - session_token: options.sessionToken - }; - - const r = await fetch(`${API_BASE}/api/s3-destination/create-bucket`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// Migrate single S3 object -async function migrateS3Object(sourceBucket, sourceKey, destBucket, destKey = null, options = {}) { - const body = { - source_bucket: sourceBucket, - source_key: sourceKey, - dest_bucket: destBucket, - dest_key: destKey || sourceKey, - source_access_key_id: options.sourceAccessKeyId, - source_secret_access_key: options.sourceSecretAccessKey, - source_region: options.sourceRegion || 'us-east-1', - source_endpoint_url: options.sourceEndpointUrl, - source_session_token: options.sourceSessionToken, - dest_access_key_id: options.destAccessKeyId, - dest_secret_access_key: options.destSecretAccessKey, - dest_region: options.destRegion || 'us-east-1', - dest_endpoint_url: options.destEndpointUrl, - dest_session_token: options.destSessionToken, - preserve_metadata: options.preserveMetadata !== undefined ? options.preserveMetadata : true, - storage_class: options.storageClass || 'STANDARD' - }; - - const r = await fetch(`${API_BASE}/api/s3/migrate-object`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// Migrate multiple S3 objects in batch -async function migrateS3Batch(objects, sourceBucket, destBucket, options = {}) { - const body = { - objects, - source_bucket: sourceBucket, - dest_bucket: destBucket, - source_access_key_id: options.sourceAccessKeyId, - source_secret_access_key: options.sourceSecretAccessKey, - source_region: options.sourceRegion || 'us-east-1', - source_endpoint_url: options.sourceEndpointUrl, - source_session_token: options.sourceSessionToken, - dest_access_key_id: options.destAccessKeyId, - dest_secret_access_key: options.destSecretAccessKey, - dest_region: options.destRegion || 'us-east-1', - dest_endpoint_url: options.destEndpointUrl, - dest_session_token: options.destSessionToken, - preserve_metadata: options.preserveMetadata !== undefined ? options.preserveMetadata : true, - storage_class: options.storageClass || 'STANDARD', - max_concurrent: options.maxConcurrent || 5 - }; - - const r = await fetch(`${API_BASE}/api/s3/migrate-batch`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// Start full S3 to S3 migration -async function startS3Migration(sourceBucket, destBucket, prefix = '', options = {}) { - const body = { - source_bucket: sourceBucket, - dest_bucket: destBucket, - prefix, - source_access_key_id: options.sourceAccessKeyId, - source_secret_access_key: options.sourceSecretAccessKey, - source_region: options.sourceRegion || 'us-east-1', - source_endpoint_url: options.sourceEndpointUrl, - source_session_token: options.sourceSessionToken, - dest_access_key_id: options.destAccessKeyId, - dest_secret_access_key: options.destSecretAccessKey, - dest_region: options.destRegion || 'us-east-1', - dest_endpoint_url: options.destEndpointUrl, - dest_session_token: options.destSessionToken, - include_patterns: options.includePatterns, - exclude_patterns: options.excludePatterns, - preserve_metadata: options.preserveMetadata !== undefined ? options.preserveMetadata : true, - storage_class: options.storageClass || 'STANDARD', - create_dest_bucket: options.createDestBucket !== undefined ? options.createDestBucket : true, - max_concurrent: options.maxConcurrent || 5 - }; - - const r = await fetch(`${API_BASE}/api/s3/start-migration`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// Get S3 migration status -async function getS3MigrationStatus(migrationId) { - const r = await fetch(`${API_BASE}/api/s3/migration-status/${migrationId}`); - return r.json(); -} - -// List S3 migrations -async function listS3Migrations() { - const r = await fetch(`${API_BASE}/api/s3/list-migrations`); - return r.json(); -} - -// Cancel S3 migration -async function cancelS3Migration(migrationId) { - const r = await fetch(`${API_BASE}/api/s3/cancel-migration/${migrationId}`, { - method: "POST", - headers: { "Content-Type": "application/json" } - }); - return r.json(); -} - -// Parse S3 URI -async function parseS3Uri(s3Uri) { - const r = await fetch(`${API_BASE}/api/s3/parse-uri`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ s3_uri: s3Uri }) - }); - return r.json(); -} - -// Generate presigned URL for S3 object -async function generatePresignedUrl(bucket, key, isSource = true, expiration = 3600, credentials = {}) { - const body = { - bucket, - key, - is_source: isSource, - expiration, - access_key_id: credentials.accessKeyId, - secret_access_key: credentials.secretAccessKey, - region: credentials.region || 'us-east-1', - endpoint_url: credentials.endpointUrl, - session_token: credentials.sessionToken - }; - - const r = await fetch(`${API_BASE}/api/s3/generate-presigned-url`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// ============================================================================ -// PostgreSQL to S3 API Functions -// ============================================================================ - -// Test PostgreSQL connection for PG to S3 -async function testPgToS3PostgresConnection(uri) { - const r = await fetch(`${API_BASE}/api/postgres-s3/test-postgres-connection`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ uri }) - }); - return r.json(); -} - -// Test S3 connection for PG to S3 -async function testPgToS3S3Connection(accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl = null) { - const r = await fetch(`${API_BASE}/api/postgres-s3/test-s3-connection`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - access_key_id: accessKeyId, - secret_access_key: secretAccessKey, - region, - endpoint_url: endpointUrl - }) - }); - return r.json(); -} - -// Get PostgreSQL schemas for PG to S3 -async function getPgToS3Schemas(uri) { - const r = await fetch(`${API_BASE}/api/postgres-s3/get-schemas`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ uri }) - }); - return r.json(); -} - -// Get PostgreSQL tables for PG to S3 -async function getPgToS3Tables(uri, schema = '') { - const r = await fetch(`${API_BASE}/api/postgres-s3/get-tables`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ uri, schema }) - }); - return r.json(); -} - -// Export single PostgreSQL table to S3 -async function exportTableToS3(postgresUri, schema, table, s3Bucket, s3Key, options = {}) { - const body = { - postgres_uri: postgresUri, - schema, - table, - s3_bucket: s3Bucket, - s3_key: s3Key, - compress: options.compress !== undefined ? options.compress : true, - format: options.format || 'csv', - access_key_id: options.accessKeyId, - secret_access_key: options.secretAccessKey, - region: options.region || 'us-east-1', - endpoint_url: options.endpointUrl - }; - - const r = await fetch(`${API_BASE}/api/postgres-s3/export-table`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// Start full PostgreSQL to S3 migration -async function startPgToS3Migration(postgresUri, s3Bucket, s3Prefix = '', options = {}) { - const body = { - postgres_uri: postgresUri, - s3_bucket: s3Bucket, - s3_prefix: s3Prefix, - schemas: options.schemas, - tables: options.tables, - compress: options.compress !== undefined ? options.compress : true, - format: options.format || 'csv', - access_key_id: options.accessKeyId, - secret_access_key: options.secretAccessKey, - region: options.region || 'us-east-1', - endpoint_url: options.endpointUrl - }; - - const r = await fetch(`${API_BASE}/api/postgres-s3/start-migration`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body) - }); - return r.json(); -} - -// Get PostgreSQL to S3 migration status -async function getPgToS3MigrationStatus(migrationId) { - const r = await fetch(`${API_BASE}/api/postgres-s3/migration-status/${migrationId}`); - return r.json(); -} - -// List PostgreSQL to S3 migrations -async function listPgToS3Migrations() { - const r = await fetch(`${API_BASE}/api/postgres-s3/list-migrations`); - return r.json(); -} - -// ============================================================================ -// Common Environment Functions -// ============================================================================ - -// Inject environment variables -async function injectEnv(envVars) { - const r = await fetch(`${API_BASE}/api/inject-env`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ environment_variables: envVars }) - }); - return r.json(); -} - -// Get current environment -async function getCurrentEnv() { - const r = await fetch(`${API_BASE}/api/get-current-env`); - return r.json(); -} - -// Health check -async function healthCheck() { - const r = await fetch(`${API_BASE}/api/health`); - return r.json(); -} - -// ============================================================================ -// Security API Functions -// ============================================================================ - -// Clear session -async function clearSession() { - const r = await fetch(`${API_BASE}/api/clear-session`, { - method: "POST", - headers: { "Content-Type": "application/json" } - }); - return r.json(); -} - -// Clear migration -async function clearMigration(migrationId) { - const r = await fetch(`${API_BASE}/api/clear-migration/${migrationId}`, { - method: "POST", - headers: { "Content-Type": "application/json" } - }); - return r.json(); -} - -// Get security status -async function getSecurityStatus() { - const r = await fetch(`${API_BASE}/api/security-status`); - return r.json(); -} - -// ============================================================================ -// Main App -// ============================================================================ - -function App() { - const root = document.getElementById("root"); - - root.innerHTML = ` -
- -
- -
-
-

🔄 Universal Migrator

-

PostgreSQL & S3 Migration Tool

-
-
-
🔒 Secure Mode
-
-
- - -
- - - - -
- - -
-

🐘 → 📤 PostgreSQL to S3 Migration

- -
- -
-
-

PostgreSQL Source

- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- -
- - -
-
- -
- - - -
- - -
- - - - -
- - -
-
-

S3 Destination

- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- -
- -
-
- -
- - -
- -
- -
-
-
-
-
- - -
-

🐘 → 🐘 PostgreSQL to PostgreSQL Migration

- -
- -
-
-

Source PostgreSQL

- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- -
- - -
-
- -
- - - -
- - -
- - - - -
- - -
-
-

Destination PostgreSQL

- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- -
- - -
-
- -
- -
- - -
-
-
- - -
-

Migration Options

-
- - -
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- -
-
-
- - -
-

📤 → 📥 S3 to S3 Migration

- -
- -
-
-

Source S3

- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- - -
- - -
- - -
- - -
-
-

Destination S3

- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- - - -
- - -
- - -
-
- - -
- -
-
- - -
-

⚙️ Environment & Security

- -
- -
-
-

Environment Variables

- -
- - - -
- -
- - -
-
- -
-

Current Configuration

-
-
- -
-

Environment Preview

-

-            
-
- - -
-
-

Security

- -
-

Session ID: Loading...

-

Active Sessions: -

-

Expiry: 10 minutes

-
- -
-
- Encryption: - AES-256 -
-
- Session Isolation: - ✅ Enabled -
-
- Auto-cleanup: - 10 minutes -
-
- -
- -
-
- -
-

Migration Data Cleanup

-

Clear sensitive data from migrations

-
- - -
-
- -
-

Migration History

-
- -
- - - - - - -
-
-
-
- -
-
Ready
- -
-
- `; - - // ==================== State Variables ==================== - - // ===== PSQL to S3 ===== - let psqlToS3Config = { - source: { host: '', user: '', password: '', port: 5432, database: '', uri: '' }, - s3: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1', endpointUrl: '', bucket: '', prefix: '' }, - compress: true, - format: 'csv' - }; - - // ===== PSQL to PSQL ===== - let psqlToPsqlConfig = { - source: { host: '', user: '', password: '', port: 5432, database: '', uri: '' }, - dest: { host: '', user: '', password: '', port: 5432, database: '', uri: '' } - }; - let psqlToPsqlSchemas = []; - let psqlToPsqlTables = []; - let psqlToPsqlSelectedSchemas = []; - let psqlToPsqlSelectedTables = []; - - // ===== S3 to S3 ===== - let s3ToS3Config = { - source: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1', endpointUrl: '', sessionToken: '', bucket: 'my-source-bucket' }, - dest: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1', endpointUrl: '', sessionToken: '', bucket: 'my-destination-bucket' } - }; - let s3ToS3SourceBuckets = []; - let s3ToS3DestBuckets = []; - - // Common state variables - let migrations = []; - let activeMigration = null; - let migrationLogs = []; - let currentEnv = {}; - let loading = false; - - // DOM Elements - const statusInfo = document.getElementById("statusInfo"); - const loadingDiv = document.getElementById("loading"); - const envPreview = document.getElementById("envPreview"); - const sourcePostgresSummary = document.getElementById("sourcePostgresSummary"); - const destPostgresSummary = document.getElementById("destPostgresSummary"); - const sourceS3Summary = document.getElementById("sourceS3Summary"); - const destS3Summary = document.getElementById("destS3Summary"); - - // ==================== Setup Event Listeners ==================== - function setupEventListeners() { - // Tab switching - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.addEventListener('click', () => { - document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); - document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); - - btn.classList.add('active'); - document.getElementById(`tab-${btn.dataset.tab}`).classList.add('active'); - }); - }); - - // ========== PSQL to S3 Inputs ========== - document.getElementById('psqlToS3_sourceHost')?.addEventListener('input', (e) => { - psqlToS3Config.source.host = e.target.value; - }); - - document.getElementById('psqlToS3_sourcePort')?.addEventListener('input', (e) => { - psqlToS3Config.source.port = parseInt(e.target.value) || 5432; - }); - - document.getElementById('psqlToS3_sourceUser')?.addEventListener('input', (e) => { - psqlToS3Config.source.user = e.target.value; - }); - - document.getElementById('psqlToS3_sourcePassword')?.addEventListener('input', (e) => { - psqlToS3Config.source.password = e.target.value; - }); - - document.getElementById('psqlToS3_sourceDatabase')?.addEventListener('input', (e) => { - psqlToS3Config.source.database = e.target.value; - }); - - document.getElementById('psqlToS3_sourceUri')?.addEventListener('input', (e) => { - psqlToS3Config.source.uri = e.target.value; - }); - - document.getElementById('psqlToS3_s3AccessKeyId')?.addEventListener('input', (e) => { - psqlToS3Config.s3.accessKeyId = e.target.value; - }); - - document.getElementById('psqlToS3_s3SecretAccessKey')?.addEventListener('input', (e) => { - psqlToS3Config.s3.secretAccessKey = e.target.value; - }); - - document.getElementById('psqlToS3_s3Region')?.addEventListener('input', (e) => { - psqlToS3Config.s3.region = e.target.value || 'us-east-1'; - }); - - document.getElementById('psqlToS3_s3EndpointUrl')?.addEventListener('input', (e) => { - psqlToS3Config.s3.endpointUrl = e.target.value; - }); - - document.getElementById('psqlToS3_s3Bucket')?.addEventListener('input', (e) => { - psqlToS3Config.s3.bucket = e.target.value; - }); - - document.getElementById('psqlToS3_s3Prefix')?.addEventListener('input', (e) => { - psqlToS3Config.s3.prefix = e.target.value; - }); - - document.getElementById('psqlToS3_compress')?.addEventListener('change', (e) => { - psqlToS3Config.compress = e.target.checked; - }); - - document.getElementById('psqlToS3_format')?.addEventListener('change', (e) => { - psqlToS3Config.format = e.target.value; - }); - - // ========== PSQL to PSQL Inputs ========== - document.getElementById('psqlToPsql_sourceHost')?.addEventListener('input', (e) => { - psqlToPsqlConfig.source.host = e.target.value; - }); - - document.getElementById('psqlToPsql_sourcePort')?.addEventListener('input', (e) => { - psqlToPsqlConfig.source.port = parseInt(e.target.value) || 5432; - }); - - document.getElementById('psqlToPsql_sourceUser')?.addEventListener('input', (e) => { - psqlToPsqlConfig.source.user = e.target.value; - }); - - document.getElementById('psqlToPsql_sourcePassword')?.addEventListener('input', (e) => { - psqlToPsqlConfig.source.password = e.target.value; - }); - - document.getElementById('psqlToPsql_sourceDatabase')?.addEventListener('input', (e) => { - psqlToPsqlConfig.source.database = e.target.value; - }); - - document.getElementById('psqlToPsql_sourceUri')?.addEventListener('input', (e) => { - psqlToPsqlConfig.source.uri = e.target.value; - }); - - document.getElementById('psqlToPsql_destHost')?.addEventListener('input', (e) => { - psqlToPsqlConfig.dest.host = e.target.value; - }); - - document.getElementById('psqlToPsql_destPort')?.addEventListener('input', (e) => { - psqlToPsqlConfig.dest.port = parseInt(e.target.value) || 5432; - }); - - document.getElementById('psqlToPsql_destUser')?.addEventListener('input', (e) => { - psqlToPsqlConfig.dest.user = e.target.value; - }); - - document.getElementById('psqlToPsql_destPassword')?.addEventListener('input', (e) => { - psqlToPsqlConfig.dest.password = e.target.value; - }); - - document.getElementById('psqlToPsql_destDatabase')?.addEventListener('input', (e) => { - psqlToPsqlConfig.dest.database = e.target.value; - }); - - document.getElementById('psqlToPsql_destUri')?.addEventListener('input', (e) => { - psqlToPsqlConfig.dest.uri = e.target.value; - }); - - // ========== S3 to S3 Inputs ========== - document.getElementById('s3ToS3_sourceAccessKeyId')?.addEventListener('input', (e) => { - s3ToS3Config.source.accessKeyId = e.target.value; - }); - - document.getElementById('s3ToS3_sourceSecretAccessKey')?.addEventListener('input', (e) => { - s3ToS3Config.source.secretAccessKey = e.target.value; - }); - - document.getElementById('s3ToS3_sourceRegion')?.addEventListener('input', (e) => { - s3ToS3Config.source.region = e.target.value || 'us-east-1'; - }); - - document.getElementById('s3ToS3_sourceEndpointUrl')?.addEventListener('input', (e) => { - s3ToS3Config.source.endpointUrl = e.target.value; - }); - - document.getElementById('s3ToS3_sourceSessionToken')?.addEventListener('input', (e) => { - s3ToS3Config.source.sessionToken = e.target.value; - }); - - document.getElementById('s3ToS3_sourceBucket')?.addEventListener('input', (e) => { - s3ToS3Config.source.bucket = e.target.value; - }); - - document.getElementById('s3ToS3_destAccessKeyId')?.addEventListener('input', (e) => { - s3ToS3Config.dest.accessKeyId = e.target.value; - }); - - document.getElementById('s3ToS3_destSecretAccessKey')?.addEventListener('input', (e) => { - s3ToS3Config.dest.secretAccessKey = e.target.value; - }); - - document.getElementById('s3ToS3_destRegion')?.addEventListener('input', (e) => { - s3ToS3Config.dest.region = e.target.value || 'us-east-1'; - }); - - document.getElementById('s3ToS3_destEndpointUrl')?.addEventListener('input', (e) => { - s3ToS3Config.dest.endpointUrl = e.target.value; - }); - - document.getElementById('s3ToS3_destSessionToken')?.addEventListener('input', (e) => { - s3ToS3Config.dest.sessionToken = e.target.value; - }); - - document.getElementById('s3ToS3_destBucket')?.addEventListener('input', (e) => { - s3ToS3Config.dest.bucket = e.target.value; - }); - - // ========== PSQL to S3 Actions ========== - document.getElementById('psqlToS3_testSourceConnection')?.addEventListener('click', testPsqlToS3SourceConnectionHandler); - document.getElementById('psqlToS3_parseSourceUri')?.addEventListener('click', parsePsqlToS3SourceUriHandler); - document.getElementById('psqlToS3_getSchemas')?.addEventListener('click', getPsqlToS3SchemasHandler); - document.getElementById('psqlToS3_getTables')?.addEventListener('click', getPsqlToS3TablesHandler); - document.getElementById('psqlToS3_testPgConnection')?.addEventListener('click', testPsqlToS3PgConnectionHandler); - document.getElementById('psqlToS3_testS3Connection')?.addEventListener('click', testPsqlToS3S3ConnectionHandler); - document.getElementById('psqlToS3_startMigration')?.addEventListener('click', startPsqlToS3MigrationHandler); - - // ========== PSQL to PSQL Actions ========== - document.getElementById('psqlToPsql_testSourceConnection')?.addEventListener('click', testPsqlToPsqlSourceConnectionHandler); - document.getElementById('psqlToPsql_parseSourceUri')?.addEventListener('click', parsePsqlToPsqlSourceUriHandler); - document.getElementById('psqlToPsql_getSchemas')?.addEventListener('click', getPsqlToPsqlSchemasHandler); - document.getElementById('psqlToPsql_getTables')?.addEventListener('click', getPsqlToPsqlTablesHandler); - document.getElementById('psqlToPsql_testDestConnection')?.addEventListener('click', testPsqlToPsqlDestConnectionHandler); - document.getElementById('psqlToPsql_parseDestUri')?.addEventListener('click', parsePsqlToPsqlDestUriHandler); - document.getElementById('psqlToPsql_startMigration')?.addEventListener('click', startPsqlToPsqlMigrationHandler); - - // ========== S3 to S3 Actions ========== - document.getElementById('s3ToS3_testSourceConnection')?.addEventListener('click', testS3ToS3SourceConnectionHandler); - document.getElementById('s3ToS3_listSourceBuckets')?.addEventListener('click', listS3ToS3SourceBucketsHandler); - document.getElementById('s3ToS3_hideSourceBuckets')?.addEventListener('click', () => { - document.getElementById('s3ToS3_sourceBucketsList').style.display = 'none'; - }); - - document.getElementById('s3ToS3_testDestConnection')?.addEventListener('click', testS3ToS3DestConnectionHandler); - document.getElementById('s3ToS3_listDestBuckets')?.addEventListener('click', listS3ToS3DestBucketsHandler); - document.getElementById('s3ToS3_createDestBucket')?.addEventListener('click', createS3ToS3DestBucketHandler); - document.getElementById('s3ToS3_hideDestBuckets')?.addEventListener('click', () => { - document.getElementById('s3ToS3_destBucketsList').style.display = 'none'; - }); - - document.getElementById('s3ToS3_startMigration')?.addEventListener('click', startS3ToS3MigrationHandler); - - // ========== Environment Actions ========== - document.getElementById('refreshEnv')?.addEventListener('click', loadCurrentEnvHandler); - document.getElementById('injectEnv')?.addEventListener('click', injectEnvironmentHandler); - document.getElementById('clearEnv')?.addEventListener('click', clearEnvironmentHandler); - document.getElementById('copyEnv')?.addEventListener('click', copyEnvToClipboardHandler); - - // ========== Security Actions ========== - document.getElementById('clearSessionBtn')?.addEventListener('click', clearSessionHandler); - document.getElementById('clearMigrationBtn')?.addEventListener('click', clearMigrationHandler); - document.getElementById('refreshMigrations')?.addEventListener('click', loadMigrationsHandler); - } - - // Helper function for summaries - function updateS3Summaries() { - if (sourceS3Summary) sourceS3Summary.textContent = s3ToS3Config.source.bucket || 'Not set'; - if (destS3Summary) destS3Summary.textContent = s3ToS3Config.dest.bucket || 'Not set'; - } - - // ==================== Helper Functions ==================== - - function setLoading(isLoading) { - loading = isLoading; - loadingDiv.style.display = isLoading ? 'block' : 'none'; - } - - function updatePostgresSummaries() { - if (sourcePostgresSummary) { - sourcePostgresSummary.textContent = psqlToPsqlConfig.source.database - ? `${psqlToPsqlConfig.source.database}@${psqlToPsqlConfig.source.host}` - : 'Not set'; - } - if (destPostgresSummary) { - destPostgresSummary.textContent = psqlToPsqlConfig.dest.database - ? `${psqlToPsqlConfig.dest.database}@${psqlToPsqlConfig.dest.host}` - : 'Not set'; - } - } - - function updateStatusInfo() { - let info = []; - if (psqlToPsqlConfig.source.database) info.push(`🐘 Source PG: ${psqlToPsqlConfig.source.database}`); - if (psqlToPsqlConfig.dest.database) info.push(`🐘 Dest PG: ${psqlToPsqlConfig.dest.database}`); - if (s3ToS3Config.source.bucket) info.push(`📤 S3: ${s3ToS3Config.source.bucket}`); - if (s3ToS3Config.dest.bucket) info.push(`📥 S3: ${s3ToS3Config.dest.bucket}`); - if (activeMigration) info.push(`🚀 Migration: ${activeMigration}`); - info.push(`⚡ ${Object.keys(currentEnv).length} env vars`); - - statusInfo.textContent = info.join(' • ') || 'Ready'; - } - - // ==================== PSQL to S3 Handlers ==================== - - async function testPsqlToS3SourceConnectionHandler() { - setLoading(true); - try { - let uri = psqlToS3Config.source.uri; - if (!uri && psqlToS3Config.source.host && psqlToS3Config.source.user && psqlToS3Config.source.password && psqlToS3Config.source.database) { - uri = `postgresql://${psqlToS3Config.source.user}:${psqlToS3Config.source.password}@${psqlToS3Config.source.host}:${psqlToS3Config.source.port}/${psqlToS3Config.source.database}`; - } - - const result = await testPostgresConnection({ uri }); - - const statusDiv = document.getElementById('psqlToS3_sourceConnectionStatus'); - if (result.success) { - showNotification(`✅ PSQL to S3 - Source connection successful!`, 'success'); - statusDiv.innerHTML = ` -

Success: ✅ Connected

-

Host: ${psqlToS3Config.source.host || result.connection?.host}:${psqlToS3Config.source.port || result.connection?.port}

-

Version: ${result.version || 'Unknown'}

- `; - statusDiv.className = 'status-message success'; - } else { - showNotification(`❌ PSQL to S3 - Source connection failed: ${result.error}`, 'error'); - statusDiv.innerHTML = `

Error: ${result.error}

`; - statusDiv.className = 'status-message error'; - } - statusDiv.style.display = 'block'; - } catch (error) { - showNotification(`❌ Error testing source PostgreSQL connection: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function parsePsqlToS3SourceUriHandler() { - const uri = document.getElementById('psqlToS3_sourceUri')?.value; - if (!uri) { - showNotification('Please enter PostgreSQL URI', 'warning'); - return; - } - - setLoading(true); - try { - const result = await parsePostgresUri(uri); - if (result.success && result.parsed) { - psqlToS3Config.source = { - host: result.parsed.host || '', - user: result.parsed.user || '', - password: result.parsed.password || '', - port: result.parsed.port || 5432, - database: result.parsed.database || '', - uri: uri - }; - - document.getElementById('psqlToS3_sourceHost').value = result.parsed.host; - document.getElementById('psqlToS3_sourceUser').value = result.parsed.user; - document.getElementById('psqlToS3_sourcePassword').value = result.parsed.password; - document.getElementById('psqlToS3_sourcePort').value = result.parsed.port; - document.getElementById('psqlToS3_sourceDatabase').value = result.parsed.database; - - showNotification('✅ Source PostgreSQL URI parsed successfully', 'success'); - } else { - showNotification(`❌ Failed to parse PostgreSQL URI: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error parsing PostgreSQL URI: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function getPsqlToS3SchemasHandler() { - setLoading(true); - try { - const params = {}; - - if (psqlToS3Config.source.uri) { - params.uri = psqlToS3Config.source.uri; - } else if (psqlToS3Config.source.host && psqlToS3Config.source.user && psqlToS3Config.source.password && psqlToS3Config.source.database) { - params.host = psqlToS3Config.source.host; - params.user = psqlToS3Config.source.user; - params.password = psqlToS3Config.source.password; - params.database = psqlToS3Config.source.database; - params.port = psqlToS3Config.source.port; - } else { - showNotification('Please enter source PostgreSQL connection details', 'warning'); - setLoading(false); - return; - } - - const result = await getPostgresSchemas(params); - - if (result.success) { - renderPsqlToS3Schemas(result.schemas || []); - document.getElementById('psqlToS3_schemasSection').style.display = 'block'; - showNotification(`✅ Found ${result.count} schema(s)`, 'success'); - } else { - showNotification(`❌ Failed to get schemas: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error getting schemas: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function getPsqlToS3TablesHandler() { - setLoading(true); - try { - const params = {}; - - if (psqlToS3Config.source.uri) { - params.uri = psqlToS3Config.source.uri; - } else if (psqlToS3Config.source.host && psqlToS3Config.source.user && psqlToS3Config.source.password && psqlToS3Config.source.database) { - params.host = psqlToS3Config.source.host; - params.user = psqlToS3Config.source.user; - params.password = psqlToS3Config.source.password; - params.database = psqlToS3Config.source.database; - params.port = psqlToS3Config.source.port; - } else { - showNotification('Please enter source PostgreSQL connection details', 'warning'); - setLoading(false); - return; - } - - const result = await getPostgresTables(params); - if (result.success) { - renderPsqlToS3Tables(result.tables || []); - document.getElementById('psqlToS3_tablesSection').style.display = 'block'; - showNotification(`✅ Found ${result.count} table(s)`, 'success'); - } else { - showNotification(`❌ Failed to get tables: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error getting tables: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function testPsqlToS3PgConnectionHandler() { - if (!psqlToS3Config.source.uri && !psqlToS3Config.source.host) { - showNotification('Please enter PostgreSQL connection details', 'warning'); - return; - } - - setLoading(true); - try { - const uri = psqlToS3Config.source.uri || buildPostgresConnectionString( - psqlToS3Config.source.host, - psqlToS3Config.source.database, - psqlToS3Config.source.user, - psqlToS3Config.source.port - ); - - const result = await testPgToS3PostgresConnection(uri); - if (result.success) { - showNotification(`✅ PostgreSQL connection successful!`, 'success'); - } else { - showNotification(`❌ PostgreSQL connection failed: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error testing PostgreSQL connection: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function testPsqlToS3S3ConnectionHandler() { - setLoading(true); - try { - const result = await testPgToS3S3Connection( - psqlToS3Config.s3.accessKeyId, - psqlToS3Config.s3.secretAccessKey, - psqlToS3Config.s3.region, - psqlToS3Config.s3.endpointUrl - ); - - if (result.success) { - showNotification(`✅ S3 connection successful!`, 'success'); - } else { - showNotification(`❌ S3 connection failed: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error testing S3 connection: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function startPsqlToS3MigrationHandler() { - if (!psqlToS3Config.source.uri && !psqlToS3Config.source.host) { - showNotification('Please enter PostgreSQL connection details', 'warning'); - return; - } - - if (!psqlToS3Config.s3.bucket) { - showNotification('Please enter S3 bucket name', 'warning'); - return; - } - - setLoading(true); - - try { - const uri = psqlToS3Config.source.uri || buildPostgresConnectionString( - psqlToS3Config.source.host, - psqlToS3Config.source.database, - psqlToS3Config.source.user, - psqlToS3Config.source.port - ); - - const result = await startPgToS3Migration( - uri, - psqlToS3Config.s3.bucket, - psqlToS3Config.s3.prefix, - { - compress: psqlToS3Config.compress, - format: psqlToS3Config.format, - accessKeyId: psqlToS3Config.s3.accessKeyId, - secretAccessKey: psqlToS3Config.s3.secretAccessKey, - region: psqlToS3Config.s3.region, - endpointUrl: psqlToS3Config.s3.endpointUrl - } - ); - - if (result.success) { - activeMigration = result.migration_id; - showNotification(`✅ PostgreSQL to S3 migration ${activeMigration} started!`, 'success'); - pollPgToS3MigrationStatus(activeMigration); - } else { - showNotification(`❌ Failed to start migration: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error starting migration: ${error.message}`, 'error'); - } - setLoading(false); - } - - // ==================== PSQL to PSQL Handlers ==================== - - async function testPsqlToPsqlSourceConnectionHandler() { - setLoading(true); - try { - let uri = psqlToPsqlConfig.source.uri; - if (!uri && psqlToPsqlConfig.source.host && psqlToPsqlConfig.source.user && psqlToPsqlConfig.source.password && psqlToPsqlConfig.source.database) { - uri = `postgresql://${psqlToPsqlConfig.source.user}:${psqlToPsqlConfig.source.password}@${psqlToPsqlConfig.source.host}:${psqlToPsqlConfig.source.port}/${psqlToPsqlConfig.source.database}`; - } - - const result = await testPostgresConnection({ uri }); - - const statusDiv = document.getElementById('psqlToPsql_sourceConnectionStatus'); - if (result.success) { - showNotification(`✅ PSQL to PSQL - Source connection successful!`, 'success'); - statusDiv.innerHTML = ` -

Success: ✅ Connected

-

Host: ${psqlToPsqlConfig.source.host || result.connection?.host}:${psqlToPsqlConfig.source.port || result.connection?.port}

-

Version: ${result.version || 'Unknown'}

- `; - statusDiv.className = 'status-message success'; - } else { - showNotification(`❌ PSQL to PSQL - Source connection failed: ${result.error}`, 'error'); - statusDiv.innerHTML = `

Error: ${result.error}

`; - statusDiv.className = 'status-message error'; - } - statusDiv.style.display = 'block'; - } catch (error) { - showNotification(`❌ Error testing source PostgreSQL connection: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function parsePsqlToPsqlSourceUriHandler() { - const uri = document.getElementById('psqlToPsql_sourceUri')?.value; - if (!uri) { - showNotification('Please enter PostgreSQL URI', 'warning'); - return; - } - - setLoading(true); - try { - const result = await parsePostgresUri(uri); - if (result.success && result.parsed) { - psqlToPsqlConfig.source = { - host: result.parsed.host || '', - user: result.parsed.user || '', - password: result.parsed.password || '', - port: result.parsed.port || 5432, - database: result.parsed.database || '', - uri: uri - }; - - document.getElementById('psqlToPsql_sourceHost').value = result.parsed.host; - document.getElementById('psqlToPsql_sourceUser').value = result.parsed.user; - document.getElementById('psqlToPsql_sourcePassword').value = result.parsed.password; - document.getElementById('psqlToPsql_sourcePort').value = result.parsed.port; - document.getElementById('psqlToPsql_sourceDatabase').value = result.parsed.database; - - showNotification('✅ Source PostgreSQL URI parsed successfully', 'success'); - } else { - showNotification(`❌ Failed to parse PostgreSQL URI: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error parsing PostgreSQL URI: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function getPsqlToPsqlSchemasHandler() { - setLoading(true); - try { - const params = {}; - - if (psqlToPsqlConfig.source.uri) { - params.uri = psqlToPsqlConfig.source.uri; - } else if (psqlToPsqlConfig.source.host && psqlToPsqlConfig.source.user && psqlToPsqlConfig.source.password && psqlToPsqlConfig.source.database) { - params.host = psqlToPsqlConfig.source.host; - params.user = psqlToPsqlConfig.source.user; - params.password = psqlToPsqlConfig.source.password; - params.database = psqlToPsqlConfig.source.database; - params.port = psqlToPsqlConfig.source.port; - } else { - showNotification('Please enter source PostgreSQL connection details', 'warning'); - setLoading(false); - return; - } - - const result = await getPostgresSchemas(params); - - if (result.success) { - psqlToPsqlSchemas = result.schemas || []; - renderPsqlToPsqlSchemas(); - document.getElementById('psqlToPsql_schemasSection').style.display = 'block'; - showNotification(`✅ Found ${result.count} schema(s)`, 'success'); - } else { - showNotification(`❌ Failed to get schemas: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error getting schemas: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function getPsqlToPsqlTablesHandler() { - setLoading(true); - try { - const params = {}; - - if (psqlToPsqlConfig.source.uri) { - params.uri = psqlToPsqlConfig.source.uri; - } else if (psqlToPsqlConfig.source.host && psqlToPsqlConfig.source.user && psqlToPsqlConfig.source.password && psqlToPsqlConfig.source.database) { - params.host = psqlToPsqlConfig.source.host; - params.user = psqlToPsqlConfig.source.user; - params.password = psqlToPsqlConfig.source.password; - params.database = psqlToPsqlConfig.source.database; - params.port = psqlToPsqlConfig.source.port; - } else { - showNotification('Please enter source PostgreSQL connection details', 'warning'); - setLoading(false); - return; - } - - const result = await getPostgresTables(params); - if (result.success) { - psqlToPsqlTables = result.tables || []; - renderPsqlToPsqlTables(); - document.getElementById('psqlToPsql_tablesSection').style.display = 'block'; - showNotification(`✅ Found ${result.count} table(s)`, 'success'); - } else { - showNotification(`❌ Failed to get tables: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error getting tables: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function testPsqlToPsqlDestConnectionHandler() { - setLoading(true); - try { - let uri = psqlToPsqlConfig.dest.uri; - if (!uri && psqlToPsqlConfig.dest.host && psqlToPsqlConfig.dest.user && psqlToPsqlConfig.dest.password && psqlToPsqlConfig.dest.database) { - uri = `postgresql://${psqlToPsqlConfig.dest.user}:${psqlToPsqlConfig.dest.password}@${psqlToPsqlConfig.dest.host}:${psqlToPsqlConfig.dest.port}/${psqlToPsqlConfig.dest.database}`; - } - - const result = await testPostgresConnection({ uri }); - - const statusDiv = document.getElementById('psqlToPsql_destConnectionStatus'); - if (result.success) { - showNotification(`✅ PSQL to PSQL - Destination connection successful!`, 'success'); - statusDiv.innerHTML = ` -

Success: ✅ Connected

-

Host: ${psqlToPsqlConfig.dest.host || result.connection?.host}:${psqlToPsqlConfig.dest.port || result.connection?.port}

-

Version: ${result.version || 'Unknown'}

- `; - statusDiv.className = 'status-message success'; - } else { - showNotification(`❌ PSQL to PSQL - Destination connection failed: ${result.error}`, 'error'); - statusDiv.innerHTML = `

Error: ${result.error}

`; - statusDiv.className = 'status-message error'; - } - statusDiv.style.display = 'block'; - } catch (error) { - showNotification(`❌ Error testing destination PostgreSQL connection: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function parsePsqlToPsqlDestUriHandler() { - const uri = document.getElementById('psqlToPsql_destUri')?.value; - if (!uri) { - showNotification('Please enter destination PostgreSQL URI', 'warning'); - return; - } - - setLoading(true); - try { - const result = await parsePostgresUri(uri); - if (result.success && result.parsed) { - psqlToPsqlConfig.dest = { - host: result.parsed.host || '', - user: result.parsed.user || '', - password: result.parsed.password || '', - port: result.parsed.port || 5432, - database: result.parsed.database || '', - uri: uri - }; - - document.getElementById('psqlToPsql_destHost').value = result.parsed.host; - document.getElementById('psqlToPsql_destUser').value = result.parsed.user; - document.getElementById('psqlToPsql_destPassword').value = result.parsed.password; - document.getElementById('psqlToPsql_destPort').value = result.parsed.port; - document.getElementById('psqlToPsql_destDatabase').value = result.parsed.database; - - showNotification('✅ Destination PostgreSQL URI parsed successfully', 'success'); - updatePostgresSummaries(); - } else { - showNotification(`❌ Failed to parse PostgreSQL URI: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error parsing PostgreSQL URI: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function startPsqlToPsqlMigrationHandler() { - let sourceUri = psqlToPsqlConfig.source.uri || buildPostgresConnectionString( - psqlToPsqlConfig.source.host, - psqlToPsqlConfig.source.database, - psqlToPsqlConfig.source.user, - psqlToPsqlConfig.source.port - ); - - let destUri = psqlToPsqlConfig.dest.uri || buildPostgresConnectionString( - psqlToPsqlConfig.dest.host, - psqlToPsqlConfig.dest.database, - psqlToPsqlConfig.dest.user, - psqlToPsqlConfig.dest.port - ); - - if (!sourceUri) { - showNotification('Please enter source PostgreSQL connection details', 'warning'); - return; - } - - if (!destUri) { - showNotification('Please enter destination PostgreSQL connection details', 'warning'); - return; - } - - setLoading(true); - - try { - const result = await startPostgresMigration( - sourceUri, - destUri, - psqlToPsqlSelectedSchemas.length > 0 ? psqlToPsqlSelectedSchemas : null, - psqlToPsqlSelectedTables.length > 0 ? psqlToPsqlSelectedTables : null - ); - - if (result.success) { - activeMigration = result.migration_id; - showNotification(`✅ PostgreSQL migration ${activeMigration} started!`, 'success'); - pollPostgresMigrationStatus(activeMigration); - } else { - showNotification(`❌ Failed to start migration: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error starting migration: ${error.message}`, 'error'); - } - setLoading(false); - } - - // ==================== S3 to S3 Handlers ==================== - - async function testS3ToS3SourceConnectionHandler() { - setLoading(true); - try { - const result = await testSourceS3Connection({ - useEnvVars: false, - accessKeyId: s3ToS3Config.source.accessKeyId, - secretAccessKey: s3ToS3Config.source.secretAccessKey, - region: s3ToS3Config.source.region, - endpointUrl: s3ToS3Config.source.endpointUrl, - sessionToken: s3ToS3Config.source.sessionToken - }); - - const statusDiv = document.getElementById('s3ToS3_sourceConnectionStatus'); - if (result.success) { - showNotification(`✅ S3 to S3 - Source connection successful!`, 'success'); - statusDiv.innerHTML = ` -

Success: ✅ Connected

-

Endpoint: ${s3ToS3Config.source.endpointUrl || 'AWS S3 (default)'}

-

Region: ${s3ToS3Config.source.region}

-

Buckets Found: ${result.bucket_count || 0}

- `; - statusDiv.className = 'status-message success'; - } else { - showNotification(`❌ S3 to S3 - Source connection failed: ${result.error}`, 'error'); - statusDiv.innerHTML = `

Error: ${result.error}

`; - statusDiv.className = 'status-message error'; - } - statusDiv.style.display = 'block'; - } catch (error) { - showNotification(`❌ Error testing source S3 connection: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function listS3ToS3SourceBucketsHandler() { - setLoading(true); - try { - const result = await listSourceS3Buckets( - s3ToS3Config.source.accessKeyId, - s3ToS3Config.source.secretAccessKey, - s3ToS3Config.source.region, - s3ToS3Config.source.endpointUrl, - s3ToS3Config.source.sessionToken - ); - - if (result.success) { - s3ToS3SourceBuckets = result.buckets || []; - renderS3ToS3SourceBuckets(); - document.getElementById('s3ToS3_sourceBucketsList').style.display = 'block'; - showNotification(`✅ Found ${s3ToS3SourceBuckets.length} source bucket(s)`, 'success'); - } else { - showNotification(`❌ Failed to list source buckets: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error listing source buckets: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function testS3ToS3DestConnectionHandler() { - setLoading(true); - try { - const result = await testDestinationS3Connection({ - useEnvVars: false, - accessKeyId: s3ToS3Config.dest.accessKeyId, - secretAccessKey: s3ToS3Config.dest.secretAccessKey, - region: s3ToS3Config.dest.region, - endpointUrl: s3ToS3Config.dest.endpointUrl, - sessionToken: s3ToS3Config.dest.sessionToken - }); - - const statusDiv = document.getElementById('s3ToS3_destConnectionStatus'); - if (result.success) { - showNotification(`✅ S3 to S3 - Destination connection successful!`, 'success'); - statusDiv.innerHTML = ` -

Success: ✅ Connected

-

Endpoint: ${s3ToS3Config.dest.endpointUrl || 'AWS S3 (default)'}

-

Region: ${s3ToS3Config.dest.region}

-

Buckets Found: ${result.bucket_count || 0}

- `; - statusDiv.className = 'status-message success'; - } else { - showNotification(`❌ S3 to S3 - Destination connection failed: ${result.error}`, 'error'); - statusDiv.innerHTML = `

Error: ${result.error}

`; - statusDiv.className = 'status-message error'; - } - statusDiv.style.display = 'block'; - } catch (error) { - showNotification(`❌ Error testing destination S3 connection: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function listS3ToS3DestBucketsHandler() { - setLoading(true); - try { - const result = await listDestinationS3Buckets( - s3ToS3Config.dest.accessKeyId, - s3ToS3Config.dest.secretAccessKey, - s3ToS3Config.dest.region, - s3ToS3Config.dest.endpointUrl, - s3ToS3Config.dest.sessionToken - ); - - if (result.success) { - s3ToS3DestBuckets = result.buckets || []; - renderS3ToS3DestBuckets(); - document.getElementById('s3ToS3_destBucketsList').style.display = 'block'; - showNotification(`✅ Found ${s3ToS3DestBuckets.length} destination bucket(s)`, 'success'); - } else { - showNotification(`❌ Failed to list destination buckets: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error listing destination buckets: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function createS3ToS3DestBucketHandler() { - if (!s3ToS3Config.dest.bucket) { - showNotification('Please enter destination bucket name', 'warning'); - return; - } - - setLoading(true); - try { - const result = await createS3Bucket( - s3ToS3Config.dest.bucket, - s3ToS3Config.dest.region, - { - accessKeyId: s3ToS3Config.dest.accessKeyId, - secretAccessKey: s3ToS3Config.dest.secretAccessKey, - endpointUrl: s3ToS3Config.dest.endpointUrl, - sessionToken: s3ToS3Config.dest.sessionToken - } - ); - - if (result.success) { - if (result.created) { - showNotification(`✅ Bucket created successfully: ${s3ToS3Config.dest.bucket}`, 'success'); - } else { - showNotification(`ℹ️ Bucket already exists: ${s3ToS3Config.dest.bucket}`, 'info'); - } - } else { - showNotification(`❌ Failed to create bucket: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error creating bucket: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function startS3ToS3MigrationHandler() { - if (!s3ToS3Config.source.bucket) { - showNotification('Please select source bucket', 'warning'); - return; - } - - if (!s3ToS3Config.dest.bucket) { - showNotification('Please select destination bucket', 'warning'); - return; - } - - setLoading(true); - - try { - const includePatterns = document.getElementById('psqlToPsql_includePatterns')?.value - ?.split(',').map(p => p.trim()).filter(p => p) || null; - const excludePatterns = document.getElementById('psqlToPsql_excludePatterns')?.value - ?.split(',').map(p => p.trim()).filter(p => p) || null; - - const result = await startS3Migration( - s3ToS3Config.source.bucket, - s3ToS3Config.dest.bucket, - '', - { - sourceAccessKeyId: s3ToS3Config.source.accessKeyId, - sourceSecretAccessKey: s3ToS3Config.source.secretAccessKey, - sourceRegion: s3ToS3Config.source.region, - sourceEndpointUrl: s3ToS3Config.source.endpointUrl, - sourceSessionToken: s3ToS3Config.source.sessionToken, - - destAccessKeyId: s3ToS3Config.dest.accessKeyId, - destSecretAccessKey: s3ToS3Config.dest.secretAccessKey, - destRegion: s3ToS3Config.dest.region, - destEndpointUrl: s3ToS3Config.dest.endpointUrl, - destSessionToken: s3ToS3Config.dest.sessionToken, - - includePatterns, - excludePatterns, - preserveMetadata: document.getElementById('psqlToPsql_preserveMetadata')?.checked, - storageClass: document.getElementById('psqlToPsql_storageClass')?.value, - createDestBucket: document.getElementById('psqlToPsql_createDestBucket')?.checked, - maxConcurrent: parseInt(document.getElementById('psqlToPsql_maxConcurrent')?.value) || 5 - } - ); - - if (result.success) { - activeMigration = result.migration_id; - showNotification(`✅ S3 to S3 migration ${activeMigration} started!`, 'success'); - pollS3MigrationStatus(activeMigration); - } else { - showNotification(`❌ Failed to start migration: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error starting migration: ${error.message}`, 'error'); - } - setLoading(false); - } - - // ==================== Polling Functions ==================== - - async function pollPostgresMigrationStatus(migrationId) { - try { - const status = await getPostgresMigrationStatus(migrationId); - if (status.success && status.status) { - if (status.status.logs) { - migrationLogs = status.status.logs; - renderMigrationLogs(); - document.getElementById('migrationLogs').style.display = 'block'; - } - - if (status.status.success === true) { - showNotification(`✅ Migration ${migrationId} completed!`, 'success'); - loadMigrationsHandler(); - } else if (status.status.success === false) { - showNotification(`❌ Migration ${migrationId} failed: ${status.status.error}`, 'error'); - } else { - setTimeout(() => pollPostgresMigrationStatus(migrationId), 2000); - } - } - } catch (error) { - console.error('Error polling migration status:', error); - setTimeout(() => pollPostgresMigrationStatus(migrationId), 5000); - } - } - - async function pollS3MigrationStatus(migrationId) { - try { - const status = await getS3MigrationStatus(migrationId); - if (status.success && status.status) { - if (status.status.logs) { - migrationLogs = status.status.logs; - renderMigrationLogs(); - document.getElementById('migrationLogs').style.display = 'block'; - } - - if (status.status.success === true) { - showNotification(`✅ Migration ${migrationId} completed!`, 'success'); - loadMigrationsHandler(); - } else if (status.status.success === false) { - showNotification(`❌ Migration ${migrationId} failed: ${status.status.error}`, 'error'); - } else { - setTimeout(() => pollS3MigrationStatus(migrationId), 2000); - } - } - } catch (error) { - console.error('Error polling migration status:', error); - setTimeout(() => pollS3MigrationStatus(migrationId), 5000); - } - } - - async function pollPgToS3MigrationStatus(migrationId) { - try { - const status = await getPgToS3MigrationStatus(migrationId); - if (status.success && status.status) { - if (status.status.logs) { - migrationLogs = status.status.logs; - renderMigrationLogs(); - document.getElementById('migrationLogs').style.display = 'block'; - } - - if (status.status.success === true) { - showNotification(`✅ Migration ${migrationId} completed!`, 'success'); - loadMigrationsHandler(); - } else if (status.status.success === false) { - showNotification(`❌ Migration ${migrationId} failed: ${status.status.error}`, 'error'); - } else { - setTimeout(() => pollPgToS3MigrationStatus(migrationId), 2000); - } - } - } catch (error) { - console.error('Error polling migration status:', error); - setTimeout(() => pollPgToS3MigrationStatus(migrationId), 5000); - } - } - - // ==================== Migration Status Functions ==================== - - async function loadMigrationsHandler() { - setLoading(true); - try { - const postgresResult = await listPostgresMigrations(); - const s3Result = await listS3Migrations(); - const pgToS3Result = await listPgToS3Migrations(); - - migrations = []; - if (postgresResult.success) migrations = migrations.concat(postgresResult.migrations || []); - if (s3Result.success) migrations = migrations.concat(s3Result.migrations || []); - if (pgToS3Result.success) migrations = migrations.concat(pgToS3Result.migrations || []); - - renderMigrations(); - document.getElementById('migrationsList').style.display = 'block'; - showNotification(`✅ Found ${migrations.length} migrations`, 'success'); - } catch (error) { - console.error('Error loading migrations:', error); - showNotification(`❌ Error loading migrations: ${error.message}`, 'error'); - } - setLoading(false); - } - - // ==================== Environment Handlers ==================== - - async function loadCurrentEnvHandler() { - try { - const result = await getCurrentEnv(); - if (result.success) { - currentEnv = result.environment_variables || {}; - envPreview.textContent = formatEnvVars(currentEnv, 'dotenv'); - renderEnvConfigCards(); - showNotification('✅ Environment refreshed', 'success'); - updateStatusInfo(); - } - } catch (error) { - showNotification(`❌ Error loading environment: ${error.message}`, 'error'); - } - } -// ==================== إضافة دوال التتبع إلى App() ==================== - -// أضف هذه الدوال داخل App() بعد دوال الـ polling الحالية - -/** - * Track migration with real-time progress using Server-Sent Events - * @param {string} migrationId - Migration ID to track - * @param {string} type - Migration type ('postgres', 's3', 'postgres-s3') - */ -function trackMigrationWithProgress(migrationId, type) { - showNotification(`📊 Tracking migration ${migrationId}...`, 'info'); - - // إنشاء منطقة عرض التقدم - const progressContainer = document.createElement('div'); - progressContainer.className = 'progress-modal'; - progressContainer.innerHTML = ` - - `; - - document.body.appendChild(progressContainer); - - const closeBtn = progressContainer.querySelector('.close-modal'); - closeBtn.addEventListener('click', () => progressContainer.remove()); - - // بدء تتبع التقدم - const progressDiv = progressContainer.querySelector(`#progress-${migrationId}`); - - progressDiv.innerHTML = ` -
-
-
-
-
Connecting...
-
- -
- `; - - const progressBar = progressDiv.querySelector('.progress-bar-fill'); - const statsDiv = progressDiv.querySelector('.progress-stats'); - const detailsDiv = progressDiv.querySelector('.progress-details'); - const stopBtn = progressDiv.querySelector('.stop-progress-btn'); - - // إنشاء تدفق التقدم - const stream = createProgressStream(migrationId, type, { - onProgress: (progress) => { - // تحديث شريط التقدم - const percentage = progress.percentage || progress.percentages?.size || 0; - progressBar.style.width = `${percentage}%`; - - // تحديث الإحصائيات - statsDiv.innerHTML = formatProgressDisplay(progress).replace(/\n/g, '
'); - - // تحديث التفاصيل - let details = ''; - if (progress.current_speed_formatted || progress.speed?.current_formatted) { - details += `
⚡ Speed: ${progress.current_speed_formatted || progress.speed?.current_formatted}
`; - } - if (progress.eta_formatted || progress.time?.eta_formatted) { - details += `
⏳ ETA: ${progress.eta_formatted || progress.time?.eta_formatted}
`; - } - if (progress.elapsed_time_formatted || progress.time?.elapsed_formatted) { - details += `
⏱️ Elapsed: ${progress.elapsed_time_formatted || progress.time?.elapsed_formatted}
`; - } - detailsDiv.innerHTML = details; - }, - onComplete: (completion) => { - statsDiv.innerHTML = '✅ Migration completed successfully!'; - progressBar.style.width = '100%'; - showNotification(`Migration ${migrationId} completed!`, 'success'); - stopBtn.remove(); - - // تحديث قائمة الترحيلات - loadMigrationsHandler(); - }, - onError: (error) => { - statsDiv.innerHTML = `❌ Error: ${error.error}`; - showNotification(`Migration error: ${error.error}`, 'error'); - } - }); - - stopBtn.addEventListener('click', () => { - stream.stop(); - progressContainer.remove(); - showNotification('Progress tracking stopped', 'info'); - }); -} - -// ==================== تعديل دوال بدء الترحيل ==================== - -// استبدل دوال بدء الترحيل بهذه النسخ المعدلة: - -async function startPsqlToS3MigrationHandler() { - if (!psqlToS3Config.source.uri && !psqlToS3Config.source.host) { - showNotification('Please enter PostgreSQL connection details', 'warning'); - return; - } - - if (!psqlToS3Config.s3.bucket) { - showNotification('Please enter S3 bucket name', 'warning'); - return; - } - - setLoading(true); - - try { - const uri = psqlToS3Config.source.uri || buildPostgresConnectionString( - psqlToS3Config.source.host, - psqlToS3Config.source.database, - psqlToS3Config.source.user, - psqlToS3Config.source.port - ); - - const result = await startPgToS3Migration( - uri, - psqlToS3Config.s3.bucket, - psqlToS3Config.s3.prefix, - { - compress: psqlToS3Config.compress, - format: psqlToS3Config.format, - accessKeyId: psqlToS3Config.s3.accessKeyId, - secretAccessKey: psqlToS3Config.s3.secretAccessKey, - region: psqlToS3Config.s3.region, - endpointUrl: psqlToS3Config.s3.endpointUrl - } - ); - - if (result.success) { - activeMigration = result.migration_id; - showNotification(`✅ PostgreSQL to S3 migration ${activeMigration} started!`, 'success'); - - // استخدام التدفق المباشر بدلاً من polling - trackMigrationWithProgress(activeMigration, 'postgres-s3'); - } else { - showNotification(`❌ Failed to start migration: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error starting migration: ${error.message}`, 'error'); - } - setLoading(false); -} - -async function startPsqlToPsqlMigrationHandler() { - let sourceUri = psqlToPsqlConfig.source.uri || buildPostgresConnectionString( - psqlToPsqlConfig.source.host, - psqlToPsqlConfig.source.database, - psqlToPsqlConfig.source.user, - psqlToPsqlConfig.source.port - ); - - let destUri = psqlToPsqlConfig.dest.uri || buildPostgresConnectionString( - psqlToPsqlConfig.dest.host, - psqlToPsqlConfig.dest.database, - psqlToPsqlConfig.dest.user, - psqlToPsqlConfig.dest.port - ); - - if (!sourceUri) { - showNotification('Please enter source PostgreSQL connection details', 'warning'); - return; - } - - if (!destUri) { - showNotification('Please enter destination PostgreSQL connection details', 'warning'); - return; - } - - setLoading(true); - - try { - const result = await startPostgresMigration( - sourceUri, - destUri, - psqlToPsqlSelectedSchemas.length > 0 ? psqlToPsqlSelectedSchemas : null, - psqlToPsqlSelectedTables.length > 0 ? psqlToPsqlSelectedTables : null - ); - - if (result.success) { - activeMigration = result.migration_id; - showNotification(`✅ PostgreSQL migration ${activeMigration} started!`, 'success'); - - // استخدام التدفق المباشر بدلاً من polling - trackMigrationWithProgress(activeMigration, 'postgres'); - } else { - showNotification(`❌ Failed to start migration: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error starting migration: ${error.message}`, 'error'); - } - setLoading(false); -} - -async function startS3ToS3MigrationHandler() { - if (!s3ToS3Config.source.bucket) { - showNotification('Please select source bucket', 'warning'); - return; - } - - if (!s3ToS3Config.dest.bucket) { - showNotification('Please select destination bucket', 'warning'); - return; - } - - setLoading(true); - - try { - const includePatterns = document.getElementById('psqlToPsql_includePatterns')?.value - ?.split(',').map(p => p.trim()).filter(p => p) || null; - const excludePatterns = document.getElementById('psqlToPsql_excludePatterns')?.value - ?.split(',').map(p => p.trim()).filter(p => p) || null; - - const result = await startS3Migration( - s3ToS3Config.source.bucket, - s3ToS3Config.dest.bucket, - '', - { - sourceAccessKeyId: s3ToS3Config.source.accessKeyId, - sourceSecretAccessKey: s3ToS3Config.source.secretAccessKey, - sourceRegion: s3ToS3Config.source.region, - sourceEndpointUrl: s3ToS3Config.source.endpointUrl, - sourceSessionToken: s3ToS3Config.source.sessionToken, - - destAccessKeyId: s3ToS3Config.dest.accessKeyId, - destSecretAccessKey: s3ToS3Config.dest.secretAccessKey, - destRegion: s3ToS3Config.dest.region, - destEndpointUrl: s3ToS3Config.dest.endpointUrl, - destSessionToken: s3ToS3Config.dest.sessionToken, - - includePatterns, - excludePatterns, - preserveMetadata: document.getElementById('psqlToPsql_preserveMetadata')?.checked, - storageClass: document.getElementById('psqlToPsql_storageClass')?.value, - createDestBucket: document.getElementById('psqlToPsql_createDestBucket')?.checked, - maxConcurrent: parseInt(document.getElementById('psqlToPsql_maxConcurrent')?.value) || 5 - } - ); - - if (result.success) { - activeMigration = result.migration_id; - showNotification(`✅ S3 to S3 migration ${activeMigration} started!`, 'success'); - - // استخدام التدفق المباشر بدلاً من polling - trackMigrationWithProgress(activeMigration, 's3'); - } else { - showNotification(`❌ Failed to start migration: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error starting migration: ${error.message}`, 'error'); - } - setLoading(false); -} - async function injectEnvironmentHandler() { - setLoading(true); - try { - const envVars = { - // PSQL to PSQL Source - SOURCE_PG_HOST: psqlToPsqlConfig.source.host, - SOURCE_PG_PORT: psqlToPsqlConfig.source.port.toString(), - SOURCE_PG_USER: psqlToPsqlConfig.source.user, - SOURCE_PG_PASSWORD: psqlToPsqlConfig.source.password, - SOURCE_PG_DATABASE: psqlToPsqlConfig.source.database, - - // PSQL to PSQL Destination - DEST_PG_HOST: psqlToPsqlConfig.dest.host, - DEST_PG_PORT: psqlToPsqlConfig.dest.port.toString(), - DEST_PG_USER: psqlToPsqlConfig.dest.user, - DEST_PG_PASSWORD: psqlToPsqlConfig.dest.password, - DEST_PG_DATABASE: psqlToPsqlConfig.dest.database, - - // S3 Source - SOURCE_AWS_ACCESS_KEY_ID: s3ToS3Config.source.accessKeyId, - SOURCE_AWS_SECRET_ACCESS_KEY: s3ToS3Config.source.secretAccessKey, - SOURCE_AWS_REGION: s3ToS3Config.source.region, - SOURCE_AWS_ENDPOINT_URL: s3ToS3Config.source.endpointUrl, - SOURCE_S3_BUCKET: s3ToS3Config.source.bucket, - - // S3 Destination - DEST_AWS_ACCESS_KEY_ID: s3ToS3Config.dest.accessKeyId, - DEST_AWS_SECRET_ACCESS_KEY: s3ToS3Config.dest.secretAccessKey, - DEST_AWS_REGION: s3ToS3Config.dest.region, - DEST_AWS_ENDPOINT_URL: s3ToS3Config.dest.endpointUrl, - DEST_S3_BUCKET: s3ToS3Config.dest.bucket - }; - - const result = await injectEnv(envVars); - if (result.success) { - showNotification(`✅ Injected ${result.injected_variables?.length || 0} environment variables`, 'success'); - loadCurrentEnvHandler(); - } else { - showNotification(`❌ Failed to inject environment: ${result.error}`, 'error'); - } - } catch (error) { - showNotification(`❌ Error injecting environment: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function clearEnvironmentHandler() { - try { - const result = await injectEnv({}); - if (result.success) { - showNotification('✅ Environment cleared', 'success'); - loadCurrentEnvHandler(); - } - } catch (error) { - showNotification(`❌ Error clearing environment: ${error.message}`, 'error'); - } - } - - async function copyEnvToClipboardHandler() { - if (!currentEnv || Object.keys(currentEnv).length === 0) { - showNotification('No environment variables to copy', 'warning'); - return; - } - - const format = document.getElementById('envFormat')?.value || 'dotenv'; - const formatted = formatEnvVars(currentEnv, format); - const success = await copyToClipboard(formatted); - if (success) { - showNotification(`✅ Copied ${Object.keys(currentEnv).length} variables to clipboard`, 'success'); - } else { - showNotification(`❌ Failed to copy to clipboard`, 'error'); - } - } - - // ==================== Security Handlers ==================== - - async function clearSessionHandler() { - setLoading(true); - try { - const result = await clearSession(); - if (result.success) { - showNotification(`✅ ${result.message}`, 'success'); - // Reset all configs - psqlToS3Config = { source: { host: '', user: '', password: '', port: 5432, database: '', uri: '' }, s3: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1', endpointUrl: '', bucket: '', prefix: '' }, compress: true, format: 'csv' }; - psqlToPsqlConfig = { source: { host: '', user: '', password: '', port: 5432, database: '', uri: '' }, dest: { host: '', user: '', password: '', port: 5432, database: '', uri: '' } }; - s3ToS3Config = { source: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1', endpointUrl: '', sessionToken: '', bucket: 'my-source-bucket' }, dest: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1', endpointUrl: '', sessionToken: '', bucket: 'my-destination-bucket' } }; - currentEnv = {}; - envPreview.textContent = ''; - updateStatusInfo(); - loadSecurityStatus(); - } else { - showNotification(`❌ Failed to clear session`, 'error'); - } - } catch (error) { - showNotification(`❌ Error clearing session: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function clearMigrationHandler() { - const migrationId = document.getElementById('cleanupMigrationId')?.value; - if (!migrationId) { - showNotification('Please enter migration ID', 'warning'); - return; - } - - setLoading(true); - try { - const result = await clearMigration(migrationId); - if (result.success) { - showNotification(`✅ ${result.message}`, 'success'); - document.getElementById('cleanupMigrationId').value = ''; - } else { - showNotification(`❌ Failed to clear migration data`, 'error'); - } - } catch (error) { - showNotification(`❌ Error clearing migration: ${error.message}`, 'error'); - } - setLoading(false); - } - - async function loadSecurityStatus() { - try { - const result = await getSecurityStatus(); - if (result.success) { - document.getElementById('sessionId').textContent = result.security_status.current_session_id || 'Unknown'; - document.getElementById('activeSessions').textContent = result.security_status.active_sessions || 0; - } - } catch (error) { - console.error('Error loading security status:', error); - } - } - - // ==================== Render Functions ==================== - - function renderPsqlToS3Schemas(schemas) { - const container = document.getElementById('psqlToS3_schemasContainer'); - container.innerHTML = schemas.map(schema => ` -
- -
- `).join(''); - } - - function renderPsqlToS3Tables(tables) { - const container = document.getElementById('psqlToS3_tablesContainer'); - const groups = groupTablesBySchema(tables); - - let html = ''; - Object.entries(groups).forEach(([schema, group]) => { - html += ` -
-

📚 Schema: ${schema}

-
- ${group.count} tables -
-
- - - - - - - - - - ${group.tables.map(table => ` - - - - - - `).join('')} - -
- - Table NameType
- - ${table.name}${table.type}
-
-
- `; - }); - container.innerHTML = html; - } - - function renderPsqlToPsqlSchemas() { - const container = document.getElementById('psqlToPsql_schemasContainer'); - container.innerHTML = psqlToPsqlSchemas.map(schema => ` -
- -
- `).join(''); - - document.querySelectorAll('#psqlToPsql_schemasContainer .schema-checkbox').forEach(checkbox => { - checkbox.addEventListener('change', (e) => { - if (e.target.checked) { - psqlToPsqlSelectedSchemas.push(e.target.value); - } else { - psqlToPsqlSelectedSchemas = psqlToPsqlSelectedSchemas.filter(s => s !== e.target.value); - } - }); - }); - } - - function renderPsqlToPsqlTables() { - const container = document.getElementById('psqlToPsql_tablesContainer'); - const groups = groupTablesBySchema(psqlToPsqlTables); - - let html = ''; - Object.entries(groups).forEach(([schema, group]) => { - html += ` -
-

📚 Schema: ${schema}

-
- ${group.count} tables -
-
- - - - - - - - - - ${group.tables.map(table => ` - - - - - - `).join('')} - -
- - Table NameType
- - ${table.name}${table.type}
-
-
- `; - }); - container.innerHTML = html; - - document.querySelectorAll('#psqlToPsql_tablesContainer .table-checkbox').forEach(checkbox => { - checkbox.addEventListener('change', (e) => { - const tableName = e.target.value; - if (e.target.checked) { - if (!psqlToPsqlSelectedTables.includes(tableName)) { - psqlToPsqlSelectedTables.push(tableName); - } - } else { - psqlToPsqlSelectedTables = psqlToPsqlSelectedTables.filter(t => t !== tableName); - } - }); - }); - } - - function renderS3ToS3SourceBuckets() { - const container = document.getElementById('s3ToS3_sourceBucketsContainer'); - container.innerHTML = s3ToS3SourceBuckets.map(bucket => ` -
-
${bucket.name}
-
- Region: ${bucket.region} - Created: ${new Date(bucket.creation_date).toLocaleDateString()} - Objects: ${bucket.object_count} - Size: ${formatFileSize(bucket.total_size)} -
- -
- `).join(''); - - document.querySelectorAll('#s3ToS3_sourceBucketsContainer .select-bucket-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - const bucketName = e.target.dataset.bucket; - s3ToS3Config.source.bucket = bucketName; - document.getElementById('s3ToS3_sourceBucket').value = bucketName; - updateS3Summaries(); - showNotification(`✅ Selected source bucket: ${bucketName}`, 'success'); - document.getElementById('s3ToS3_sourceBucketsList').style.display = 'none'; - }); - }); - } - - function renderS3ToS3DestBuckets() { - const container = document.getElementById('s3ToS3_destBucketsContainer'); - container.innerHTML = s3ToS3DestBuckets.map(bucket => ` -
-
${bucket.name}
-
- Region: ${bucket.region} - Created: ${new Date(bucket.creation_date).toLocaleDateString()} - Objects: ${bucket.object_count} - Size: ${formatFileSize(bucket.total_size)} -
- -
- `).join(''); - - document.querySelectorAll('#s3ToS3_destBucketsContainer .select-bucket-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - const bucketName = e.target.dataset.bucket; - s3ToS3Config.dest.bucket = bucketName; - document.getElementById('s3ToS3_destBucket').value = bucketName; - updateS3Summaries(); - showNotification(`✅ Selected destination bucket: ${bucketName}`, 'success'); - document.getElementById('s3ToS3_destBucketsList').style.display = 'none'; - }); - }); - } - - function renderMigrations() { - const container = document.getElementById('migrationsContainer'); - if (!container) return; - - if (migrations.length === 0) { - container.innerHTML = '

No migrations found

'; - return; - } - - container.innerHTML = migrations.map(migration => ` -
-
- ${migration.id} - ${migration.status} -
-
-

Status: ${migration.status}

-

Started: ${migration.started_at ? new Date(migration.started_at * 1000).toLocaleString() : 'N/A'}

-
-
- - ${migration.status === 'running' ? ` - - ` : ''} -
-
- `).join(''); - - document.querySelectorAll('.view-logs-btn').forEach(btn => { - btn.addEventListener('click', async (e) => { - const migrationId = e.target.dataset.id; - setActiveMigration(migrationId); - }); - }); - } - - function renderMigrationLogs() { - const container = document.getElementById('logsContainer'); - if (!container) return; - - if (!migrationLogs || migrationLogs.length === 0) { - container.innerHTML = '

No logs available

'; - return; - } - - container.innerHTML = migrationLogs.map(log => ` -
- ${log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : ''} - ${log.message} -
- `).join(''); - } - - function renderEnvConfigCards() { - const container = document.getElementById('envConfigCards'); - if (!container) return; - - container.innerHTML = ` -
-

🐘 PSQL to PSQL Source

-
${formatPostgresConfig(psqlToPsqlConfig.source)}
-
-
-

🐘 PSQL to PSQL Dest

-
${formatPostgresConfig(psqlToPsqlConfig.dest)}
-
-
-

📤 S3 to S3 Source

-
${formatS3Config({ 
-          endpoint_url: s3ToS3Config.source.endpointUrl,
-          region: s3ToS3Config.source.region,
-          bucket: s3ToS3Config.source.bucket,
-          access_key_id: s3ToS3Config.source.accessKeyId,
-          secret_access_key: s3ToS3Config.source.secretAccessKey
-        }, 'source')}
-
-
-

📥 S3 to S3 Dest

-
${formatS3Config({ 
-          endpoint_url: s3ToS3Config.dest.endpointUrl,
-          region: s3ToS3Config.dest.region,
-          bucket: s3ToS3Config.dest.bucket,
-          access_key_id: s3ToS3Config.dest.accessKeyId,
-          secret_access_key: s3ToS3Config.dest.secretAccessKey
-        }, 'destination')}
-
- `; - } - - // ==================== Additional Handlers ==================== - - async function setActiveMigration(migrationId) { - activeMigration = migrationId; - setLoading(true); - try { - let result = await getPostgresMigrationStatus(migrationId); - if (!result.success) { - result = await getS3MigrationStatus(migrationId); - } - if (!result.success) { - result = await getPgToS3MigrationStatus(migrationId); - } - - if (result.success && result.status) { - // Switch to environment tab where migrations are shown - document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); - document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); - document.querySelector('[data-tab="environment"]').classList.add('active'); - document.getElementById('tab-environment').classList.add('active'); - - if (result.status.logs && Array.isArray(result.status.logs)) { - migrationLogs = result.status.logs; - renderMigrationLogs(); - document.getElementById('migrationLogs').style.display = 'block'; - } - - showNotification(`✅ Loaded logs for migration ${migrationId}`, 'success'); - } - } catch (error) { - console.error('Error fetching migration logs:', error); - showNotification(`❌ Error loading migration logs: ${error.message}`, 'error'); - } - setLoading(false); - } - - // ==================== Initialize ==================== - function init() { - setupEventListeners(); - - // Set initial summaries - updatePostgresSummaries(); - updateS3Summaries(); - - // Load initial data - loadCurrentEnvHandler(); - loadSecurityStatus(); - updateStatusInfo(); - } - - init(); -} - -// ==================== Start the app ==================== -document.addEventListener("DOMContentLoaded", App); \ No newline at end of file +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); \ No newline at end of file