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 = {};
let activePeer = null;
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
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
const collapseSidebarBtn = document.getElementById('collapse-sidebar-btn');
@ -38,10 +123,20 @@ function handlePeerData(data, topicId, peer) {
const response = JSON.parse(data.toString());
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 (window.activePeer === peer) {
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') {
appendTerminalOutput(response.data, response.containerId, response.encoding);
} else if (response.type === 'containerConfig') {
@ -49,13 +144,14 @@ function handlePeerData(data, topicId, peer) {
window.inspectContainerCallback(response.data);
window.inspectContainerCallback = null; // Reset the callback
}
} else if (response.type === 'stats') {
updateContainerStats(response.data);
} else if (response.error) {
console.error(`[ERROR] Server error: ${response.error}`);
}
if (typeof window.handlePeerResponse === 'function') {
window.handlePeerResponse(response);
}
} 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
addActionListeners(row, container);
// Add event listener for duplicate button
const duplicateBtn = row.querySelector('.action-duplicate');
duplicateBtn.addEventListener('click', () => openDuplicateModal(container));
// Add event listener for duplicate button
const duplicateBtn = row.querySelector('.action-duplicate');
duplicateBtn.addEventListener('click', () => openDuplicateModal(container));
});
}
@ -356,63 +453,120 @@ function addActionListeners(row, container) {
const removeBtn = row.querySelector('.action-remove');
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 });
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 });
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', () => {
// Show the delete confirmation modal
// Remove Button
removeBtn.addEventListener('click', async () => {
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
deleteModal.show();
// Handle confirmation button
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
confirmDeleteBtn.onclick = () => {
console.log(`[INFO] Deleting container: ${container.Id}`);
confirmDeleteBtn.onclick = async () => {
deleteModal.hide();
showStatusIndicator(`Deleting container "${container.Names[0]}"...`);
// Check if the container has active terminals
if (window.openTerminals[container.Id]) {
console.log(`[INFO] Closing active terminals for container: ${container.Id}`);
window.openTerminals[container.Id].forEach((terminalId) => {
try {
cleanUpTerminal(terminalId); // Use your terminal.js cleanup logic
cleanUpTerminal(terminalId);
} catch (err) {
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');
if (terminalModal.style.display === 'flex') {
console.log(`[INFO] Hiding terminal modal for container: ${container.Id}`);
terminalModal.style.display = 'none';
}
// Send the removeContainer command
sendCommand('removeContainer', { id: container.Id });
deleteModal.hide(); // Close the delete confirmation modal
// Remove the container row from the container list
const row = containerList.querySelector(`tr[data-container-id="${container.Id}"]`);
if (row) {
row.remove();
console.log(`[INFO] Removed container row for container ID: ${container.Id}`);
const expectedMessageFragment = `Container ${container.Id} removed`;
try {
const response = await waitForPeerResponse(expectedMessageFragment);
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', () => {
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 updateContainerStats(stats) {
console.log(`[DEBUG] Updating stats for container ID: ${stats.id}`);
@ -444,30 +598,41 @@ function updateContainerStats(stats) {
function openDuplicateModal(container) {
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 });
// Listen for the response
// Listen for the inspectContainer response
window.inspectContainerCallback = (config) => {
hideStatusIndicator();
if (!config) {
alert('Failed to retrieve container configuration.');
console.error('[ERROR] Failed to retrieve container configuration.');
showAlert('danger', 'Failed to retrieve container configuration.');
return;
}
console.log("TESTER: " + config.HostConfig?.CpusetCpus)
let CPUs = config.HostConfig?.CpusetCpus.split(",");
console.log(`[DEBUG] Retrieved container configuration: ${JSON.stringify(config)}`);
// Populate the modal fields with the current configurations
document.getElementById('container-name').value = config.Name.replace(/^\//, '');
document.getElementById('container-hostname').value = config.Config.Hostname.replace(/^\//, '');
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);
// Parse configuration and populate the modal fields
try {
const CPUs = config.HostConfig?.CpusetCpus?.split(',') || [];
// Show the modal
duplicateModal.show();
document.getElementById('container-name').value = config.Name.replace(/^\//, '');
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
duplicateContainerForm.addEventListener('submit', (e) => {
e.preventDefault();
duplicateModal.hide();
showStatusIndicator('Duplicating container...');
const name = document.getElementById('container-name').value.trim();
const hostname = document.getElementById('container-hostname').value.trim();
@ -488,23 +656,21 @@ duplicateContainerForm.addEventListener('submit', (e) => {
try {
config = JSON.parse(configJSON);
} catch (err) {
alert('Invalid JSON in configuration.');
hideStatusIndicator();
showAlert('danger', 'Invalid JSON in configuration.');
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 });
// Close the modal
duplicateModal.hide();
// Trigger container list update after a short delay
// Simulate delay for the demo
setTimeout(() => {
console.log('[INFO] Fetching updated container list after duplication');
hideStatusIndicator();
showAlert('success', 'Container duplicated successfully!');
// Refresh container list
sendCommand('listContainers');
}, 2000); // Wait for duplication to complete
}, 2000); // Simulated processing time
});

View File

@ -120,7 +120,6 @@
#terminal-modal .header {
background-color: #444;
cursor: move;
/* Change cursor to indicate drag is possible */
padding: 10px;
display: flex;
justify-content: space-between;
@ -165,6 +164,30 @@
border-radius: 5px;
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>
</head>
@ -222,7 +245,7 @@
<input type="text" class="form-control" id="container-name" required>
</div>
<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>
</div>
<div class="mb-3">
@ -230,7 +253,7 @@
<input type="text" class="form-control" id="container-image" required>
</div>
<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>
</div>
<div class="mb-3">
@ -241,7 +264,6 @@
<label for="container-memory" class="form-label">Memory (MB)</label>
<input type="number" class="form-control" id="container-memory" required>
</div>
<!-- Container Configuration as JSON -->
<div class="mb-3">
<label for="container-config" class="form-label">Container Configuration (JSON)</label>
<textarea class="form-control" id="container-config" rows="10" required></textarea>
@ -264,37 +286,50 @@
</div>
</div>
<div id="terminal-container"></div>
<div id="terminal-resize-handle"></div> <!-- Resize handle -->
<div id="terminal-resize-handle"></div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<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>
</div>
<div class="modal-body">
Are you sure you want to delete this container?
</div>
<div class="modal-footer">
<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>
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<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>
</div>
<div class="modal-body">
Are you sure you want to delete this container?
</div>
<div class="modal-footer">
<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>
</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 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 -->
<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>
<!-- Your App JS -->
<script type="module" src="app.js"></script>
</body>