add indicators for tasks

This commit is contained in:
Raven Scott 2024-11-29 21:57:53 -05:00
parent ebe25cbe1c
commit e33023e250
2 changed files with 273 additions and 72 deletions

268
app.js
View File

@ -20,9 +20,94 @@ const connections = {};
window.openTerminals = {}; window.openTerminals = {};
let activePeer = null; let activePeer = null;
window.activePeer = null; // Expose to other modules window.activePeer = null; // Expose to other modules
hideStatusIndicator();
function waitForPeerResponse(expectedMessageFragment, timeout = 900000) {
console.log(`[DEBUG] Waiting for peer response with fragment: "${expectedMessageFragment}"`);
return new Promise((resolve, reject) => {
const startTime = Date.now();
window.handlePeerResponse = (response) => {
console.log(`[DEBUG] Received response: ${JSON.stringify(response)}`);
if (response && response.success && response.message.includes(expectedMessageFragment)) {
console.log(`[DEBUG] Expected response received: ${response.message}`);
resolve(response);
} else if (Date.now() - startTime > timeout) {
console.warn('[WARN] Timeout while waiting for peer response');
reject(new Error('Timeout waiting for peer response'));
}
};
// Timeout fallback
setTimeout(() => {
console.warn('[WARN] Timed out waiting for response');
reject(new Error('Timed out waiting for peer response'));
}, timeout);
});
}
// Initialize the app // Initialize the app
console.log('[INFO] Client app initialized'); console.log('[INFO] Client app initialized');
document.addEventListener('DOMContentLoaded', () => {
const statusIndicator = document.getElementById('status-indicator');
if (statusIndicator) {
statusIndicator.remove();
console.log('[INFO] Status indicator removed from DOM on load');
}
});
// Show Status Indicator
// Modify showStatusIndicator to recreate it dynamically
function showStatusIndicator(message = 'Processing...') {
const statusIndicator = document.createElement('div');
statusIndicator.id = 'status-indicator';
statusIndicator.className = 'position-fixed top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-dark bg-opacity-75';
statusIndicator.innerHTML = `
<div class="text-center">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3 text-light">${message}</p>
</div>
`;
document.body.appendChild(statusIndicator);
}
function hideStatusIndicator() {
const statusIndicator = document.getElementById('status-indicator');
if (statusIndicator) {
console.log('[DEBUG] Hiding status indicator');
statusIndicator.remove();
} else {
console.error('[ERROR] Status indicator element not found!');
}
}
// Show Alert
function showAlert(type, message) {
const alertContainer = document.getElementById('alert-container');
const alert = document.createElement('div');
alert.className = `alert alert-${type} alert-dismissible fade show`;
alert.role = 'alert';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
alertContainer.appendChild(alert);
// Automatically hide the status indicator for errors or success
if (type === 'danger' || type === 'success') {
hideStatusIndicator();
}
// Automatically remove the alert after 5 seconds
setTimeout(() => {
alert.classList.remove('show');
setTimeout(() => alert.remove(), 300); // Bootstrap's fade-out transition duration
}, 5000);
}
// Collapse Sidebar Functionality // Collapse Sidebar Functionality
const collapseSidebarBtn = document.getElementById('collapse-sidebar-btn'); const collapseSidebarBtn = document.getElementById('collapse-sidebar-btn');
@ -38,10 +123,20 @@ function handlePeerData(data, topicId, peer) {
const response = JSON.parse(data.toString()); const response = JSON.parse(data.toString());
console.log(`[DEBUG] Received data from peer (topic: ${topicId}): ${JSON.stringify(response)}`); console.log(`[DEBUG] Received data from peer (topic: ${topicId}): ${JSON.stringify(response)}`);
if (response.error) {
console.error(`[ERROR] Server error: ${response.error}`);
showAlert('danger', response.error);
hideStatusIndicator();
return;
}
if (response.type === 'containers') { if (response.type === 'containers') {
if (window.activePeer === peer) { if (window.activePeer === peer) {
renderContainers(response.data); renderContainers(response.data);
} }
} else if (response.type === 'stats') {
console.log(`[DEBUG] Updating stats for container: ${response.data.id}`);
updateContainerStats(response.data); // Call the stats update function
} else if (response.type === 'terminalOutput') { } else if (response.type === 'terminalOutput') {
appendTerminalOutput(response.data, response.containerId, response.encoding); appendTerminalOutput(response.data, response.containerId, response.encoding);
} else if (response.type === 'containerConfig') { } else if (response.type === 'containerConfig') {
@ -49,13 +144,14 @@ function handlePeerData(data, topicId, peer) {
window.inspectContainerCallback(response.data); window.inspectContainerCallback(response.data);
window.inspectContainerCallback = null; // Reset the callback window.inspectContainerCallback = null; // Reset the callback
} }
} else if (response.type === 'stats') { }
updateContainerStats(response.data);
} else if (response.error) { if (typeof window.handlePeerResponse === 'function') {
console.error(`[ERROR] Server error: ${response.error}`); window.handlePeerResponse(response);
} }
} catch (err) { } catch (err) {
console.error(`[ERROR] Failed to parse data from peer (topic: ${topicId}): ${err.message}`); console.error(`[ERROR] Failed to process peer data: ${err.message}`);
showAlert('danger', 'Failed to process peer data.');
} }
} }
@ -344,9 +440,10 @@ function renderContainers(containers) {
// Add event listeners for action buttons // Add event listeners for action buttons
addActionListeners(row, container); addActionListeners(row, container);
// Add event listener for duplicate button // Add event listener for duplicate button
const duplicateBtn = row.querySelector('.action-duplicate'); const duplicateBtn = row.querySelector('.action-duplicate');
duplicateBtn.addEventListener('click', () => openDuplicateModal(container)); duplicateBtn.addEventListener('click', () => openDuplicateModal(container));
}); });
} }
@ -356,63 +453,120 @@ function addActionListeners(row, container) {
const removeBtn = row.querySelector('.action-remove'); const removeBtn = row.querySelector('.action-remove');
const terminalBtn = row.querySelector('.action-terminal'); const terminalBtn = row.querySelector('.action-terminal');
startBtn.addEventListener('click', () => { // Start Button
startBtn.addEventListener('click', async () => {
showStatusIndicator(`Starting container "${container.Names[0]}"...`);
sendCommand('startContainer', { id: container.Id }); sendCommand('startContainer', { id: container.Id });
const expectedMessageFragment = `Container ${container.Id} started`;
try {
const response = await waitForPeerResponse(expectedMessageFragment);
console.log('[DEBUG] Start container response:', response);
showAlert('success', response.message);
// Refresh the container list to update states
sendCommand('listContainers');
} catch (error) {
console.error('[ERROR] Failed to start container:', error.message);
showAlert('danger', error.message || 'Failed to start container.');
} finally {
console.log('[DEBUG] Hiding status indicator in startBtn finally block');
hideStatusIndicator();
}
}); });
stopBtn.addEventListener('click', () => { stopBtn.addEventListener('click', async () => {
showStatusIndicator(`Stopping container "${container.Names[0]}"...`);
sendCommand('stopContainer', { id: container.Id }); sendCommand('stopContainer', { id: container.Id });
const expectedMessageFragment = `Container ${container.Id} stopped`;
try {
const response = await waitForPeerResponse(expectedMessageFragment);
console.log('[DEBUG] Stop container response:', response);
showAlert('success', response.message);
// Refresh the container list to update states
sendCommand('listContainers');
} catch (error) {
console.error('[ERROR] Failed to stop container:', error.message);
showAlert('danger', error.message || 'Failed to stop container.');
} finally {
console.log('[DEBUG] Hiding status indicator in stopBtn finally block');
hideStatusIndicator();
}
}); });
removeBtn.addEventListener('click', () => { // Remove Button
// Show the delete confirmation modal removeBtn.addEventListener('click', async () => {
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal')); const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
deleteModal.show(); deleteModal.show();
// Handle confirmation button
const confirmDeleteBtn = document.getElementById('confirm-delete-btn'); const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
confirmDeleteBtn.onclick = () => { confirmDeleteBtn.onclick = async () => {
console.log(`[INFO] Deleting container: ${container.Id}`); deleteModal.hide();
showStatusIndicator(`Deleting container "${container.Names[0]}"...`);
// Check if the container has active terminals // Check if the container has active terminals
if (window.openTerminals[container.Id]) { if (window.openTerminals[container.Id]) {
console.log(`[INFO] Closing active terminals for container: ${container.Id}`); console.log(`[INFO] Closing active terminals for container: ${container.Id}`);
window.openTerminals[container.Id].forEach((terminalId) => { window.openTerminals[container.Id].forEach((terminalId) => {
try { try {
cleanUpTerminal(terminalId); // Use your terminal.js cleanup logic cleanUpTerminal(terminalId);
} catch (err) { } catch (err) {
console.error(`[ERROR] Failed to clean up terminal ${terminalId}: ${err.message}`); console.error(`[ERROR] Failed to clean up terminal ${terminalId}: ${err.message}`);
} }
}); });
delete window.openTerminals[container.Id]; // Remove from open terminals delete window.openTerminals[container.Id];
} }
// Check if the terminal modal is active and hide it // Hide the terminal modal if it is active
const terminalModal = document.getElementById('terminal-modal'); const terminalModal = document.getElementById('terminal-modal');
if (terminalModal.style.display === 'flex') { if (terminalModal.style.display === 'flex') {
console.log(`[INFO] Hiding terminal modal for container: ${container.Id}`); console.log(`[INFO] Hiding terminal modal for container: ${container.Id}`);
terminalModal.style.display = 'none'; terminalModal.style.display = 'none';
} }
// Send the removeContainer command
sendCommand('removeContainer', { id: container.Id }); sendCommand('removeContainer', { id: container.Id });
deleteModal.hide(); // Close the delete confirmation modal
// Remove the container row from the container list const expectedMessageFragment = `Container ${container.Id} removed`;
const row = containerList.querySelector(`tr[data-container-id="${container.Id}"]`);
if (row) { try {
row.remove(); const response = await waitForPeerResponse(expectedMessageFragment);
console.log(`[INFO] Removed container row for container ID: ${container.Id}`); console.log('[DEBUG] Remove container response:', response);
showAlert('success', response.message);
// Refresh the container list to update states
sendCommand('listContainers');
} catch (error) {
console.error('[ERROR] Failed to delete container:', error.message);
showAlert('danger', error.message || `Failed to delete container "${container.Names[0]}".`);
} finally {
console.log('[DEBUG] Hiding status indicator in removeBtn finally block');
hideStatusIndicator();
} }
}; };
}); });
terminalBtn.addEventListener('click', () => { terminalBtn.addEventListener('click', () => {
startTerminal(container.Id, container.Names[0] || container.Id); console.log(`[DEBUG] Opening terminal for container ID: ${container.Id}`);
try {
startTerminal(container.Id, container.Names[0] || container.Id);
} catch (error) {
console.error(`[ERROR] Failed to start terminal for container ${container.Id}: ${error.message}`);
showAlert('danger', `Failed to start terminal: ${error.message}`);
}
}); });
} }
// Function to update container statistics // Function to update container statistics
function updateContainerStats(stats) { function updateContainerStats(stats) {
console.log(`[DEBUG] Updating stats for container ID: ${stats.id}`); console.log(`[DEBUG] Updating stats for container ID: ${stats.id}`);
@ -444,30 +598,41 @@ function updateContainerStats(stats) {
function openDuplicateModal(container) { function openDuplicateModal(container) {
console.log(`[INFO] Opening Duplicate Modal for container: ${container.Id}`); console.log(`[INFO] Opening Duplicate Modal for container: ${container.Id}`);
// Send a command to get container configurations showStatusIndicator('Fetching container configuration...');
// Send a command to inspect the container
sendCommand('inspectContainer', { id: container.Id }); sendCommand('inspectContainer', { id: container.Id });
// Listen for the response // Listen for the inspectContainer response
window.inspectContainerCallback = (config) => { window.inspectContainerCallback = (config) => {
hideStatusIndicator();
if (!config) { if (!config) {
alert('Failed to retrieve container configuration.'); console.error('[ERROR] Failed to retrieve container configuration.');
showAlert('danger', 'Failed to retrieve container configuration.');
return; return;
} }
console.log("TESTER: " + config.HostConfig?.CpusetCpus) console.log(`[DEBUG] Retrieved container configuration: ${JSON.stringify(config)}`);
let CPUs = config.HostConfig?.CpusetCpus.split(",");
// Populate the modal fields with the current configurations // Parse configuration and populate the modal fields
document.getElementById('container-name').value = config.Name.replace(/^\//, ''); try {
document.getElementById('container-hostname').value = config.Config.Hostname.replace(/^\//, ''); const CPUs = config.HostConfig?.CpusetCpus?.split(',') || [];
document.getElementById('container-image').value = config.Config.Image;
document.getElementById('container-netmode').value = config.HostConfig?.NetworkMode;
document.getElementById('container-cpu').value = CPUs.length;
document.getElementById('container-memory').value = Math.round(config.HostConfig?.Memory / (1024 * 1024));
document.getElementById('container-config').value = JSON.stringify(config, null, 2);
// Show the modal document.getElementById('container-name').value = config.Name.replace(/^\//, '');
duplicateModal.show(); document.getElementById('container-hostname').value = config.Config.Hostname || '';
document.getElementById('container-image').value = config.Config.Image || '';
document.getElementById('container-netmode').value = config.HostConfig?.NetworkMode || '';
document.getElementById('container-cpu').value = CPUs.length || 0;
document.getElementById('container-memory').value = Math.round(config.HostConfig?.Memory / (1024 * 1024)) || 0;
document.getElementById('container-config').value = JSON.stringify(config, null, 2);
// Show the duplicate modal
duplicateModal.show();
} catch (error) {
console.error(`[ERROR] Failed to populate modal fields: ${error.message}`);
showAlert('danger', 'Failed to populate container configuration fields.');
}
}; };
} }
@ -475,6 +640,9 @@ function openDuplicateModal(container) {
// Handle the Duplicate Container Form Submission // Handle the Duplicate Container Form Submission
duplicateContainerForm.addEventListener('submit', (e) => { duplicateContainerForm.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
duplicateModal.hide();
showStatusIndicator('Duplicating container...');
const name = document.getElementById('container-name').value.trim(); const name = document.getElementById('container-name').value.trim();
const hostname = document.getElementById('container-hostname').value.trim(); const hostname = document.getElementById('container-hostname').value.trim();
@ -488,23 +656,21 @@ duplicateContainerForm.addEventListener('submit', (e) => {
try { try {
config = JSON.parse(configJSON); config = JSON.parse(configJSON);
} catch (err) { } catch (err) {
alert('Invalid JSON in configuration.'); hideStatusIndicator();
showAlert('danger', 'Invalid JSON in configuration.');
return; return;
} }
console.log(`[INFO] Sending duplicateContainer command for name: ${name}`);
// Send the duplicate command to the server
sendCommand('duplicateContainer', { name, image, hostname, netmode, cpu, memory, config }); sendCommand('duplicateContainer', { name, image, hostname, netmode, cpu, memory, config });
// Close the modal // Simulate delay for the demo
duplicateModal.hide();
// Trigger container list update after a short delay
setTimeout(() => { setTimeout(() => {
console.log('[INFO] Fetching updated container list after duplication'); hideStatusIndicator();
showAlert('success', 'Container duplicated successfully!');
// Refresh container list
sendCommand('listContainers'); sendCommand('listContainers');
}, 2000); // Wait for duplication to complete }, 2000); // Simulated processing time
}); });

View File

@ -120,7 +120,6 @@
#terminal-modal .header { #terminal-modal .header {
background-color: #444; background-color: #444;
cursor: move; cursor: move;
/* Change cursor to indicate drag is possible */
padding: 10px; padding: 10px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -165,6 +164,30 @@
border-radius: 5px; border-radius: 5px;
cursor: pointer; cursor: pointer;
} }
#status-indicator {
display: none; /* Ensure it's hidden by default */
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.75);
z-index: 1050;
}
#status-indicator .spinner-border {
width: 3rem;
height: 3rem;
}
#status-indicator p {
margin-top: 1rem;
color: #fff;
font-size: 1.25rem;
}
</style> </style>
</head> </head>
@ -222,7 +245,7 @@
<input type="text" class="form-control" id="container-name" required> <input type="text" class="form-control" id="container-name" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="container-image" class="form-label">Hostname</label> <label for="container-hostname" class="form-label">Hostname</label>
<input type="text" class="form-control" id="container-hostname" required> <input type="text" class="form-control" id="container-hostname" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@ -230,7 +253,7 @@
<input type="text" class="form-control" id="container-image" required> <input type="text" class="form-control" id="container-image" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="container-image" class="form-label">Net Mode</label> <label for="container-netmode" class="form-label">Net Mode</label>
<input type="text" class="form-control" id="container-netmode" required> <input type="text" class="form-control" id="container-netmode" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@ -241,7 +264,6 @@
<label for="container-memory" class="form-label">Memory (MB)</label> <label for="container-memory" class="form-label">Memory (MB)</label>
<input type="number" class="form-control" id="container-memory" required> <input type="number" class="form-control" id="container-memory" required>
</div> </div>
<!-- Container Configuration as JSON -->
<div class="mb-3"> <div class="mb-3">
<label for="container-config" class="form-label">Container Configuration (JSON)</label> <label for="container-config" class="form-label">Container Configuration (JSON)</label>
<textarea class="form-control" id="container-config" rows="10" required></textarea> <textarea class="form-control" id="container-config" rows="10" required></textarea>
@ -264,37 +286,50 @@
</div> </div>
</div> </div>
<div id="terminal-container"></div> <div id="terminal-container"></div>
<div id="terminal-resize-handle"></div> <!-- Resize handle --> <div id="terminal-resize-handle"></div>
</div> </div>
<!-- Delete Confirmation Modal --> <!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true"> <div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm"> <div class="modal-dialog modal-sm">
<div class="modal-content bg-dark text-white"> <div class="modal-content bg-dark text-white">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">Confirm Deletion</h5> <h5 class="modal-title" id="deleteModalLabel">Confirm Deletion</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
Are you sure you want to delete this container? Are you sure you want to delete this container?
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="confirm-delete-btn" class="btn btn-danger">Delete</button> <button type="button" id="confirm-delete-btn" class="btn btn-danger">Delete</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Status Indicator Overlay -->
<div id="status-indicator"
class="position-fixed top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-dark bg-opacity-75"
style="display: none; z-index: 1050;">
<div class="text-center">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3 text-light">Processing...</p>
</div>
</div> </div>
<div id="tray"></div> <!-- Alert Container -->
<div id="alert-container" class="position-fixed top-0 start-50 translate-middle-x mt-3"
style="z-index: 1051; max-width: 90%;"></div>
<!-- xterm.js --> <!-- xterm.js -->
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script> <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
<!-- Bootstrap JS for Modal Functionality --> <!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Your App JS --> <!-- Your App JS -->
<script type="module" src="app.js"></script> <script type="module" src="app.js"></script>
</body> </body>