continued improvements

This commit is contained in:
Raven Scott
2025-11-24 19:02:16 -05:00
parent 9b5884b975
commit 1a283c6087
2 changed files with 439 additions and 160 deletions
+285 -160
View File
@@ -643,34 +643,53 @@ function renderImages(images) {
imagesList.querySelectorAll('.action-remove-image').forEach(btn => {
btn.addEventListener('click', () => {
const imageId = btn.dataset.imageId;
if (confirm('Are you sure you want to remove this image?')) {
showConfirmModal('Are you sure you want to remove this image?', () => {
sendCommand('removeImage', { id: imageId, force: true });
setTimeout(() => loadImages(), 1000);
}
});
});
});
imagesList.querySelectorAll('.action-tag-image').forEach(btn => {
btn.addEventListener('click', async () => {
const imageId = btn.dataset.imageId;
const repo = prompt('Enter repository name (e.g., myrepo):');
if (repo) {
const tag = prompt('Enter tag (default: latest):', 'latest') || 'latest';
showStatusIndicator(`Tagging image...`);
sendCommand('tagImage', { id: imageId, repo, tag });
try {
const response = await waitForPeerResponse('Image tagged as');
showAlert('success', response.message || 'Image tagged successfully');
loadImages();
} catch (error) {
console.error('[ERROR] Failed to tag image:', error);
showAlert('danger', error.message || 'Failed to tag image');
} finally {
hideStatusIndicator();
const modal = new bootstrap.Modal(document.getElementById('tagImageModal'));
const repoInput = document.getElementById('tag-repo');
const tagInput = document.getElementById('tag-tag');
const confirmBtn = document.getElementById('confirm-tag-btn');
repoInput.value = '';
tagInput.value = 'latest';
// Remove old listeners
const newConfirmBtn = confirmBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
newConfirmBtn.addEventListener('click', async () => {
const repo = repoInput.value.trim();
if (repo) {
const tag = tagInput.value.trim() || 'latest';
modal.hide();
showStatusIndicator(`Tagging image...`);
sendCommand('tagImage', { id: imageId, repo, tag });
try {
const response = await waitForPeerResponse('Image tagged as');
showAlert('success', response.message || 'Image tagged successfully');
loadImages();
} catch (error) {
console.error('[ERROR] Failed to tag image:', error);
showAlert('danger', error.message || 'Failed to tag image');
} finally {
hideStatusIndicator();
}
} else {
showAlert('danger', 'Repository name is required');
}
}
});
modal.show();
});
});
@@ -735,10 +754,10 @@ function renderNetworks(networks) {
networksList.querySelectorAll('.action-remove-network').forEach(btn => {
btn.addEventListener('click', () => {
const networkId = btn.dataset.networkId;
if (confirm('Are you sure you want to remove this network?')) {
showConfirmModal('Are you sure you want to remove this network?', () => {
sendCommand('removeNetwork', { id: networkId });
setTimeout(() => loadNetworks(), 1000);
}
});
});
});
@@ -752,6 +771,13 @@ function renderNetworks(networks) {
networksList.querySelectorAll('.action-connect-network').forEach(btn => {
btn.addEventListener('click', async () => {
const networkId = btn.dataset.networkId;
const modal = new bootstrap.Modal(document.getElementById('connectNetworkModal'));
const containerSelect = document.getElementById('connect-container-select');
const confirmBtn = document.getElementById('confirm-connect-btn');
// Store networkId for later use
modal._networkId = networkId;
// Get list of containers for selection
sendCommand('listContainers');
@@ -762,18 +788,32 @@ function renderNetworks(networks) {
const containers = response.data;
if (containers.length === 0) {
showAlert('warning', 'No containers available to connect');
if (typeof originalHandler === 'function') {
window.handlePeerResponse = originalHandler;
}
return;
}
// Create a simple selection dialog
const containerNames = containers.map(c => c.Names[0]?.replace(/^\//, '') || c.Id.substring(0, 12));
const selectedName = prompt(`Enter container name to connect to network:\n\nAvailable: ${containerNames.slice(0, 5).join(', ')}${containerNames.length > 5 ? '...' : ''}`, containerNames[0]);
// Populate select dropdown
containerSelect.innerHTML = '<option value="">Select a container...</option>';
containers.forEach(container => {
const name = container.Names[0]?.replace(/^\//, '') || container.Id.substring(0, 12);
const option = document.createElement('option');
option.value = container.Id;
option.textContent = `${name} (${container.State})`;
containerSelect.appendChild(option);
});
if (selectedName) {
const container = containers.find(c => c.Names[0]?.replace(/^\//, '') === selectedName || c.Id.startsWith(selectedName));
if (container) {
// Remove old listeners
const newConfirmBtn = confirmBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
newConfirmBtn.addEventListener('click', async () => {
const containerId = containerSelect.value;
if (containerId) {
modal.hide();
showStatusIndicator(`Connecting container to network...`);
sendCommand('connectNetwork', { networkId, containerId: container.Id });
sendCommand('connectNetwork', { networkId: modal._networkId, containerId });
try {
const response = await waitForPeerResponse('Container connected to network');
@@ -786,9 +826,11 @@ function renderNetworks(networks) {
hideStatusIndicator();
}
} else {
showAlert('danger', 'Container not found');
showAlert('danger', 'Please select a container');
}
}
});
modal.show();
if (typeof originalHandler === 'function') {
window.handlePeerResponse = originalHandler;
@@ -846,7 +888,20 @@ function renderStacks(stacks) {
stacksListBody.querySelectorAll('.action-remove-stack').forEach(btn => {
btn.addEventListener('click', async () => {
const stackName = btn.dataset.stackName;
if (!confirm(`Remove stack "${stackName}"? This will remove all containers in the stack.`)) return;
let confirmed = false;
await new Promise((resolve) => {
showConfirmModal(`Remove stack "${stackName}"? This will remove all containers in the stack.`, () => {
confirmed = true;
resolve();
});
const modalEl = document.getElementById('confirmModal');
if (modalEl) {
modalEl.addEventListener('hidden.bs.modal', () => {
if (!confirmed) resolve();
}, { once: true });
}
});
if (!confirmed) return;
showStatusIndicator(`Removing stack "${stackName}"...`);
sendCommand('removeStack', { stackName });
@@ -948,10 +1003,10 @@ function renderVolumes(volumes) {
volumesList.querySelectorAll('.action-remove-volume').forEach(btn => {
btn.addEventListener('click', () => {
const volumeName = btn.dataset.volumeName;
if (confirm('Are you sure you want to remove this volume? This cannot be undone.')) {
showConfirmModal('Are you sure you want to remove this volume? This cannot be undone.', () => {
sendCommand('removeVolume', { name: volumeName });
setTimeout(() => loadVolumes(), 1000);
}
});
});
});
@@ -2395,9 +2450,11 @@ document.addEventListener('DOMContentLoaded', () => {
if (clearBtn) {
clearBtn.addEventListener('click', () => {
const logsContent = document.getElementById('container-logs-content');
if (logsContent && confirm('Clear all logs?')) {
logsContent.innerHTML = '';
logsState.allLogs = [];
if (logsContent) {
showConfirmModal('Clear all logs?', () => {
logsContent.innerHTML = '';
logsState.allLogs = [];
});
}
});
}
@@ -2757,6 +2814,28 @@ document.addEventListener('DOMContentLoaded', () => {
};
});
// Helper function for confirmation modals
function showConfirmModal(message, onConfirm) {
const modal = new bootstrap.Modal(document.getElementById('confirmModal'));
const messageEl = document.getElementById('confirmModalMessage');
const confirmBtn = document.getElementById('confirmModalBtn');
messageEl.textContent = message;
// Remove old listeners
const newConfirmBtn = confirmBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
newConfirmBtn.addEventListener('click', () => {
modal.hide();
if (typeof onConfirm === 'function') {
onConfirm();
}
});
modal.show();
}
// Bulk Operations Functions
function getSelectedContainers() {
const checkboxes = document.querySelectorAll('.container-checkbox:checked');
@@ -2843,7 +2922,9 @@ async function bulkStartContainers() {
return;
}
if (!confirm(`Start ${selected.length} container(s)?`)) return;
await new Promise((resolve) => {
showConfirmModal(`Start ${selected.length} container(s)?`, resolve);
});
showStatusIndicator(`Starting ${selected.length} container(s)...`);
@@ -2871,7 +2952,20 @@ async function bulkStopContainers() {
return;
}
if (!confirm(`Stop ${selected.length} container(s)?`)) return;
let confirmed = false;
await new Promise((resolve) => {
showConfirmModal(`Stop ${selected.length} container(s)?`, () => {
confirmed = true;
resolve();
});
const modalEl = document.getElementById('confirmModal');
if (modalEl) {
modalEl.addEventListener('hidden.bs.modal', () => {
if (!confirmed) resolve();
}, { once: true });
}
});
if (!confirmed) return;
showStatusIndicator(`Stopping ${selected.length} container(s)...`);
@@ -2899,7 +2993,20 @@ async function bulkRemoveContainers() {
return;
}
if (!confirm(`Remove ${selected.length} container(s)? This action cannot be undone.`)) return;
let confirmed = false;
await new Promise((resolve) => {
showConfirmModal(`Remove ${selected.length} container(s)? This action cannot be undone.`, () => {
confirmed = true;
resolve();
});
const modalEl = document.getElementById('confirmModal');
if (modalEl) {
modalEl.addEventListener('hidden.bs.modal', () => {
if (!confirmed) resolve();
}, { once: true });
}
});
if (!confirmed) return;
showStatusIndicator(`Removing ${selected.length} container(s)...`);
@@ -2940,7 +3047,20 @@ async function bulkRemoveImages() {
const selected = getSelectedImages();
if (selected.length === 0) return;
if (!confirm(`Remove ${selected.length} image(s)? This action cannot be undone.`)) return;
let confirmed = false;
await new Promise((resolve) => {
showConfirmModal(`Remove ${selected.length} image(s)? This action cannot be undone.`, () => {
confirmed = true;
resolve();
});
const modalEl = document.getElementById('confirmModal');
if (modalEl) {
modalEl.addEventListener('hidden.bs.modal', () => {
if (!confirmed) resolve();
}, { once: true });
}
});
if (!confirmed) return;
showStatusIndicator(`Removing ${selected.length} image(s)...`);
let completed = 0;
@@ -4408,7 +4528,15 @@ function renderContainers(containers, topicId) {
<td>
<input type="checkbox" class="container-checkbox" data-container-id="${containerId}">
</td>
<td><a href="#" class="container-name-link" data-container-id="${containerId}">${name}</a></td>
<td>
<div class="d-flex align-items-center gap-2">
<span class="container-name-display" data-container-id="${containerId}" style="cursor: pointer; user-select: none;">${name}</span>
<button class="btn btn-outline-info action-rename p-1" title="Rename" style="font-size: 0.75rem;">
<i class="fas fa-edit"></i>
</button>
<a href="#" class="container-name-link d-none" data-container-id="${containerId}">${name}</a>
</div>
</td>
<td>${image}</td>
<td><span class="badge ${statusClass}">${state}</span></td>
<td class="cpu">
@@ -4442,33 +4570,18 @@ function renderContainers(containers, topicId) {
<button class="btn btn-outline-secondary action-pause p-1" title="Pause" ${container.State !== 'running' ? 'disabled' : ''}>
<i class="fas fa-pause"></i>
</button>
<button class="btn btn-outline-secondary action-unpause p-1" title="Unpause" ${container.State !== 'paused' ? 'disabled' : ''}>
<i class="fas fa-play"></i>
</button>
<button class="btn btn-outline-primary action-logs p-1" title="Logs">
<i class="fas fa-list-alt"></i>
</button>
<button class="btn btn-outline-primary action-terminal p-1" title="Terminal" ${container.State !== 'running' ? 'disabled' : ''}>
<i class="fas fa-terminal"></i>
</button>
<button class="btn btn-outline-success action-details p-1" title="View Details">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-outline-info action-inspect p-1" title="Inspect">
<i class="fas fa-info-circle"></i>
</button>
<button class="btn btn-outline-secondary action-duplicate p-1" title="Duplicate">
<i class="fas fa-clone"></i>
</button>
<button class="btn btn-outline-info action-rename p-1" title="Rename">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline-success action-commit p-1" title="Commit" ${container.State !== 'running' ? 'disabled' : ''}>
<i class="fas fa-save"></i>
</button>
<button class="btn btn-outline-primary action-exec p-1" title="Exec" ${container.State !== 'running' ? 'disabled' : ''}>
<i class="fas fa-code"></i>
</button>
<button class="btn btn-outline-danger action-remove p-1" title="Remove">
<i class="fas fa-trash"></i>
</button>
@@ -4485,12 +4598,7 @@ function renderContainers(containers, topicId) {
// Add event listener for duplicate button
const duplicateBtn = row.querySelector('.action-duplicate');
duplicateBtn.addEventListener('click', () => openDuplicateModal(container));
// Add event listener for details button
const detailsBtn = row.querySelector('.action-details');
if (detailsBtn) {
detailsBtn.addEventListener('click', () => showContainerDetails(container));
}
// Add event listener for clickable container name
// Add event listener for clickable container name (hidden link for details view)
const nameLink = row.querySelector('.container-name-link');
if (nameLink) {
nameLink.addEventListener('click', (e) => {
@@ -4498,6 +4606,15 @@ function renderContainers(containers, topicId) {
showContainerDetails(container);
});
}
// Add event listener for container name display (double-click to view details)
const nameDisplay = row.querySelector('.container-name-display');
if (nameDisplay) {
nameDisplay.addEventListener('dblclick', (e) => {
e.preventDefault();
showContainerDetails(container);
});
}
// Add event listeners for action buttons
addActionListeners(row, container);
});
@@ -4513,7 +4630,6 @@ function addActionListeners(row, container) {
const startBtn = row.querySelector('.action-start');
const stopBtn = row.querySelector('.action-stop');
const pauseBtn = row.querySelector('.action-pause');
const unpauseBtn = row.querySelector('.action-unpause');
const removeBtn = row.querySelector('.action-remove');
const terminalBtn = row.querySelector('.action-terminal');
const restartBtn = row.querySelector('.action-restart');
@@ -4623,107 +4739,116 @@ function addActionListeners(row, container) {
});
}
// Unpause Button
if (unpauseBtn) {
unpauseBtn.addEventListener('click', async () => {
showStatusIndicator(`Unpausing container "${container.Names[0]}"...`);
sendCommand('unpauseContainer', { id: container.Id });
const expectedMessageFragment = `Container ${container.Id} unpaused`;
try {
const response = await waitForPeerResponse(expectedMessageFragment);
showAlert('success', response.message);
sendCommand('listContainers');
} catch (error) {
console.error('[ERROR] Failed to unpause container:', error.message);
showAlert('danger', error.message || 'Failed to unpause container.');
} finally {
hideStatusIndicator();
}
});
}
// Rename Button
// Rename Button - Inline Edit
if (renameBtn) {
renameBtn.addEventListener('click', () => {
const currentName = container.Names[0]?.replace(/^\//, '') || '';
const newName = prompt(`Rename container "${currentName}":`, currentName);
if (newName && newName !== currentName) {
showStatusIndicator(`Renaming container...`);
sendCommand('renameContainer', { id: container.Id, name: newName });
const nameDisplay = row.querySelector('.container-name-display');
if (!nameDisplay) return;
const currentName = nameDisplay.textContent.trim();
const originalName = currentName;
// Create input field
const input = document.createElement('input');
input.type = 'text';
input.value = currentName;
input.className = 'form-control form-control-sm bg-dark text-white';
input.style.width = '200px';
input.style.display = 'inline-block';
// Replace display with input
const parent = nameDisplay.parentElement;
const nameDisplayClone = nameDisplay.cloneNode(true);
nameDisplay.style.display = 'none';
parent.insertBefore(input, nameDisplay);
// Focus and select
input.focus();
input.select();
// Save function
const saveName = async () => {
const newName = input.value.trim();
if (newName && newName !== originalName) {
sendCommand('renameContainer', { id: container.Id, name: newName });
const expectedMessageFragment = `Container renamed to "${newName}"`;
const expectedMessageFragment = `Container renamed to "${newName}"`;
waitForPeerResponse(expectedMessageFragment).then(response => {
showAlert('success', response.message);
sendCommand('listContainers');
}).catch(error => {
console.error('[ERROR] Failed to rename container:', error.message);
showAlert('danger', error.message || 'Failed to rename container.');
}).finally(() => {
hideStatusIndicator();
});
}
});
}
// Set up response handler
const originalHandler = window.handlePeerResponse;
const responseHandler = (response) => {
if (response && response.success && response.message && response.message.includes(expectedMessageFragment)) {
showAlert('success', response.message);
sendCommand('listContainers');
// Restore original handler
if (typeof originalHandler === 'function') {
window.handlePeerResponse = originalHandler;
} else {
window.handlePeerResponse = null;
}
} else if (typeof originalHandler === 'function') {
originalHandler(response);
}
};
window.handlePeerResponse = responseHandler;
// Commit Button
if (commitBtn) {
commitBtn.addEventListener('click', () => {
const currentName = container.Names[0]?.replace(/^\//, '') || 'container';
const repo = prompt(`Enter repository name for commit:`, currentName);
if (repo) {
const tag = prompt(`Enter tag (default: latest):`, 'latest') || 'latest';
const message = prompt(`Enter commit message (optional):`, '') || '';
// Also listen for container list updates as confirmation
const checkInterval = setInterval(() => {
const nameDisplays = document.querySelectorAll('.container-name-display');
nameDisplays.forEach(display => {
if (display.textContent.trim() === newName && display.dataset.containerId === container.Id) {
clearInterval(checkInterval);
showAlert('success', `Container renamed to "${newName}"`);
// Restore original handler
if (typeof originalHandler === 'function') {
window.handlePeerResponse = originalHandler;
} else {
window.handlePeerResponse = null;
}
}
});
}, 500);
// Clean up interval after 10 seconds
setTimeout(() => {
clearInterval(checkInterval);
}, 10000);
} else if (!newName) {
showAlert('danger', 'Container name cannot be empty');
nameDisplay.textContent = originalName;
} else {
// Name unchanged, just restore display
nameDisplay.textContent = originalName;
}
showStatusIndicator(`Committing container...`);
sendCommand('commitContainer', {
id: container.Id,
repo: repo,
tag: tag,
message: message
});
waitForPeerResponse('committed').then(response => {
showAlert('success', response.message || 'Container committed successfully');
sendCommand('listImages');
}).catch(error => {
console.error('[ERROR] Failed to commit container:', error.message);
showAlert('danger', error.message || 'Failed to commit container.');
}).finally(() => {
hideStatusIndicator();
});
}
});
}
// Exec Button
if (execBtn) {
execBtn.addEventListener('click', () => {
const command = prompt(`Enter command to execute in container:`, '/bin/sh');
if (command) {
showStatusIndicator(`Executing command...`);
sendCommand('execContainer', {
id: container.Id,
cmd: command.split(' '),
tty: true
});
// Open terminal for exec output
waitForPeerResponse('Exec session started').then(response => {
if (response.execId) {
// Start terminal session for exec - use regular terminal for now
startTerminal(container.Id, `Exec: ${container.Names[0]?.replace(/^\//, '') || container.Id.substring(0, 12)}`);
showAlert('success', 'Exec session started');
}
}).catch(error => {
console.error('[ERROR] Failed to exec container:', error.message);
showAlert('danger', error.message || 'Failed to exec container.');
}).finally(() => {
hideStatusIndicator();
});
}
// Restore display
parent.removeChild(input);
nameDisplay.style.display = '';
};
// Cancel function
const cancelEdit = () => {
parent.removeChild(input);
nameDisplay.style.display = '';
};
// Handle Enter key
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
saveName();
} else if (e.key === 'Escape') {
e.preventDefault();
cancelEdit();
}
});
// Handle blur (click outside)
input.addEventListener('blur', () => {
saveName();
});
});
}
@@ -5667,11 +5792,11 @@ function initNotificationTray() {
if (clearAllBtn) {
clearAllBtn.addEventListener('click', () => {
if (confirm('Are you sure you want to clear all notifications?')) {
showConfirmModal('Are you sure you want to clear all notifications?', () => {
notificationManager.clearAll();
renderNotifications();
updateBadge();
}
});
});
}