Add service filter, batch operations, and snapshot features
- Service Filter Bar: search by name, filter by status (online/offline) - Batch Operations: multi-select containers for start/stop/restart - Container Snapshots: create and manage Docker checkpoints - Added filter bar and batch action bar to index.html - Added snapshot button to Admin tools section - New JS modules: service-filter.js, batch-operations.js, snapshot.js - Updated build.js to include new modules in bundle
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
// ========== CONTAINER SNAPSHOT / CHECKPOINT ==========
|
||||
(function() {
|
||||
// Inject modal HTML
|
||||
injectModal('snapshot-modal', `<div id="snapshot-modal" class="weather-modal">
|
||||
<div class="weather-modal-content" style="min-width: 700px; max-width: 850px;">
|
||||
<h3>💾 Container Snapshots</h3>
|
||||
<p class="modal-subtitle">
|
||||
Create and manage Docker container checkpoints for instant state recovery.
|
||||
</p>
|
||||
|
||||
<div id="snapshot-container-select-wrapper" style="margin-bottom: 16px;">
|
||||
<label style="font-size: 0.85rem; color: var(--muted);">Select Container:</label>
|
||||
<select id="snapshot-container-select" style="width: 100%; padding: 8px 12px; margin-top: 4px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--fg); font-size: 0.9rem;">
|
||||
<option value="">-- Select a container --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="snapshot-details" style="display: none; margin-bottom: 16px; padding: 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--card-hover);">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 0.85rem;">
|
||||
<div><span style="color: var(--muted);">Image:</span> <span id="snapshot-image"></span></div>
|
||||
<div><span style="color: var(--muted);">Status:</span> <span id="snapshot-status"></span></div>
|
||||
<div><span style="color: var(--muted);">Created:</span> <span id="snapshot-created"></span></div>
|
||||
<div><span style="color: var(--muted);">Container ID:</span> <span id="snapshot-id" style="font-family: monospace;"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-tabs" style="margin-bottom: 12px;">
|
||||
<button class="panel-tab active" data-panel="snapshot-create">Create Snapshot</button>
|
||||
<button class="panel-tab" data-panel="snapshot-list">Manage Snapshots</button>
|
||||
</div>
|
||||
|
||||
<div id="snapshot-create" class="panel-section active">
|
||||
<div style="margin-bottom: 12px;">
|
||||
<label style="font-size: 0.85rem; color: var(--muted);">Snapshot Name:</label>
|
||||
<input type="text" id="snapshot-name" placeholder="e.g., before-update-2024"
|
||||
style="width: 100%; padding: 8px 12px; margin-top: 4px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--fg); font-size: 0.9rem; box-sizing: border-box;" />
|
||||
</div>
|
||||
<div style="margin-bottom: 12px;">
|
||||
<label class="checkbox-label" style="font-size: 0.85rem;">
|
||||
<input type="checkbox" id="snapshot-leave-running" checked />
|
||||
Leave container running after checkpoint (resume without restart)
|
||||
</label>
|
||||
</div>
|
||||
<button id="snapshot-create-btn" class="btn-accent-solid" style="width: 100%;">💾 Create Snapshot</button>
|
||||
<div id="snapshot-create-status" style="margin-top: 12px; text-align: center; font-size: 0.85rem;"></div>
|
||||
</div>
|
||||
|
||||
<div id="snapshot-list" class="panel-section" style="display: none;">
|
||||
<div id="snapshot-list-container" class="scroll-container" style="max-height: 300px;">
|
||||
<div class="panel-empty"><span class="empty-icon">💾</span>Select a container to view its snapshots</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="weather-modal-buttons modal-footer-bar">
|
||||
<button id="snapshot-close">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
const modal = document.getElementById('snapshot-modal');
|
||||
const openBtn = document.getElementById('snapshot-btn');
|
||||
const closeBtn = document.getElementById('snapshot-close');
|
||||
const containerSelect = document.getElementById('snapshot-container-select');
|
||||
const detailsDiv = document.getElementById('snapshot-details');
|
||||
const createBtn = document.getElementById('snapshot-create-btn');
|
||||
const createStatus = document.getElementById('snapshot-create-status');
|
||||
|
||||
let currentContainerId = null;
|
||||
|
||||
async function loadContainers() {
|
||||
try {
|
||||
const res = await fetch('/api/v1/containers');
|
||||
const data = await res.json();
|
||||
if (!data.success || !data.containers) return;
|
||||
|
||||
containerSelect.innerHTML = '<option value="">-- Select a container --</option>';
|
||||
for (const c of data.containers) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = c.id;
|
||||
opt.textContent = `${c.name || c.id} (${c.image || 'unknown'})`;
|
||||
opt.dataset.name = c.name;
|
||||
opt.dataset.image = c.image;
|
||||
opt.dataset.status = c.status;
|
||||
opt.dataset.created = c.created;
|
||||
containerSelect.appendChild(opt);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load containers:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function showContainerDetails(opt) {
|
||||
if (!opt || !opt.value) {
|
||||
detailsDiv.style.display = 'none';
|
||||
currentContainerId = null;
|
||||
return;
|
||||
}
|
||||
currentContainerId = opt.value;
|
||||
document.getElementById('snapshot-image').textContent = opt.dataset.image || '-';
|
||||
document.getElementById('snapshot-status').textContent = opt.dataset.status || '-';
|
||||
document.getElementById('snapshot-created').textContent = opt.dataset.created ? new Date(opt.dataset.created * 1000).toLocaleString() : '-';
|
||||
document.getElementById('snapshot-id').textContent = opt.value.substring(0, 12);
|
||||
detailsDiv.style.display = '';
|
||||
}
|
||||
|
||||
async function createSnapshot() {
|
||||
if (!currentContainerId) {
|
||||
createStatus.textContent = 'Please select a container first';
|
||||
createStatus.style.color = 'var(--bad-fg)';
|
||||
return;
|
||||
}
|
||||
const name = document.getElementById('snapshot-name').value.trim();
|
||||
if (!name) {
|
||||
createStatus.textContent = 'Please enter a snapshot name';
|
||||
createStatus.style.color = 'var(--bad-fg)';
|
||||
return;
|
||||
}
|
||||
const leaveRunning = document.getElementById('snapshot-leave-running').checked;
|
||||
|
||||
createBtn.disabled = true;
|
||||
createBtn.textContent = 'Creating...';
|
||||
createStatus.textContent = '';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/v1/containers/${encodeURIComponent(currentContainerId)}/checkpoint`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, leaveRunning })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
createStatus.textContent = `✓ Snapshot "${name}" created successfully`;
|
||||
createStatus.style.color = 'var(--ok-fg)';
|
||||
document.getElementById('snapshot-name').value = '';
|
||||
} else {
|
||||
createStatus.textContent = `✗ Failed: ${data.error || 'Unknown error'}`;
|
||||
createStatus.style.color = 'var(--bad-fg)';
|
||||
}
|
||||
} catch (e) {
|
||||
createStatus.textContent = `✗ Error: ${e.message}`;
|
||||
createStatus.style.color = 'var(--bad-fg)';
|
||||
} finally {
|
||||
createBtn.disabled = false;
|
||||
createBtn.textContent = '💾 Create Snapshot';
|
||||
}
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
modal.classList.add('show');
|
||||
loadContainers();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.classList.remove('show');
|
||||
detailsDiv.style.display = 'none';
|
||||
currentContainerId = null;
|
||||
containerSelect.selectedIndex = 0;
|
||||
}
|
||||
|
||||
openBtn?.addEventListener('click', openModal);
|
||||
closeBtn?.addEventListener('click', closeModal);
|
||||
wireModal(modal, closeBtn);
|
||||
|
||||
containerSelect?.addEventListener('change', (e) => {
|
||||
const opt = containerSelect.options[containerSelect.selectedIndex];
|
||||
showContainerDetails(opt);
|
||||
});
|
||||
|
||||
createBtn?.addEventListener('click', createSnapshot);
|
||||
|
||||
// Tab switching
|
||||
modal?.querySelectorAll('.panel-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
modal.querySelectorAll('.panel-tab').forEach(t => t.classList.remove('active'));
|
||||
modal.querySelectorAll('.panel-section').forEach(s => s.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
modal.querySelector(`#${tab.dataset.panel}`).classList.add('active');
|
||||
});
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user