الملفات
psqlmigrator/App.jsx

2432 أسطر
122 KiB
JavaScript
خام الرابط الدائم اللوم التاريخ

هذا الملف يحتوي على أحرف Unicode غير مرئية

هذا الملف يحتوي على أحرف Unicode غير مرئية لا يمكن التمييز بينها بعين الإنسان ولكن قد تتم معالجتها بشكل مختلف بواسطة الحاسوب. إذا كنت تعتقد أن هذا مقصود، يمكنك تجاهل هذا التحذير بأمان. استخدم زر الهروب للكشف عنها.

هذا الملف يحتوي على أحرف Unicode قد تُخلط مع أحرف أخرى. إذا كنت تعتقد أن هذا مقصود، يمكنك تجاهل هذا التحذير بأمان. استخدم زر الهروب للكشف عنها.

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 = `
<div class="progress-bar-fill" style="width: ${percentage}%"></div>
<span class="progress-text">${percentage.toFixed(1)}%</span>
`;
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 (
<div className="progress-modal">
<div className="modal-content">
<div className="modal-header">
<h3>
<span className="migration-status-badge" style={{
backgroundColor: progress.status === 'running' ? '#4CAF50' :
progress.status === 'completed' ? '#2196F3' :
progress.status === 'error' ? '#f44336' : '#FF9800'
}}></span>
Migration Progress: {progress.migrationId}
</h3>
<button className="close-modal" onClick={stopProgressTracking}>&times;</button>
</div>
<div className="modal-body">
<div className="progress-container">
<div className="progress-bar-container">
<div
className="progress-bar-fill"
style={{ width: `${percentage}%` }}
/>
<span className="progress-bar-label">{percentage.toFixed(1)}%</span>
</div>
<pre className="progress-bar-text">{bar}</pre>
<div className="progress-stats-grid">
{progress.processed_size_formatted && progress.total_size_formatted && (
<div className="stat-item">
<span className="stat-label">Size</span>
<span className="stat-value">{progress.processed_size_formatted} / {progress.total_size_formatted}</span>
</div>
)}
{progress.processed_objects !== undefined && progress.total_objects !== undefined && (
<div className="stat-item">
<span className="stat-label">Objects</span>
<span className="stat-value">{progress.processed_objects} / {progress.total_objects}</span>
</div>
)}
{progress.processed?.tables !== undefined && progress.total?.tables !== undefined && (
<div className="stat-item">
<span className="stat-label">Tables</span>
<span className="stat-value">{progress.processed.tables} / {progress.total.tables}</span>
</div>
)}
{progress.current_speed_formatted && (
<div className="stat-item">
<span className="stat-label">Speed</span>
<span className="stat-value">{progress.current_speed_formatted}</span>
</div>
)}
{progress.eta_formatted && (
<div className="stat-item">
<span className="stat-label">ETA</span>
<span className="stat-value">{progress.eta_formatted}</span>
</div>
)}
{progress.elapsed_time_formatted && (
<div className="stat-item">
<span className="stat-label">Elapsed</span>
<span className="stat-value">{progress.elapsed_time_formatted}</span>
</div>
)}
</div>
{(progress.current_object || progress.current_table) && (
<div className="current-item">
<h4>Currently Processing:</h4>
{progress.current_object && (
<div className="current-object">
<span className="current-icon">📄</span>
<span className="current-name">{progress.current_object}</span>
</div>
)}
{progress.current_table && (
<div className="current-table">
<span className="current-icon">📊</span>
<span className="current-name">{progress.current_table.name}</span>
{progress.current_table.rows && (
<span className="current-progress">
{progress.current_table.rows_processed} / {progress.current_table.rows} rows
</span>
)}
</div>
)}
</div>
)}
{progress.logs && progress.logs.length > 0 && (
<div className="progress-logs">
<h4>Activity Log</h4>
<div className="logs-container">
{progress.logs.slice(-10).map((log, index) => (
<div key={index} className={`log-entry ${log.level || 'info'}`}>
<span className="log-time">
{new Date(log.timestamp).toLocaleTimeString()}
</span>
<span className="log-message">{log.message}</span>
</div>
))}
</div>
</div>
)}
<div className="progress-controls">
{progress.status === 'running' && (
<button onClick={stopProgressTracking} className="btn btn-warning">
Stop Tracking
</button>
)}
{progress.status === 'completed' && (
<button onClick={() => setShowProgressModal(false)} className="btn btn-primary">
Close
</button>
)}
</div>
</div>
</div>
</div>
</div>
);
};
const renderProgressHistory = () => {
if (progressHistory.length === 0) return null;
return (
<div className="progress-history">
<h4>Recent Migrations</h4>
<div className="history-list">
{progressHistory.slice(-5).reverse().map((item, index) => (
<div key={index} className={`history-item ${item.status}`}>
<span className="history-id">{item.migrationId}</span>
<span className="history-type">{item.type}</span>
<span className="history-status">{item.status}</span>
<span className="history-time">
{new Date(item.completedAt).toLocaleTimeString()}
</span>
{item.totalTime && (
<span className="history-time">{formatTimeEstimate(item.totalTime)}</span>
)}
</div>
))}
</div>
</div>
);
};
// ============================================================================
// Render
// ============================================================================
return (
<div className="App">
{/* Notifications */}
<div className="notifications">
{notifications.map(notification => (
<div key={notification.id} className={`notification ${notification.type}`}>
{notification.message}
</div>
))}
</div>
<header className="App-header">
<h1>🔄 Unified Migration Tool</h1>
<p>PostgreSQL, S3 to S3, and PostgreSQL to S3 migrations</p>
</header>
<div className="container">
{/* Tabs */}
<div className="tabs">
<button className={activeTab === 'postgres' ? 'active' : ''} onClick={() => setActiveTab('postgres')}>
🐘 PostgreSQL
</button>
<button className={activeTab === 's3-source' ? 'active' : ''} onClick={() => setActiveTab('s3-source')}>
📤 Source S3
</button>
<button className={activeTab === 's3-dest' ? 'active' : ''} onClick={() => setActiveTab('s3-dest')}>
📥 Destination S3
</button>
<button className={activeTab === 's3-browse' ? 'active' : ''} onClick={() => setActiveTab('s3-browse')}>
📁 Browse S3
</button>
<button className={activeTab === 'pg-to-s3' ? 'active' : ''} onClick={() => setActiveTab('pg-to-s3')}>
🔄 PostgreSQL S3
</button>
<button className={activeTab === 'migration' ? 'active' : ''} onClick={() => setActiveTab('migration')}>
🚀 Migrations
</button>
<button className={activeTab === 'environment' ? 'active' : ''} onClick={() => setActiveTab('environment')}>
Environment
</button>
</div>
{/* ==================== PostgreSQL Tab ==================== */}
{activeTab === 'postgres' && (
<div className="tab-content">
<h2>🐘 PostgreSQL Configuration</h2>
<div className="migration-layout">
<div className="migration-column">
<div className="config-section">
<h3>Source PostgreSQL</h3>
<div className="config-grid">
<div className="config-group">
<label>Host:</label>
<input
type="text"
value={postgresConfig.host}
onChange={(e) => setPostgresConfig(prev => ({ ...prev, host: e.target.value, uri: '' }))}
placeholder="localhost"
/>
</div>
<div className="config-group">
<label>Port:</label>
<input
type="number"
value={postgresConfig.port}
onChange={(e) => setPostgresConfig(prev => ({ ...prev, port: parseInt(e.target.value) || 5432, uri: '' }))}
placeholder="5432"
/>
</div>
<div className="config-group">
<label>User:</label>
<input
type="text"
value={postgresConfig.user}
onChange={(e) => setPostgresConfig(prev => ({ ...prev, user: e.target.value, uri: '' }))}
placeholder="postgres"
/>
</div>
<div className="config-group">
<label>Password:</label>
<input
type="password"
value={postgresConfig.password}
onChange={(e) => setPostgresConfig(prev => ({ ...prev, password: e.target.value, uri: '' }))}
placeholder="••••••••"
/>
</div>
<div className="config-group">
<label>Database:</label>
<input
type="text"
value={postgresConfig.database}
onChange={(e) => setPostgresConfig(prev => ({ ...prev, database: e.target.value, uri: '' }))}
placeholder="postgres"
/>
</div>
</div>
<div className="uri-input-group">
<label>Or paste URI:</label>
<input
type="text"
value={postgresConfig.uri}
onChange={(e) => setPostgresConfig(prev => ({ ...prev, uri: e.target.value }))}
placeholder="postgresql://user:pass@host:5432/database"
className="uri-input"
/>
<button onClick={() => parsePostgresUri(postgresConfig.uri, true)} className="btn-small">
Parse
</button>
</div>
<div className="action-group">
<button onClick={() => testPostgresConnection(true)} disabled={loading} className="btn btn-test">
Test Source
</button>
<button onClick={() => getPostgresSchemas(true)} disabled={loading} className="btn btn-primary">
📚 Get Schemas
</button>
<button onClick={() => getPostgresTables('', true)} disabled={loading} className="btn btn-primary">
📊 Get Tables
</button>
</div>
{postgresConnectionStatus && (
<div className={`status-message ${postgresConnectionStatus.success ? 'success' : 'error'}`}>
<h4>Source Connection Status:</h4>
<p><strong>Success:</strong> {postgresConnectionStatus.success ? ' Yes' : ' No'}</p>
{postgresConnectionStatus.host && <p><strong>Host:</strong> {postgresConnectionStatus.host}:{postgresConnectionStatus.port}</p>}
{postgresConnectionStatus.version && <p><strong>Version:</strong> {postgresConnectionStatus.version}</p>}
{postgresConnectionStatus.database && <p><strong>Database:</strong> {postgresConnectionStatus.database}</p>}
{postgresConnectionStatus.error && <p><strong>Error:</strong> {postgresConnectionStatus.error}</p>}
{postgresConnectionStatus.diagnostic && (
<div className="diagnostic">
<p><strong>Diagnostic:</strong> {postgresConnectionStatus.diagnostic.message}</p>
<p><strong>Reason:</strong> {postgresConnectionStatus.diagnostic.reason}</p>
</div>
)}
</div>
)}
</div>
</div>
<div className="migration-column">
<div className="config-section">
<h3>Destination PostgreSQL</h3>
<div className="config-grid">
<div className="config-group">
<label>Host:</label>
<input
type="text"
value={destPostgresConfig.host}
onChange={(e) => setDestPostgresConfig(prev => ({ ...prev, host: e.target.value, uri: '' }))}
placeholder="localhost"
/>
</div>
<div className="config-group">
<label>Port:</label>
<input
type="number"
value={destPostgresConfig.port}
onChange={(e) => setDestPostgresConfig(prev => ({ ...prev, port: parseInt(e.target.value) || 5432, uri: '' }))}
placeholder="5432"
/>
</div>
<div className="config-group">
<label>User:</label>
<input
type="text"
value={destPostgresConfig.user}
onChange={(e) => setDestPostgresConfig(prev => ({ ...prev, user: e.target.value, uri: '' }))}
placeholder="postgres"
/>
</div>
<div className="config-group">
<label>Password:</label>
<input
type="password"
value={destPostgresConfig.password}
onChange={(e) => setDestPostgresConfig(prev => ({ ...prev, password: e.target.value, uri: '' }))}
placeholder="••••••••"
/>
</div>
<div className="config-group">
<label>Database:</label>
<input
type="text"
value={destPostgresConfig.database}
onChange={(e) => setDestPostgresConfig(prev => ({ ...prev, database: e.target.value, uri: '' }))}
placeholder="postgres"
/>
</div>
</div>
<div className="uri-input-group">
<label>Or paste URI:</label>
<input
type="text"
value={destPostgresConfig.uri}
onChange={(e) => setDestPostgresConfig(prev => ({ ...prev, uri: e.target.value }))}
placeholder="postgresql://user:pass@host:5432/database"
className="uri-input"
/>
<button onClick={() => parsePostgresUri(destPostgresConfig.uri, false)} className="btn-small">
Parse
</button>
</div>
<div className="action-group">
<button onClick={() => testPostgresConnection(false)} disabled={loading} className="btn btn-test">
Test Destination
</button>
</div>
{destPostgresConnectionStatus && (
<div className={`status-message ${destPostgresConnectionStatus.success ? 'success' : 'error'}`}>
<h4>Destination Connection Status:</h4>
<p><strong>Success:</strong> {destPostgresConnectionStatus.success ? ' Yes' : ' No'}</p>
{destPostgresConnectionStatus.host && <p><strong>Host:</strong> {destPostgresConnectionStatus.host}:{destPostgresConnectionStatus.port}</p>}
{destPostgresConnectionStatus.version && <p><strong>Version:</strong> {destPostgresConnectionStatus.version}</p>}
{destPostgresConnectionStatus.database && <p><strong>Database:</strong> {destPostgresConnectionStatus.database}</p>}
{destPostgresConnectionStatus.error && <p><strong>Error:</strong> {destPostgresConnectionStatus.error}</p>}
{destPostgresConnectionStatus.diagnostic && (
<div className="diagnostic">
<p><strong>Diagnostic:</strong> {destPostgresConnectionStatus.diagnostic.message}</p>
<p><strong>Reason:</strong> {destPostgresConnectionStatus.diagnostic.reason}</p>
</div>
)}
</div>
)}
</div>
</div>
</div>
{postgresSchemas.length > 0 && (
<div className="schemas-section">
<h3>Available Schemas</h3>
<div className="schemas-grid">
{postgresSchemas.map((schema, index) => (
<div key={index} className="schema-card">
<label>
<input
type="checkbox"
checked={selectedSchemas.includes(schema)}
onChange={() => toggleSchemaSelection(schema)}
/>
{schema}
</label>
<button onClick={() => getPostgresTables(schema, true)} className="btn-small">
View Tables
</button>
</div>
))}
</div>
</div>
)}
{postgresTables.length > 0 && (
<div className="tables-section">
<h3>Tables in Source Database</h3>
<div className="tables-header">
<div className="table-actions">
<button onClick={selectAllTables} className="btn-small">
Select All
</button>
<button onClick={clearSelectedTables} className="btn-small">
Clear Selection
</button>
</div>
</div>
{Object.entries(tableGroups).map(([schema, group]) => (
<div key={schema} className="table-group">
<h4>📚 Schema: {schema}</h4>
<div className="group-stats">
<span>{group.count} tables</span>
</div>
<table className="tables-table">
<thead>
<tr>
<th style={{ width: '30px' }}>
<input
type="checkbox"
onChange={(e) => {
const groupTables = group.tables.map(t => t.name);
if (e.target.checked) {
setSelectedTables([...new Set([...selectedTables, ...groupTables])]);
} else {
setSelectedTables(selectedTables.filter(name => !groupTables.includes(name)));
}
}}
checked={group.tables.every(t => selectedTables.includes(t.name))}
/>
</th>
<th>Table Name</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{group.tables.map((table, index) => (
<tr key={index} className={selectedTables.includes(table.name) ? 'selected' : ''}>
<td>
<input
type="checkbox"
checked={selectedTables.includes(table.name)}
onChange={() => toggleTableSelection(table.name)}
/>
</td>
<td className="table-name">{table.name}</td>
<td className="table-type">{table.type}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
)}
</div>
)}
{/* ==================== Source S3 Tab ==================== */}
{activeTab === 's3-source' && (
<div className="tab-content">
<h2>📤 Source S3 Configuration</h2>
<div className="connection-section">
<h3>AWS Credentials</h3>
<div className="config-grid">
<div className="config-group">
<label>Access Key ID:</label>
<input
type="password"
value={sourceS3Config.accessKeyId}
onChange={(e) => setSourceS3Config(prev => ({ ...prev, accessKeyId: e.target.value }))}
placeholder="AKIAIOSFODNN7EXAMPLE"
/>
</div>
<div className="config-group">
<label>Secret Access Key:</label>
<input
type="password"
value={sourceS3Config.secretAccessKey}
onChange={(e) => setSourceS3Config(prev => ({ ...prev, secretAccessKey: e.target.value }))}
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
/>
</div>
<div className="config-group">
<label>Region:</label>
<input
type="text"
value={sourceS3Config.region}
onChange={(e) => setSourceS3Config(prev => ({ ...prev, region: e.target.value }))}
placeholder="us-east-1"
/>
</div>
<div className="config-group">
<label>Endpoint URL:</label>
<input
type="text"
value={sourceS3Config.endpointUrl}
onChange={(e) => setSourceS3Config(prev => ({ ...prev, endpointUrl: e.target.value }))}
placeholder="https://s3.amazonaws.com (optional)"
/>
</div>
<div className="config-group">
<label>Session Token:</label>
<input
type="password"
value={sourceS3Config.sessionToken}
onChange={(e) => setSourceS3Config(prev => ({ ...prev, sessionToken: e.target.value }))}
placeholder="Optional for temporary credentials"
/>
</div>
</div>
<div className="s3-actions">
<button onClick={testSourceS3Connection} disabled={loading} className="btn btn-test">
Test Source Connection
</button>
<button onClick={listSourceBuckets} disabled={loading} className="btn btn-primary">
📁 List Buckets
</button>
</div>
{sourceS3ConnectionStatus && (
<div className={`status-message ${sourceS3ConnectionStatus.success ? 'success' : 'error'}`}>
<h4>Source Connection Status:</h4>
<p><strong>Success:</strong> {sourceS3ConnectionStatus.success ? ' Yes' : ' No'}</p>
{sourceS3ConnectionStatus.endpoint && <p><strong>Endpoint:</strong> {sourceS3ConnectionStatus.endpoint}</p>}
{sourceS3ConnectionStatus.region && <p><strong>Region:</strong> {sourceS3ConnectionStatus.region}</p>}
{sourceS3ConnectionStatus.bucketCount !== undefined && <p><strong>Buckets Found:</strong> {sourceS3ConnectionStatus.bucketCount}</p>}
{sourceS3ConnectionStatus.error && <p><strong>Error:</strong> {sourceS3ConnectionStatus.error}</p>}
{sourceS3ConnectionStatus.diagnostic && (
<div className="diagnostic">
<p><strong>Diagnostic:</strong> {sourceS3ConnectionStatus.diagnostic.message}</p>
<p><strong>Reason:</strong> {sourceS3ConnectionStatus.diagnostic.reason}</p>
</div>
)}
</div>
)}
</div>
<div className="bucket-section">
<h3>Source Bucket Selection</h3>
<div className="s3-input-group">
<input
type="text"
placeholder="Source bucket name"
value={selectedSourceBucket}
onChange={(e) => setSelectedSourceBucket(e.target.value)}
className="bucket-input"
/>
</div>
{showSourceBuckets && sourceBuckets.length > 0 && (
<div className="buckets-list">
<h4>Available Source Buckets:</h4>
<div className="buckets-grid">
{sourceBuckets.map((bucket, index) => (
<div key={index} className="bucket-card" onClick={() => selectSourceBucket(bucket.name)}>
<div className="bucket-name">{bucket.name}</div>
<div className="bucket-details">
<span className="bucket-region">Region: {bucket.region}</span>
<span className="bucket-created">Created: {new Date(bucket.creation_date).toLocaleDateString()}</span>
<span className="bucket-size">Objects: {bucket.object_count}</span>
<span className="bucket-size">Size: {formatFileSize(bucket.total_size)}</span>
</div>
</div>
))}
</div>
<button onClick={() => setShowSourceBuckets(false)} className="btn-small">
Hide Buckets
</button>
</div>
)}
</div>
</div>
)}
{/* ==================== Destination S3 Tab ==================== */}
{activeTab === 's3-dest' && (
<div className="tab-content">
<h2>📥 Destination S3 Configuration</h2>
<div className="connection-section">
<h3>AWS Credentials</h3>
<div className="config-grid">
<div className="config-group">
<label>Access Key ID:</label>
<input
type="password"
value={destS3Config.accessKeyId}
onChange={(e) => setDestS3Config(prev => ({ ...prev, accessKeyId: e.target.value }))}
placeholder="AKIAIOSFODNN7EXAMPLE"
/>
</div>
<div className="config-group">
<label>Secret Access Key:</label>
<input
type="password"
value={destS3Config.secretAccessKey}
onChange={(e) => setDestS3Config(prev => ({ ...prev, secretAccessKey: e.target.value }))}
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
/>
</div>
<div className="config-group">
<label>Region:</label>
<input
type="text"
value={destS3Config.region}
onChange={(e) => setDestS3Config(prev => ({ ...prev, region: e.target.value }))}
placeholder="us-east-1"
/>
</div>
<div className="config-group">
<label>Endpoint URL:</label>
<input
type="text"
value={destS3Config.endpointUrl}
onChange={(e) => setDestS3Config(prev => ({ ...prev, endpointUrl: e.target.value }))}
placeholder="https://s3.amazonaws.com (optional)"
/>
</div>
<div className="config-group">
<label>Session Token:</label>
<input
type="password"
value={destS3Config.sessionToken}
onChange={(e) => setDestS3Config(prev => ({ ...prev, sessionToken: e.target.value }))}
placeholder="Optional for temporary credentials"
/>
</div>
</div>
<div className="s3-actions">
<button onClick={testDestinationS3Connection} disabled={loading} className="btn btn-test">
Test Destination Connection
</button>
<button onClick={listDestinationBuckets} disabled={loading} className="btn btn-primary">
📁 List Buckets
</button>
</div>
{destS3ConnectionStatus && (
<div className={`status-message ${destS3ConnectionStatus.success ? 'success' : 'error'}`}>
<h4>Destination Connection Status:</h4>
<p><strong>Success:</strong> {destS3ConnectionStatus.success ? ' Yes' : ' No'}</p>
{destS3ConnectionStatus.endpoint && <p><strong>Endpoint:</strong> {destS3ConnectionStatus.endpoint}</p>}
{destS3ConnectionStatus.region && <p><strong>Region:</strong> {destS3ConnectionStatus.region}</p>}
{destS3ConnectionStatus.bucketCount !== undefined && <p><strong>Buckets Found:</strong> {destS3ConnectionStatus.bucketCount}</p>}
{destS3ConnectionStatus.error && <p><strong>Error:</strong> {destS3ConnectionStatus.error}</p>}
{destS3ConnectionStatus.diagnostic && (
<div className="diagnostic">
<p><strong>Diagnostic:</strong> {destS3ConnectionStatus.diagnostic.message}</p>
<p><strong>Reason:</strong> {destS3ConnectionStatus.diagnostic.reason}</p>
</div>
)}
</div>
)}
</div>
<div className="bucket-section">
<h3>Destination Bucket</h3>
<div className="s3-input-group">
<input
type="text"
placeholder="Destination bucket name"
value={selectedDestBucket}
onChange={(e) => setSelectedDestBucket(e.target.value)}
className="bucket-input"
/>
<button onClick={createDestinationBucket} disabled={loading} className="btn btn-success">
Create Bucket
</button>
</div>
{showDestBuckets && destBuckets.length > 0 && (
<div className="buckets-list">
<h4>Available Destination Buckets:</h4>
<div className="buckets-grid">
{destBuckets.map((bucket, index) => (
<div key={index} className="bucket-card" onClick={() => selectDestBucket(bucket.name)}>
<div className="bucket-name">{bucket.name}</div>
<div className="bucket-details">
<span className="bucket-region">Region: {bucket.region}</span>
<span className="bucket-created">Created: {new Date(bucket.creation_date).toLocaleDateString()}</span>
<span className="bucket-size">Objects: {bucket.object_count}</span>
<span className="bucket-size">Size: {formatFileSize(bucket.total_size)}</span>
</div>
</div>
))}
</div>
<button onClick={() => setShowDestBuckets(false)} className="btn-small">
Hide Buckets
</button>
</div>
)}
</div>
</div>
)}
{/* ==================== Browse S3 Tab ==================== */}
{activeTab === 's3-browse' && (
<div className="tab-content">
<h2>📁 Browse Source S3 Objects</h2>
<div className="browse-controls">
<div className="s3-input-group">
<label>Source Bucket:</label>
<input
type="text"
value={selectedSourceBucket}
onChange={(e) => setSelectedSourceBucket(e.target.value)}
className="bucket-input"
placeholder="source-bucket-name"
/>
<label>Prefix (optional):</label>
<input
type="text"
value={selectedPrefix}
onChange={(e) => setSelectedPrefix(e.target.value)}
className="prefix-input"
placeholder="folder/subfolder/"
/>
<button onClick={listSourceObjects} disabled={loading} className="btn btn-primary">
🔍 List Objects
</button>
</div>
<div className="s3-uri-input">
<label>Or paste S3 URI:</label>
<input
type="text"
placeholder="s3://bucket-name/path/to/objects"
onBlur={(e) => parseS3Uri(e.target.value)}
className="uri-input"
/>
</div>
</div>
{parsedS3Uri && parsedS3Uri.success && (
<div className="parsed-info">
<h4>Parsed S3 URI:</h4>
<div className="parsed-details">
<div><strong>Bucket:</strong> {parsedS3Uri.parsed?.bucket}</div>
<div><strong>Key:</strong> {parsedS3Uri.parsed?.key || '/'}</div>
</div>
</div>
)}
{sourceObjects.length > 0 && (
<div className="objects-section">
<div className="objects-header">
<h3>
Objects in s3://{selectedSourceBucket}/{selectedPrefix}
<span className="badge">{sourceObjects.length} files</span>
<span className="badge">{formatFileSize(totalSize)}</span>
</h3>
<div className="object-actions">
<button onClick={selectAllObjects} className="btn-small">
Select All
</button>
<button onClick={clearSelectedObjects} className="btn-small">
Clear Selection
</button>
<button
onClick={migrateSelectedObjects}
disabled={selectedObjects.length === 0 || loading}
className="btn btn-migrate"
>
🚀 Migrate Selected ({selectedObjects.length})
</button>
</div>
</div>
{/* Object Groups */}
{Object.entries(objectGroups).map(([prefix, group]) => (
<div key={prefix} className="object-group">
<h4>📁 {prefix}</h4>
<div className="group-stats">
<span>{group.count} objects</span>
<span>{formatFileSize(group.totalSize)}</span>
</div>
<table className="objects-table">
<thead>
<tr>
<th style={{ width: '30px' }}>
<input
type="checkbox"
onChange={(e) => {
const groupKeys = group.objects.map(obj => obj.key);
if (e.target.checked) {
setSelectedObjects([...new Set([...selectedObjects, ...groupKeys])]);
} else {
setSelectedObjects(selectedObjects.filter(key => !groupKeys.includes(key)));
}
}}
checked={group.objects.every(obj => selectedObjects.includes(obj.key))}
/>
</th>
<th>Key</th>
<th>Size</th>
<th>Last Modified</th>
<th>ETag</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{group.objects.map((obj, index) => (
<tr key={index} className={selectedObjects.includes(obj.key) ? 'selected' : ''}>
<td>
<input
type="checkbox"
checked={selectedObjects.includes(obj.key)}
onChange={() => toggleObjectSelection(obj.key)}
/>
</td>
<td className="object-key">{obj.key}</td>
<td className="object-size">{formatFileSize(obj.size)}</td>
<td className="object-modified">
{new Date(obj.last_modified).toLocaleString()}
</td>
<td className="object-etag">{obj.etag?.substring(0, 8)}...</td>
<td>
<button
onClick={() => migrateSingleObject(obj.key)}
className="btn-small"
disabled={loading}
>
Migrate
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
)}
{sourceObjects.length === 0 && selectedSourceBucket && (
<div className="no-objects">
<p>📭 No objects found in s3://{selectedSourceBucket}/{selectedPrefix}</p>
</div>
)}
</div>
)}
{/* ==================== PostgreSQL to S3 Tab ==================== */}
{activeTab === 'pg-to-s3' && (
<div className="tab-content">
<h2>🔄 PostgreSQL to S3 Migration</h2>
<div className="config-section">
<h3>PostgreSQL Configuration</h3>
<div className="config-grid">
<div className="config-group full-width">
<label>PostgreSQL URI:</label>
<input
type="text"
value={pgToS3Config.postgresUri}
onChange={(e) => setPgToS3Config(prev => ({ ...prev, postgresUri: e.target.value }))}
placeholder="postgresql://user:pass@host:5432/database"
className="uri-input"
/>
</div>
</div>
<button onClick={testPgToS3PostgresConnection} disabled={loading} className="btn btn-test">
Test PostgreSQL Connection
</button>
</div>
<div className="config-section">
<h3>S3 Configuration</h3>
<div className="config-grid">
<div className="config-group">
<label>Access Key ID:</label>
<input
type="password"
value={sourceS3Config.accessKeyId}
onChange={(e) => setSourceS3Config(prev => ({ ...prev, accessKeyId: e.target.value }))}
placeholder="AKIAIOSFODNN7EXAMPLE"
/>
</div>
<div className="config-group">
<label>Secret Access Key:</label>
<input
type="password"
value={sourceS3Config.secretAccessKey}
onChange={(e) => setSourceS3Config(prev => ({ ...prev, secretAccessKey: e.target.value }))}
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
/>
</div>
<div className="config-group">
<label>Region:</label>
<input
type="text"
value={sourceS3Config.region}
onChange={(e) => setSourceS3Config(prev => ({ ...prev, region: e.target.value }))}
placeholder="us-east-1"
/>
</div>
<div className="config-group">
<label>Endpoint URL:</label>
<input
type="text"
value={sourceS3Config.endpointUrl}
onChange={(e) => setSourceS3Config(prev => ({ ...prev, endpointUrl: e.target.value }))}
placeholder="https://s3.amazonaws.com (optional)"
/>
</div>
</div>
<button onClick={testPgToS3S3Connection} disabled={loading} className="btn btn-test">
Test S3 Connection
</button>
</div>
<div className="config-section">
<h3>Migration Settings</h3>
<div className="config-grid">
<div className="config-group">
<label>S3 Bucket:</label>
<input
type="text"
value={pgToS3Config.s3Bucket}
onChange={(e) => setPgToS3Config(prev => ({ ...prev, s3Bucket: e.target.value }))}
placeholder="my-s3-bucket"
/>
</div>
<div className="config-group">
<label>S3 Prefix:</label>
<input
type="text"
value={pgToS3Config.s3Prefix}
onChange={(e) => setPgToS3Config(prev => ({ ...prev, s3Prefix: e.target.value }))}
placeholder="backups/"
/>
</div>
<div className="config-group">
<label>
<input
type="checkbox"
checked={pgToS3Config.compress}
onChange={(e) => setPgToS3Config(prev => ({ ...prev, compress: e.target.checked }))}
/>
Compress (GZIP)
</label>
</div>
<div className="config-group">
<label>Format:</label>
<select
value={pgToS3Config.format}
onChange={(e) => setPgToS3Config(prev => ({ ...prev, format: e.target.value }))}
>
<option value="csv">CSV</option>
<option value="json">JSON</option>
</select>
</div>
</div>
</div>
<div className="migration-controls">
<button
onClick={startPgToS3Migration}
disabled={loading || !pgToS3Config.postgresUri || !pgToS3Config.s3Bucket}
className="btn btn-migrate btn-large"
>
{loading ? 'Processing...' : '🚀 Start PostgreSQL to S3 Migration'}
</button>
</div>
</div>
)}
{/* ==================== Migration Tab ==================== */}
{activeTab === 'migration' && (
<div className="tab-content">
<h2>🚀 Migration Status</h2>
<div className="migration-options">
<h4>Migration Options</h4>
<div className="options-grid">
<label>
<input
type="checkbox"
checked={migrationOptions.createDestBucket}
onChange={(e) => setMigrationOptions({...migrationOptions, createDestBucket: e.target.checked})}
/>
Create Destination Bucket if Missing
</label>
<label>
<input
type="checkbox"
checked={migrationOptions.preserveMetadata}
onChange={(e) => setMigrationOptions({...migrationOptions, preserveMetadata: e.target.checked})}
/>
Preserve Metadata
</label>
<div className="option-item">
<label>Storage Class:</label>
<select
value={migrationOptions.storageClass}
onChange={(e) => setMigrationOptions({...migrationOptions, storageClass: e.target.value})}
>
<option value="STANDARD">Standard</option>
<option value="STANDARD_IA">Standard-IA</option>
<option value="INTELLIGENT_TIERING">Intelligent-Tiering</option>
<option value="ONEZONE_IA">OneZone-IA</option>
<option value="GLACIER">Glacier</option>
<option value="DEEP_ARCHIVE">Deep Archive</option>
</select>
</div>
<div className="option-item">
<label>Max Concurrent:</label>
<input
type="number"
min="1"
max="20"
value={migrationOptions.maxConcurrent}
onChange={(e) => setMigrationOptions({...migrationOptions, maxConcurrent: parseInt(e.target.value) || 5})}
/>
</div>
<div className="option-item full-width">
<label>Include Patterns (comma separated):</label>
<input
type="text"
value={migrationOptions.includePatterns}
onChange={(e) => setMigrationOptions({...migrationOptions, includePatterns: e.target.value})}
placeholder="*.csv, *.json, important/*"
/>
</div>
<div className="option-item full-width">
<label>Exclude Patterns (comma separated):</label>
<input
type="text"
value={migrationOptions.excludePatterns}
onChange={(e) => setMigrationOptions({...migrationOptions, excludePatterns: e.target.value})}
placeholder="*.tmp, *.log, temp/*"
/>
</div>
</div>
</div>
<div className="migration-controls">
<button
onClick={startS3Migration}
disabled={loading || !selectedSourceBucket || !selectedDestBucket}
className="btn btn-migrate btn-large"
>
{loading ? 'Processing...' : '🚀 Start S3 to S3 Migration'}
</button>
<button
onClick={startPostgresMigration}
disabled={loading || !postgresConfig.uri}
className="btn btn-migrate btn-large"
>
{loading ? 'Processing...' : '🚀 Start PostgreSQL Migration'}
</button>
</div>
{postgresTables.length > 0 && (
<div className="migration-stats">
<h4>Migration Statistics</h4>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-label">Total Tables</div>
<div className="stat-value">{postgresTables.length}</div>
</div>
<div className="stat-card">
<div className="stat-label">Total Rows</div>
<div className="stat-value">{totalRows.toLocaleString()}</div>
</div>
<div className="stat-card">
<div className="stat-label">Selected Tables</div>
<div className="stat-value">{selectedTables.length}</div>
</div>
</div>
</div>
)}
{sourceObjects.length > 0 && (
<div className="migration-stats">
<h4>Migration Statistics</h4>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-label">Total Objects</div>
<div className="stat-value">{sourceObjects.length}</div>
</div>
<div className="stat-card">
<div className="stat-label">Total Size</div>
<div className="stat-value">{formatFileSize(totalSize)}</div>
</div>
<div className="stat-card">
<div className="stat-label">Selected Objects</div>
<div className="stat-value">{selectedObjects.length}</div>
</div>
</div>
</div>
)}
{migrations.length > 0 && (
<div className="migrations-list">
<h3>Previous Migrations ({migrations.length})</h3>
<div className="migrations-grid">
{migrations.map((migration, index) => (
<div key={index} className="migration-card">
<div className="migration-header">
<span className="migration-id">{migration.id}</span>
<span className={`migration-status ${migration.status}`}>
{migration.status}
</span>
</div>
<div className="migration-body">
<p><strong>Status:</strong> {migration.status}</p>
<p><strong>Started:</strong> {migration.started_at ? new Date(migration.started_at * 1000).toLocaleString() : 'N/A'}</p>
</div>
<div className="migration-actions">
<button onClick={() => setActiveMigration(migration.id)} className="btn-small">
View Details
</button>
{migration.status === 'running' && (
<button onClick={() => cancelMigration(migration.id, 's3')} className="btn-small btn-warning">
Cancel
</button>
)}
</div>
</div>
))}
</div>
</div>
)}
{activeMigration && migrationLogs.length > 0 && (
<div className="migration-logs">
<h3>Migration Logs ({activeMigration})</h3>
<div className="logs-container">
{migrationLogs.map((log, index) => (
<div key={index} className={`log-entry ${log.level || 'info'}`}>
<span className="log-time">
{log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : ''}
</span>
<span className="log-message">{log.message}</span>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* ==================== Environment Tab ==================== */}
{activeTab === 'environment' && (
<div className="tab-content">
<h2>⚙️ Environment Variables Management</h2>
<div className="env-actions">
<button onClick={loadCurrentEnv} className="btn btn-secondary">🔄 Refresh Environment</button>
<button onClick={injectEnvironment} className="btn btn-primary">⚡ Inject Environment</button>
<button onClick={clearEnvironment} className="btn btn-warning">🗑️ Clear Environment</button>
</div>
<div className="env-config-summary">
<h3>Current Configuration</h3>
<div className="config-cards">
<div className="config-card">
<h4>🐘 PostgreSQL Source</h4>
<pre>{formatPostgresConfig(postgresConfig)}</pre>
</div>
<div className="config-card">
<h4>🐘 PostgreSQL Destination</h4>
<pre>{formatPostgresConfig(destPostgresConfig)}</pre>
</div>
<div className="config-card">
<h4>📤 Source S3</h4>
<pre>{formatS3Config(sourceS3Config, 'source')}</pre>
</div>
<div className="config-card">
<h4>📥 Destination S3</h4>
<pre>{formatS3Config(destS3Config, 'destination')}</pre>
</div>
</div>
</div>
<div className="env-format-selector">
<label>Format:</label>
<select value={envFormat} onChange={(e) => setEnvFormat(e.target.value)}>
<option value="dotenv">.env file</option>
<option value="json">JSON</option>
<option value="shell_export">Shell export</option>
<option value="docker_env">Docker -e flags</option>
<option value="docker_compose">Docker Compose</option>
</select>
<button onClick={copyEnvToClipboard} disabled={Object.keys(currentEnv).length === 0} className="btn btn-secondary">
📋 Copy to Clipboard
</button>
</div>
<div className="env-preview">
<h3>Current Environment Variables ({Object.keys(currentEnv).length})</h3>
{Object.keys(currentEnv).length > 0 ? (
<pre>{formatEnvVars(currentEnv, envFormat)}</pre>
) : (
<p className="no-env">No environment variables found. Test connections or inject environment first.</p>
)}
</div>
</div>
)}
{/* Status Bar */}
<div className="status-bar">
{loading && <div className="spinner"> Processing...</div>}
<div className="status-info">
{selectedSourceBucket && <span>📤 Source: {selectedSourceBucket}</span>}
{selectedDestBucket && <span>📥 Destination: {selectedDestBucket}</span>}
{selectedPostgresDb && <span>🐘 DB: {selectedPostgresDb}</span>}
{selectedObjects.length > 0 && <span>📁 {selectedObjects.length} objects selected</span>}
{selectedTables.length > 0 && <span>📊 {selectedTables.length} tables selected</span>}
{activeMigration && <span>🚀 Active migration: {activeMigration}</span>}
<span> {Object.keys(currentEnv).length} env vars</span>
</div>
</div>
</div>
{/* Progress Modal */}
{renderProgressModal()}
{/* Progress History */}
{renderProgressHistory()}
</div>
);
}
export default App;