3366 أسطر
124 KiB
JavaScript
3366 أسطر
124 KiB
JavaScript
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 = `
|
||
<span class="notification-icon">${type === 'success' ? '✅' : type === 'error' ? '❌' : type === 'warning' ? '⚠️' : 'ℹ️'}</span>
|
||
<span class="notification-message">${message}</span>
|
||
<button class="notification-close" onclick="this.parentElement.remove()">×</button>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="container">
|
||
<!-- Notifications Container -->
|
||
<div id="notifications"></div>
|
||
|
||
<header class="header">
|
||
<div class="header-content">
|
||
<h1>🔄 Universal Migrator</h1>
|
||
<p>PostgreSQL & S3 Migration Tool</p>
|
||
</div>
|
||
<div class="header-actions">
|
||
<div class="security-badge">🔒 Secure Mode</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Simplified Tabs - 4 tabs only -->
|
||
<div class="tabs">
|
||
<button class="tab-btn active" data-tab="psql-to-s3">🐘 → 📤 PSQL to S3</button>
|
||
<button class="tab-btn" data-tab="psql-to-psql">🐘 → 🐘 PSQL to PSQL</button>
|
||
<button class="tab-btn" data-tab="s3-to-s3">📤 → 📥 S3 to S3</button>
|
||
<button class="tab-btn" data-tab="environment">⚙️ Environment</button>
|
||
</div>
|
||
|
||
<!-- ==================== PSQL to S3 Tab ==================== -->
|
||
<div class="tab-content active" id="tab-psql-to-s3">
|
||
<h2>🐘 → 📤 PostgreSQL to S3 Migration</h2>
|
||
|
||
<div class="migration-layout">
|
||
<!-- Left Column - PostgreSQL Configuration -->
|
||
<div class="migration-column">
|
||
<div class="config-section">
|
||
<h3>PostgreSQL Source</h3>
|
||
|
||
<div class="config-group">
|
||
<label>Host:</label>
|
||
<input type="text" id="psqlToS3_sourceHost" placeholder="localhost">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Port:</label>
|
||
<input type="number" id="psqlToS3_sourcePort" value="5432">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>User:</label>
|
||
<input type="text" id="psqlToS3_sourceUser" placeholder="postgres">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Password:</label>
|
||
<input type="password" id="psqlToS3_sourcePassword" placeholder="••••••••">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Database:</label>
|
||
<input type="text" id="psqlToS3_sourceDatabase" placeholder="postgres">
|
||
</div>
|
||
|
||
<div class="uri-input-group">
|
||
<label>Or paste URI:</label>
|
||
<div class="uri-input-wrapper">
|
||
<input type="text" id="psqlToS3_sourceUri" placeholder="postgresql://user:pass@host:5432/database">
|
||
<button id="psqlToS3_parseSourceUri" class="btn-small">Parse</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button id="psqlToS3_testSourceConnection" class="btn-test">Test Connection</button>
|
||
<button id="psqlToS3_getSchemas" class="btn-secondary">📚 Get Schemas</button>
|
||
<button id="psqlToS3_getTables" class="btn-secondary">📊 Get Tables</button>
|
||
</div>
|
||
|
||
<div id="psqlToS3_sourceConnectionStatus" style="display:none;" class="status-message"></div>
|
||
</div>
|
||
|
||
<div id="psqlToS3_schemasSection" style="display:none;" class="results-section">
|
||
<h4>Available Schemas</h4>
|
||
<div id="psqlToS3_schemasContainer" class="schemas-grid"></div>
|
||
</div>
|
||
|
||
<div id="psqlToS3_tablesSection" style="display:none;" class="results-section">
|
||
<h4>Tables</h4>
|
||
<div id="psqlToS3_tablesContainer"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Column - S3 Configuration -->
|
||
<div class="migration-column">
|
||
<div class="config-section">
|
||
<h3>S3 Destination</h3>
|
||
|
||
<div class="config-group">
|
||
<label>Access Key ID:</label>
|
||
<input type="password" id="psqlToS3_s3AccessKeyId">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Secret Access Key:</label>
|
||
<input type="password" id="psqlToS3_s3SecretAccessKey">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Region:</label>
|
||
<input type="text" id="psqlToS3_s3Region" value="us-east-1">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Endpoint URL:</label>
|
||
<input type="text" id="psqlToS3_s3EndpointUrl" placeholder="Optional">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>S3 Bucket:</label>
|
||
<input type="text" id="psqlToS3_s3Bucket" placeholder="my-s3-bucket">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>S3 Prefix:</label>
|
||
<input type="text" id="psqlToS3_s3Prefix" placeholder="backups/">
|
||
</div>
|
||
|
||
<div class="config-row">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" id="psqlToS3_compress" checked> Compress (GZIP)
|
||
</label>
|
||
<div class="select-wrapper">
|
||
<select id="psqlToS3_format">
|
||
<option value="csv">CSV</option>
|
||
<option value="json">JSON</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button id="psqlToS3_testPgConnection" class="btn-test">Test PG Connection</button>
|
||
<button id="psqlToS3_testS3Connection" class="btn-test">Test S3 Connection</button>
|
||
</div>
|
||
|
||
<div class="migration-controls">
|
||
<button id="psqlToS3_startMigration" class="btn-migrate btn-large">🚀 Start PSQL to S3 Migration</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ==================== PSQL to PSQL Tab ==================== -->
|
||
<div class="tab-content" id="tab-psql-to-psql">
|
||
<h2>🐘 → 🐘 PostgreSQL to PostgreSQL Migration</h2>
|
||
|
||
<div class="migration-layout">
|
||
<!-- Left Column - Source PostgreSQL -->
|
||
<div class="migration-column">
|
||
<div class="config-section">
|
||
<h3>Source PostgreSQL</h3>
|
||
|
||
<div class="config-group">
|
||
<label>Host:</label>
|
||
<input type="text" id="psqlToPsql_sourceHost" placeholder="localhost">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Port:</label>
|
||
<input type="number" id="psqlToPsql_sourcePort" value="5432">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>User:</label>
|
||
<input type="text" id="psqlToPsql_sourceUser" placeholder="postgres">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Password:</label>
|
||
<input type="password" id="psqlToPsql_sourcePassword" placeholder="••••••••">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Database:</label>
|
||
<input type="text" id="psqlToPsql_sourceDatabase" placeholder="postgres">
|
||
</div>
|
||
|
||
<div class="uri-input-group">
|
||
<label>Or paste URI:</label>
|
||
<div class="uri-input-wrapper">
|
||
<input type="text" id="psqlToPsql_sourceUri" placeholder="postgresql://user:pass@host:5432/database">
|
||
<button id="psqlToPsql_parseSourceUri" class="btn-small">Parse</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button id="psqlToPsql_testSourceConnection" class="btn-test">Test Connection</button>
|
||
<button id="psqlToPsql_getSchemas" class="btn-secondary">📚 Get Schemas</button>
|
||
<button id="psqlToPsql_getTables" class="btn-secondary">📊 Get Tables</button>
|
||
</div>
|
||
|
||
<div id="psqlToPsql_sourceConnectionStatus" style="display:none;" class="status-message"></div>
|
||
</div>
|
||
|
||
<div id="psqlToPsql_schemasSection" style="display:none;" class="results-section">
|
||
<h4>Available Schemas</h4>
|
||
<div id="psqlToPsql_schemasContainer" class="schemas-grid"></div>
|
||
</div>
|
||
|
||
<div id="psqlToPsql_tablesSection" style="display:none;" class="results-section">
|
||
<h4>Tables</h4>
|
||
<div id="psqlToPsql_tablesContainer"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Column - Destination PostgreSQL -->
|
||
<div class="migration-column">
|
||
<div class="config-section">
|
||
<h3>Destination PostgreSQL</h3>
|
||
|
||
<div class="config-group">
|
||
<label>Host:</label>
|
||
<input type="text" id="psqlToPsql_destHost" placeholder="localhost">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Port:</label>
|
||
<input type="number" id="psqlToPsql_destPort" value="5432">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>User:</label>
|
||
<input type="text" id="psqlToPsql_destUser" placeholder="postgres">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Password:</label>
|
||
<input type="password" id="psqlToPsql_destPassword" placeholder="••••••••">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Database:</label>
|
||
<input type="text" id="psqlToPsql_destDatabase" placeholder="postgres">
|
||
</div>
|
||
|
||
<div class="uri-input-group">
|
||
<label>Or paste URI:</label>
|
||
<div class="uri-input-wrapper">
|
||
<input type="text" id="psqlToPsql_destUri" placeholder="postgresql://user:pass@host:5432/database">
|
||
<button id="psqlToPsql_parseDestUri" class="btn-small">Parse</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button id="psqlToPsql_testDestConnection" class="btn-test">Test Connection</button>
|
||
</div>
|
||
|
||
<div id="psqlToPsql_destConnectionStatus" style="display:none;" class="status-message"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Migration Options and Controls -->
|
||
<div class="migration-options-panel">
|
||
<h3>Migration Options</h3>
|
||
<div class="options-row">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" id="psqlToPsql_createDestBucket" checked> Create Destination Bucket if Missing
|
||
</label>
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" id="psqlToPsql_preserveMetadata" checked> Preserve Metadata
|
||
</label>
|
||
</div>
|
||
<div class="options-row">
|
||
<div class="option-item">
|
||
<label>Storage Class:</label>
|
||
<select id="psqlToPsql_storageClass">
|
||
<option value="STANDARD">Standard</option>
|
||
<option value="STANDARD_IA">Standard-IA</option>
|
||
<option value="INTELLIGENT_TIERING">Intelligent-Tiering</option>
|
||
<option value="GLACIER">Glacier</option>
|
||
</select>
|
||
</div>
|
||
<div class="option-item">
|
||
<label>Max Concurrent:</label>
|
||
<input type="number" id="psqlToPsql_maxConcurrent" value="5" min="1" max="20">
|
||
</div>
|
||
</div>
|
||
<div class="options-row full-width">
|
||
<div class="option-item">
|
||
<label>Include Patterns:</label>
|
||
<input type="text" id="psqlToPsql_includePatterns" placeholder="*.csv, *.json, important/*">
|
||
</div>
|
||
<div class="option-item">
|
||
<label>Exclude Patterns:</label>
|
||
<input type="text" id="psqlToPsql_excludePatterns" placeholder="*.tmp, *.log, temp/*">
|
||
</div>
|
||
</div>
|
||
<div class="migration-controls">
|
||
<button id="psqlToPsql_startMigration" class="btn-migrate btn-large">🚀 Start PSQL to PSQL Migration</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ==================== S3 to S3 Tab ==================== -->
|
||
<div class="tab-content" id="tab-s3-to-s3">
|
||
<h2>📤 → 📥 S3 to S3 Migration</h2>
|
||
|
||
<div class="migration-layout">
|
||
<!-- Left Column - Source S3 -->
|
||
<div class="migration-column">
|
||
<div class="config-section">
|
||
<h3>Source S3</h3>
|
||
|
||
<div class="config-group">
|
||
<label>Access Key ID:</label>
|
||
<input type="password" id="s3ToS3_sourceAccessKeyId">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Secret Access Key:</label>
|
||
<input type="password" id="s3ToS3_sourceSecretAccessKey">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Region:</label>
|
||
<input type="text" id="s3ToS3_sourceRegion" value="us-east-1">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Endpoint URL:</label>
|
||
<input type="text" id="s3ToS3_sourceEndpointUrl" placeholder="Optional">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Session Token:</label>
|
||
<input type="password" id="s3ToS3_sourceSessionToken" placeholder="Optional">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Bucket:</label>
|
||
<input type="text" id="s3ToS3_sourceBucket" placeholder="source-bucket" value="my-source-bucket">
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button id="s3ToS3_testSourceConnection" class="btn-test">Test Connection</button>
|
||
<button id="s3ToS3_listSourceBuckets" class="btn-secondary">📁 List Buckets</button>
|
||
</div>
|
||
|
||
<div id="s3ToS3_sourceConnectionStatus" style="display:none;" class="status-message"></div>
|
||
</div>
|
||
|
||
<div id="s3ToS3_sourceBucketsList" style="display:none;" class="results-section">
|
||
<h4>Available Buckets:</h4>
|
||
<div id="s3ToS3_sourceBucketsContainer"></div>
|
||
<button id="s3ToS3_hideSourceBuckets" class="btn-small">Hide</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Column - Destination S3 -->
|
||
<div class="migration-column">
|
||
<div class="config-section">
|
||
<h3>Destination S3</h3>
|
||
|
||
<div class="config-group">
|
||
<label>Access Key ID:</label>
|
||
<input type="password" id="s3ToS3_destAccessKeyId">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Secret Access Key:</label>
|
||
<input type="password" id="s3ToS3_destSecretAccessKey">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Region:</label>
|
||
<input type="text" id="s3ToS3_destRegion" value="us-east-1">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Endpoint URL:</label>
|
||
<input type="text" id="s3ToS3_destEndpointUrl" placeholder="Optional">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Session Token:</label>
|
||
<input type="password" id="s3ToS3_destSessionToken" placeholder="Optional">
|
||
</div>
|
||
<div class="config-group">
|
||
<label>Bucket:</label>
|
||
<input type="text" id="s3ToS3_destBucket" placeholder="destination-bucket" value="my-destination-bucket">
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button id="s3ToS3_testDestConnection" class="btn-test">Test Connection</button>
|
||
<button id="s3ToS3_listDestBuckets" class="btn-secondary">📁 List Buckets</button>
|
||
<button id="s3ToS3_createDestBucket" class="btn-success">➕ Create Bucket</button>
|
||
</div>
|
||
|
||
<div id="s3ToS3_destConnectionStatus" style="display:none;" class="status-message"></div>
|
||
</div>
|
||
|
||
<div id="s3ToS3_destBucketsList" style="display:none;" class="results-section">
|
||
<h4>Available Buckets:</h4>
|
||
<div id="s3ToS3_destBucketsContainer"></div>
|
||
<button id="s3ToS3_hideDestBuckets" class="btn-small">Hide</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Migration Controls -->
|
||
<div class="migration-controls-panel">
|
||
<button id="s3ToS3_startMigration" class="btn-migrate btn-large">🚀 Start S3 to S3 Migration</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ==================== Environment Tab ==================== -->
|
||
<div class="tab-content" id="tab-environment">
|
||
<h2>⚙️ Environment & Security</h2>
|
||
|
||
<div class="environment-layout">
|
||
<!-- Left Column - Environment Variables -->
|
||
<div class="environment-column">
|
||
<div class="config-section">
|
||
<h3>Environment Variables</h3>
|
||
|
||
<div class="action-group">
|
||
<button id="refreshEnv" class="btn-secondary">🔄 Refresh</button>
|
||
<button id="injectEnv" class="btn-primary">⚡ Inject</button>
|
||
<button id="clearEnv" class="btn-warning">🗑️ Clear</button>
|
||
</div>
|
||
|
||
<div class="format-controls">
|
||
<select id="envFormat">
|
||
<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 id="copyEnv" class="btn-secondary">📋 Copy</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="config-section">
|
||
<h3>Current Configuration</h3>
|
||
<div id="envConfigCards" class="config-cards"></div>
|
||
</div>
|
||
|
||
<div class="config-section">
|
||
<h3>Environment Preview</h3>
|
||
<pre id="envPreview" class="env-preview"></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Column - Security & Migrations -->
|
||
<div class="environment-column">
|
||
<div class="config-section">
|
||
<h3>Security</h3>
|
||
|
||
<div class="security-info">
|
||
<p><strong>Session ID:</strong> <span id="sessionId">Loading...</span></p>
|
||
<p><strong>Active Sessions:</strong> <span id="activeSessions">-</span></p>
|
||
<p><strong>Expiry:</strong> 10 minutes</p>
|
||
</div>
|
||
|
||
<div class="security-features">
|
||
<div class="security-feature">
|
||
<span>Encryption:</span>
|
||
<span class="feature-value">AES-256</span>
|
||
</div>
|
||
<div class="security-feature">
|
||
<span>Session Isolation:</span>
|
||
<span class="feature-value">✅ Enabled</span>
|
||
</div>
|
||
<div class="security-feature">
|
||
<span>Auto-cleanup:</span>
|
||
<span class="feature-value">10 minutes</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button id="clearSessionBtn" class="btn-warning">🗑️ Clear Session</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="config-section">
|
||
<h3>Migration Data Cleanup</h3>
|
||
<p>Clear sensitive data from migrations</p>
|
||
<div class="cleanup-controls">
|
||
<input type="text" id="cleanupMigrationId" placeholder="Migration ID">
|
||
<button id="clearMigrationBtn" class="btn-warning">🧹 Clear</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="config-section">
|
||
<h3>Migration History</h3>
|
||
<div class="section-header">
|
||
<button id="refreshMigrations" class="btn-secondary">🔄 Refresh</button>
|
||
</div>
|
||
|
||
<div id="migrationStats" style="display:none;" class="migration-stats">
|
||
<h4>Statistics</h4>
|
||
<div id="statsGrid" class="stats-grid"></div>
|
||
</div>
|
||
|
||
<div id="migrationsList" style="display:none;">
|
||
<div id="migrationsContainer"></div>
|
||
</div>
|
||
|
||
<div id="migrationLogs" style="display:none;">
|
||
<h4>Logs</h4>
|
||
<div id="logsContainer"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status-bar">
|
||
<div id="statusInfo">Ready</div>
|
||
<div id="loading" style="display:none;">⏳ Processing...</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// ==================== 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 = `
|
||
<p><strong>Success:</strong> ✅ Connected</p>
|
||
<p><strong>Host:</strong> ${psqlToS3Config.source.host || result.connection?.host}:${psqlToS3Config.source.port || result.connection?.port}</p>
|
||
<p><strong>Version:</strong> ${result.version || 'Unknown'}</p>
|
||
`;
|
||
statusDiv.className = 'status-message success';
|
||
} else {
|
||
showNotification(`❌ PSQL to S3 - Source connection failed: ${result.error}`, 'error');
|
||
statusDiv.innerHTML = `<p><strong>Error:</strong> ${result.error}</p>`;
|
||
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 = `
|
||
<p><strong>Success:</strong> ✅ Connected</p>
|
||
<p><strong>Host:</strong> ${psqlToPsqlConfig.source.host || result.connection?.host}:${psqlToPsqlConfig.source.port || result.connection?.port}</p>
|
||
<p><strong>Version:</strong> ${result.version || 'Unknown'}</p>
|
||
`;
|
||
statusDiv.className = 'status-message success';
|
||
} else {
|
||
showNotification(`❌ PSQL to PSQL - Source connection failed: ${result.error}`, 'error');
|
||
statusDiv.innerHTML = `<p><strong>Error:</strong> ${result.error}</p>`;
|
||
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 = `
|
||
<p><strong>Success:</strong> ✅ Connected</p>
|
||
<p><strong>Host:</strong> ${psqlToPsqlConfig.dest.host || result.connection?.host}:${psqlToPsqlConfig.dest.port || result.connection?.port}</p>
|
||
<p><strong>Version:</strong> ${result.version || 'Unknown'}</p>
|
||
`;
|
||
statusDiv.className = 'status-message success';
|
||
} else {
|
||
showNotification(`❌ PSQL to PSQL - Destination connection failed: ${result.error}`, 'error');
|
||
statusDiv.innerHTML = `<p><strong>Error:</strong> ${result.error}</p>`;
|
||
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 = `
|
||
<p><strong>Success:</strong> ✅ Connected</p>
|
||
<p><strong>Endpoint:</strong> ${s3ToS3Config.source.endpointUrl || 'AWS S3 (default)'}</p>
|
||
<p><strong>Region:</strong> ${s3ToS3Config.source.region}</p>
|
||
<p><strong>Buckets Found:</strong> ${result.bucket_count || 0}</p>
|
||
`;
|
||
statusDiv.className = 'status-message success';
|
||
} else {
|
||
showNotification(`❌ S3 to S3 - Source connection failed: ${result.error}`, 'error');
|
||
statusDiv.innerHTML = `<p><strong>Error:</strong> ${result.error}</p>`;
|
||
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 = `
|
||
<p><strong>Success:</strong> ✅ Connected</p>
|
||
<p><strong>Endpoint:</strong> ${s3ToS3Config.dest.endpointUrl || 'AWS S3 (default)'}</p>
|
||
<p><strong>Region:</strong> ${s3ToS3Config.dest.region}</p>
|
||
<p><strong>Buckets Found:</strong> ${result.bucket_count || 0}</p>
|
||
`;
|
||
statusDiv.className = 'status-message success';
|
||
} else {
|
||
showNotification(`❌ S3 to S3 - Destination connection failed: ${result.error}`, 'error');
|
||
statusDiv.innerHTML = `<p><strong>Error:</strong> ${result.error}</p>`;
|
||
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 = `
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3>Migration Progress: ${migrationId}</h3>
|
||
<button class="close-modal">×</button>
|
||
</div>
|
||
<div class="modal-body" id="progress-${migrationId}"></div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(progressContainer);
|
||
|
||
const closeBtn = progressContainer.querySelector('.close-modal');
|
||
closeBtn.addEventListener('click', () => progressContainer.remove());
|
||
|
||
// بدء تتبع التقدم
|
||
const progressDiv = progressContainer.querySelector(`#progress-${migrationId}`);
|
||
|
||
progressDiv.innerHTML = `
|
||
<div class="progress-container">
|
||
<div class="progress-bar-container">
|
||
<div class="progress-bar-fill" style="width: 0%"></div>
|
||
</div>
|
||
<div class="progress-stats">Connecting...</div>
|
||
<div class="progress-details"></div>
|
||
<button class="stop-progress-btn">Stop Tracking</button>
|
||
</div>
|
||
`;
|
||
|
||
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, '<br>');
|
||
|
||
// تحديث التفاصيل
|
||
let details = '';
|
||
if (progress.current_speed_formatted || progress.speed?.current_formatted) {
|
||
details += `<div>⚡ Speed: ${progress.current_speed_formatted || progress.speed?.current_formatted}</div>`;
|
||
}
|
||
if (progress.eta_formatted || progress.time?.eta_formatted) {
|
||
details += `<div>⏳ ETA: ${progress.eta_formatted || progress.time?.eta_formatted}</div>`;
|
||
}
|
||
if (progress.elapsed_time_formatted || progress.time?.elapsed_formatted) {
|
||
details += `<div>⏱️ Elapsed: ${progress.elapsed_time_formatted || progress.time?.elapsed_formatted}</div>`;
|
||
}
|
||
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 => `
|
||
<div class="schema-card">
|
||
<label>
|
||
<input type="checkbox" class="schema-checkbox" value="${schema}">
|
||
${schema}
|
||
</label>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function renderPsqlToS3Tables(tables) {
|
||
const container = document.getElementById('psqlToS3_tablesContainer');
|
||
const groups = groupTablesBySchema(tables);
|
||
|
||
let html = '';
|
||
Object.entries(groups).forEach(([schema, group]) => {
|
||
html += `
|
||
<div class="table-group">
|
||
<h4>📚 Schema: ${schema}</h4>
|
||
<div class="group-stats">
|
||
<span>${group.count} tables</span>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="tables-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 30px">
|
||
<input type="checkbox" class="schema-select-all" data-schema="${schema}">
|
||
</th>
|
||
<th>Table Name</th>
|
||
<th>Type</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${group.tables.map(table => `
|
||
<tr>
|
||
<td>
|
||
<input type="checkbox" class="table-checkbox" value="${table.name}" data-schema="${schema}">
|
||
</td>
|
||
<td>${table.name}</td>
|
||
<td>${table.type}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function renderPsqlToPsqlSchemas() {
|
||
const container = document.getElementById('psqlToPsql_schemasContainer');
|
||
container.innerHTML = psqlToPsqlSchemas.map(schema => `
|
||
<div class="schema-card">
|
||
<label>
|
||
<input type="checkbox" class="schema-checkbox" value="${schema}">
|
||
${schema}
|
||
</label>
|
||
</div>
|
||
`).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 += `
|
||
<div class="table-group">
|
||
<h4>📚 Schema: ${schema}</h4>
|
||
<div class="group-stats">
|
||
<span>${group.count} tables</span>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="tables-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 30px">
|
||
<input type="checkbox" class="schema-select-all" data-schema="${schema}">
|
||
</th>
|
||
<th>Table Name</th>
|
||
<th>Type</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${group.tables.map(table => `
|
||
<tr>
|
||
<td>
|
||
<input type="checkbox" class="table-checkbox" value="${table.name}" data-schema="${schema}">
|
||
</td>
|
||
<td>${table.name}</td>
|
||
<td>${table.type}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
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 => `
|
||
<div class="bucket-card" data-bucket="${bucket.name}">
|
||
<div class="bucket-name">${bucket.name}</div>
|
||
<div class="bucket-details">
|
||
<span class="bucket-region">Region: ${bucket.region}</span>
|
||
<span class="bucket-created">Created: ${new Date(bucket.creation_date).toLocaleDateString()}</span>
|
||
<span class="bucket-size">Objects: ${bucket.object_count}</span>
|
||
<span class="bucket-size">Size: ${formatFileSize(bucket.total_size)}</span>
|
||
</div>
|
||
<button class="select-bucket-btn" data-bucket="${bucket.name}">Select</button>
|
||
</div>
|
||
`).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 => `
|
||
<div class="bucket-card" data-bucket="${bucket.name}">
|
||
<div class="bucket-name">${bucket.name}</div>
|
||
<div class="bucket-details">
|
||
<span class="bucket-region">Region: ${bucket.region}</span>
|
||
<span class="bucket-created">Created: ${new Date(bucket.creation_date).toLocaleDateString()}</span>
|
||
<span class="bucket-size">Objects: ${bucket.object_count}</span>
|
||
<span class="bucket-size">Size: ${formatFileSize(bucket.total_size)}</span>
|
||
</div>
|
||
<button class="select-bucket-btn" data-bucket="${bucket.name}">Select</button>
|
||
</div>
|
||
`).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 = '<p class="no-migrations">No migrations found</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = migrations.map(migration => `
|
||
<div class="migration-card">
|
||
<div class="migration-header">
|
||
<span class="migration-id">${migration.id}</span>
|
||
<span class="migration-status ${migration.status}">${migration.status}</span>
|
||
</div>
|
||
<div class="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 class="migration-actions">
|
||
<button class="view-logs-btn" data-id="${migration.id}">View Logs</button>
|
||
${migration.status === 'running' ? `
|
||
<button class="cancel-migration-btn" data-id="${migration.id}">Cancel</button>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`).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 = '<p class="no-logs">No logs available</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = migrationLogs.map(log => `
|
||
<div class="log-entry ${log.level || 'info'}">
|
||
<span class="log-time">${log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : ''}</span>
|
||
<span class="log-message">${log.message}</span>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function renderEnvConfigCards() {
|
||
const container = document.getElementById('envConfigCards');
|
||
if (!container) return;
|
||
|
||
container.innerHTML = `
|
||
<div class="config-card">
|
||
<h4>🐘 PSQL to PSQL Source</h4>
|
||
<pre>${formatPostgresConfig(psqlToPsqlConfig.source)}</pre>
|
||
</div>
|
||
<div class="config-card">
|
||
<h4>🐘 PSQL to PSQL Dest</h4>
|
||
<pre>${formatPostgresConfig(psqlToPsqlConfig.dest)}</pre>
|
||
</div>
|
||
<div class="config-card">
|
||
<h4>📤 S3 to S3 Source</h4>
|
||
<pre>${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')}</pre>
|
||
</div>
|
||
<div class="config-card">
|
||
<h4>📥 S3 to S3 Dest</h4>
|
||
<pre>${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')}</pre>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ==================== 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); |