SGO/templates/import.html
Eduardo Figueroa 6886c8871c
Initial Commit
2025-11-20 12:03:30 -08:00

432 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SGO - Import</title>
<link rel="icon" href="{{ url_for('static', filename='images/logo.svg') }}" type="image/svg+xml">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<style>
.import-container {
max-width: 800px;
margin: 4rem auto;
padding: 2rem;
}
.import-card {
background: var(--card-bg);
border-radius: 0.5rem;
padding: 2rem;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border-color);
}
.import-title {
font-size: 2rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 0.5rem;
}
.import-subtitle {
color: var(--text-secondary);
margin-bottom: 2rem;
}
.profile-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
padding: 0.5rem;
margin-bottom: 1rem;
}
.profile-item {
padding: 0.75rem;
border-radius: 0.25rem;
margin-bottom: 0.25rem;
cursor: pointer;
transition: all 0.2s;
}
.profile-item:hover {
background: var(--bg-color);
}
.profile-item label {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
}
.profile-item input[type="checkbox"] {
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
}
.mfa-section {
display: none;
margin-top: 1.5rem;
padding: 1.5rem;
background: var(--bg-color);
border-radius: 0.375rem;
}
.mfa-section.active {
display: block;
}
.mfa-inputs {
display: grid;
gap: 1rem;
}
.mfa-input-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.mfa-input-group label {
font-weight: 600;
color: var(--text-primary);
}
.mfa-input-row {
display: flex;
gap: 0.5rem;
align-items: center;
}
.mfa-input-group input {
flex: 1;
padding: 0.75rem;
border: 2px solid var(--border-color);
border-radius: 0.375rem;
font-size: 1rem;
}
.mfa-input-group input:focus {
outline: none;
border-color: var(--primary-color);
}
.profile-import-btn {
padding: 0.75rem 1.5rem;
background: var(--primary-color);
color: white;
border: none;
border-radius: 0.375rem;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.profile-import-btn:hover:not(:disabled) {
background: var(--primary-hover);
transform: translateY(-1px);
}
.profile-import-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.profile-import-btn.success {
background: var(--success-color);
}
.profile-import-btn.error {
background: var(--danger-color);
}
.import-btn {
width: 100%;
padding: 1rem;
background: var(--primary-color);
color: white;
border: none;
border-radius: 0.5rem;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin-top: 1rem;
}
.import-btn:hover:not(:disabled) {
background: var(--primary-hover);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.import-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.select-all-btn {
padding: 0.5rem 1rem;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
margin-bottom: 0.5rem;
}
.select-all-btn:hover {
background: var(--border-color);
}
.progress-section {
display: none;
margin-top: 1.5rem;
padding: 1.5rem;
background: var(--bg-color);
border-radius: 0.375rem;
}
.progress-section.active {
display: block;
}
#progressLog {
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
padding: 0.5rem;
background: var(--bg-color);
}
.progress-item {
padding: 0.5rem 0;
color: var(--text-secondary);
}
.progress-item.success {
color: var(--success-color);
}
.progress-item.error {
color: var(--danger-color);
}
</style>
</head>
<body>
<div class="import-container">
<div class="import-card">
<h1 class="import-title">SG Observatory</h1>
<p class="import-subtitle">Select AWS profiles to import EC2 instances and Security Groups</p>
<div id="loadingProfiles" class="loading">
<div class="spinner"></div>
Loading AWS profiles...
</div>
<div id="profileSelection" style="display: none;">
<button class="select-all-btn" onclick="toggleSelectAll()">Select All / Deselect All</button>
<div class="profile-list" id="profileList"></div>
<div class="mfa-section" id="mfaSection">
<h3>MFA Codes</h3>
<p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 1rem;">
Enter MFA/OTP codes for profiles that require authentication
</p>
<div class="mfa-inputs" id="mfaInputs"></div>
</div>
<button class="import-btn" id="doneBtn" onclick="goToExplorer()">
Done - Go to Explorer
</button>
<div class="progress-section" id="progressSection">
<h3>Import Progress</h3>
<div id="progressLog"></div>
</div>
</div>
</div>
</div>
<script>
let profiles = [];
let selectedProfiles = new Set();
let importedProfiles = new Set();
async function loadProfiles() {
try {
const response = await fetch('/api/profiles');
const data = await response.json();
if (data.error) {
document.getElementById('loadingProfiles').innerHTML =
`<div class="empty-state"><div class="empty-state-icon">⚠️</div><p>${data.error}</p></div>`;
return;
}
profiles = data.profiles;
renderProfiles();
document.getElementById('loadingProfiles').style.display = 'none';
document.getElementById('profileSelection').style.display = 'block';
} catch (error) {
document.getElementById('loadingProfiles').innerHTML =
`<div class="empty-state"><div class="empty-state-icon">⚠️</div><p>Error loading profiles: ${error.message}</p></div>`;
}
}
function renderProfiles() {
const list = document.getElementById('profileList');
list.innerHTML = profiles.map((profile, idx) => `
<div class="profile-item">
<label>
<input type="checkbox"
id="profile-${idx}"
value="${profile}"
onchange="handleProfileSelection()">
<span>${profile}</span>
</label>
</div>
`).join('');
}
function toggleSelectAll() {
const checkboxes = document.querySelectorAll('.profile-item input[type="checkbox"]');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(cb => cb.checked = !allChecked);
handleProfileSelection();
}
function handleProfileSelection() {
selectedProfiles.clear();
document.querySelectorAll('.profile-item input[type="checkbox"]:checked').forEach(cb => {
selectedProfiles.add(cb.value);
});
if (selectedProfiles.size > 0) {
renderMfaInputs();
document.getElementById('mfaSection').classList.add('active');
} else {
document.getElementById('mfaSection').classList.remove('active');
}
}
function renderMfaInputs() {
const container = document.getElementById('mfaInputs');
// Save current MFA values and button states before re-rendering
const savedMfaValues = {};
const savedButtonStates = {};
selectedProfiles.forEach(profile => {
const input = document.getElementById(`mfa-${profile}`);
const btn = document.getElementById(`btn-${profile}`);
if (input) {
savedMfaValues[profile] = input.value;
}
if (btn) {
savedButtonStates[profile] = {
text: btn.textContent,
disabled: btn.disabled,
classes: btn.className
};
}
});
// Render inputs
container.innerHTML = Array.from(selectedProfiles).map(profile => `
<div class="mfa-input-group">
<label for="mfa-${profile}">${profile}</label>
<div class="mfa-input-row">
<input type="text"
id="mfa-${profile}"
placeholder="Enter MFA code (leave blank if not required)"
maxlength="6"
pattern="[0-9]*">
<button class="profile-import-btn"
id="btn-${profile}"
onclick="startProfileImport('${profile}')">
Start Import
</button>
</div>
</div>
`).join('');
// Restore saved values and button states
selectedProfiles.forEach(profile => {
const input = document.getElementById(`mfa-${profile}`);
const btn = document.getElementById(`btn-${profile}`);
if (savedMfaValues[profile] !== undefined) {
input.value = savedMfaValues[profile];
}
if (savedButtonStates[profile]) {
btn.textContent = savedButtonStates[profile].text;
btn.disabled = savedButtonStates[profile].disabled;
btn.className = savedButtonStates[profile].classes;
}
});
}
async function startProfileImport(profile) {
const btn = document.getElementById(`btn-${profile}`);
const mfaInput = document.getElementById(`mfa-${profile}`);
const progressSection = document.getElementById('progressSection');
const progressLog = document.getElementById('progressLog');
// Disable button and show progress
btn.disabled = true;
btn.textContent = 'Importing...';
progressSection.classList.add('active');
// Get MFA code for this profile
const mfaCode = mfaInput.value.trim();
try {
const response = await fetch('/api/import-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
profile: profile,
mfa_code: mfaCode
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let importSuccess = false;
while (true) {
const {value, done} = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n').filter(l => l.trim());
lines.forEach(line => {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.substring(6));
let className = 'progress-item';
if (data.status === 'success') className += ' success';
if (data.status === 'error') className += ' error';
progressLog.innerHTML += `<div class="${className}">${data.message}</div>`;
progressLog.scrollTop = progressLog.scrollHeight;
if (data.status === 'complete') {
importSuccess = true;
importedProfiles.add(profile);
btn.textContent = '✓ Imported';
btn.classList.add('success');
} else if (data.status === 'error' && data.message.includes('✗')) {
btn.textContent = '✗ Failed';
btn.classList.add('error');
btn.disabled = false;
}
}
});
}
if (!importSuccess && !btn.classList.contains('error')) {
btn.textContent = 'Start Import';
btn.disabled = false;
}
} catch (error) {
progressLog.innerHTML += `<div class="progress-item error">Error: ${error.message}</div>`;
btn.textContent = '✗ Failed';
btn.classList.add('error');
btn.disabled = false;
}
}
function goToExplorer() {
window.location.href = '/explorer';
}
// Load profiles on page load
loadProfiles();
</script>
</body>
</html>