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}
+
+ ))}
+
+
+
+
+
+ {/* 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
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* ==================== Source S3 Tab ==================== */}
+ {activeTab === 's3-source' && (
+
+
📤 Source S3 Configuration
+
+
+
AWS 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
+
+
+
+
+
+
+
+ {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
+
+
+
+ {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)}
+
+
+
+ ))}
+
+ )}
+
+ {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
+
+
+
+
+
+
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 = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
🐘 → 📤 PostgreSQL to S3 Migration
-
-
-
-
-
-
PostgreSQL Source
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
S3 Destination
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
🐘 → 🐘 PostgreSQL to PostgreSQL Migration
-
-
-
-
-
-
Source PostgreSQL
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Destination PostgreSQL
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Migration Options
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
📤 → 📥 S3 to S3 Migration
-
-
-
-
-
-
Source S3
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Available Buckets:
-
-
-
-
-
-
-
-
-
Destination S3
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Available Buckets:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
⚙️ 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
-
⏳ Processing...
-
-
- `;
-
- // ==================== 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
-
-
-
- `;
- });
- 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
-
-
-
- `;
- });
- 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 => `
-
-
-
-
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