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,138 @@
|
||||
// ========== BATCH CONTAINER OPERATIONS ==========
|
||||
(function() {
|
||||
const batchBtn = document.getElementById('batch-operations-btn');
|
||||
const batchBar = document.getElementById('batch-action-bar');
|
||||
const batchCount = document.getElementById('batch-selected-count');
|
||||
const startBtn = document.getElementById('batch-start-btn');
|
||||
const stopBtn = document.getElementById('batch-stop-btn');
|
||||
const restartBtn = document.getElementById('batch-restart-btn');
|
||||
const cancelBtn = document.getElementById('batch-cancel-btn');
|
||||
|
||||
let batchMode = false;
|
||||
let selectedContainers = new Set();
|
||||
|
||||
function enterBatchMode() {
|
||||
batchMode = true;
|
||||
selectedContainers.clear();
|
||||
batchBar.style.display = '';
|
||||
batchBtn.textContent = '✓ Exit Batch Mode';
|
||||
updateSelectedCount();
|
||||
|
||||
// Add checkboxes to all cards with containerId
|
||||
const cards = document.querySelectorAll('#cards .card[data-app]');
|
||||
cards.forEach(card => {
|
||||
const containerId = card.dataset.containerId;
|
||||
if (!containerId) return;
|
||||
|
||||
// Remove existing checkbox if any
|
||||
const existing = card.querySelector('.batch-checkbox');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.className = 'batch-checkbox';
|
||||
checkbox.dataset.containerId = containerId;
|
||||
checkbox.dataset.serviceName = card.querySelector('.name')?.textContent || containerId;
|
||||
checkbox.style.cssText = 'position: absolute; top: 8px; left: 8px; z-index: 10; width: 18px; height: 18px; cursor: pointer;';
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
e.stopPropagation();
|
||||
if (checkbox.checked) {
|
||||
selectedContainers.add(containerId);
|
||||
} else {
|
||||
selectedContainers.delete(containerId);
|
||||
}
|
||||
updateSelectedCount();
|
||||
});
|
||||
card.style.position = 'relative';
|
||||
card.insertBefore(checkbox, card.firstChild);
|
||||
});
|
||||
}
|
||||
|
||||
function exitBatchMode() {
|
||||
batchMode = false;
|
||||
selectedContainers.clear();
|
||||
batchBar.style.display = 'none';
|
||||
batchBtn.textContent = '☰ Batch Operations';
|
||||
|
||||
// Remove all checkboxes
|
||||
document.querySelectorAll('.batch-checkbox').forEach(cb => cb.remove());
|
||||
}
|
||||
|
||||
function updateSelectedCount() {
|
||||
const count = selectedContainers.size;
|
||||
batchCount.textContent = `${count} selected`;
|
||||
startBtn.disabled = count === 0;
|
||||
stopBtn.disabled = count === 0;
|
||||
restartBtn.disabled = count === 0;
|
||||
}
|
||||
|
||||
async function batchAction(action) {
|
||||
if (selectedContainers.size === 0) return;
|
||||
|
||||
const containers = Array.from(selectedContainers);
|
||||
const actionLabel = { start: 'Starting', stop: 'Stopping', restart: 'Restarting' }[action];
|
||||
|
||||
if (!confirm(`${actionLabel} ${containers.length} container(s)? This cannot be undone.`)) return;
|
||||
|
||||
const btns = [startBtn, stopBtn, restartBtn];
|
||||
btns.forEach(b => { b.disabled = true; b.textContent = '...'; });
|
||||
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
const errors = [];
|
||||
|
||||
for (const containerId of containers) {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/containers/${encodeURIComponent(containerId)}/${action}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (res.ok) {
|
||||
success++;
|
||||
} else {
|
||||
failed++;
|
||||
const data = await res.json().catch(() => ({}));
|
||||
errors.push(`${containerId}: ${data.error || res.statusText}`);
|
||||
}
|
||||
} catch (e) {
|
||||
failed++;
|
||||
errors.push(`${containerId}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore buttons
|
||||
btns[0].textContent = '▶ Start All';
|
||||
btns[1].textContent = '⬛ Stop All';
|
||||
btns[2].textContent = '🔄 Restart All';
|
||||
updateSelectedCount();
|
||||
|
||||
// Show results
|
||||
if (failed === 0) {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification(`${actionLabel} completed: ${success} container(s)`, 'success');
|
||||
}
|
||||
} else {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification(`${actionLabel}: ${success} succeeded, ${failed} failed`, 'warning');
|
||||
}
|
||||
console.error('Batch operation errors:', errors);
|
||||
}
|
||||
|
||||
// Refresh dashboard after a short delay
|
||||
setTimeout(() => {
|
||||
if (typeof refreshAll === 'function') refreshAll();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
batchBtn?.addEventListener('click', () => {
|
||||
if (batchMode) {
|
||||
exitBatchMode();
|
||||
} else {
|
||||
enterBatchMode();
|
||||
}
|
||||
});
|
||||
|
||||
startBtn?.addEventListener('click', () => batchAction('start'));
|
||||
stopBtn?.addEventListener('click', () => batchAction('stop'));
|
||||
restartBtn?.addEventListener('click', () => batchAction('restart'));
|
||||
cancelBtn?.addEventListener('click', exitBatchMode);
|
||||
})();
|
||||
Reference in New Issue
Block a user