peartainer/app.js
2024-11-26 08:14:43 -05:00

339 lines
12 KiB
JavaScript

import Hyperswarm from 'hyperswarm';
import b4a from 'b4a';
import { startTerminal, appendTerminalOutput } from './libs/terminal.js';
// DOM Elements
const containerList = document.getElementById('container-list');
const connectionList = document.getElementById('connection-list');
const addConnectionForm = document.getElementById('add-connection-form');
const newConnectionTopic = document.getElementById('new-connection-topic');
const connectionTitle = document.getElementById('connection-title');
const dashboard = document.getElementById('dashboard');
// Modal Elements
const duplicateModalElement = document.getElementById('duplicateModal');
const duplicateModal = new bootstrap.Modal(duplicateModalElement);
const duplicateContainerForm = document.getElementById('duplicate-container-form');
// Global variables
const connections = {};
let activePeer = null;
window.activePeer = null; // Expose to other modules
// Initialize the app
console.log('[INFO] Client app initialized');
// Collapse Sidebar Functionality
const collapseSidebarBtn = document.getElementById('collapse-sidebar-btn');
collapseSidebarBtn.addEventListener('click', () => {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('collapsed');
const btn = collapseSidebarBtn;
btn.innerHTML = sidebar.classList.contains('collapsed') ? '>' : '<';
});
// Add a new connection
addConnectionForm.addEventListener('submit', (e) => {
e.preventDefault();
const topicHex = newConnectionTopic.value.trim();
if (topicHex) {
addConnection(topicHex);
newConnectionTopic.value = '';
}
});
// Function to add a new connection
function addConnection(topicHex) {
const topic = b4a.from(topicHex, 'hex');
const topicId = topicHex.substring(0, 12);
console.log(`[INFO] Adding connection with topic: ${topicHex}`);
const connectionItem = document.createElement('li');
connectionItem.className = 'list-group-item d-flex align-items-center';
connectionItem.dataset.topicId = topicId;
connectionItem.innerHTML = `
<span class="connection-status status-disconnected"></span>${topicId}
`;
connectionItem.addEventListener('click', () => switchConnection(topicId));
connectionList.appendChild(connectionItem);
connections[topicId] = { topic, peer: null, swarm: null };
// Create a new swarm for this connection
const swarm = new Hyperswarm();
connections[topicId].swarm = swarm;
swarm.join(topic, { client: true, server: false });
swarm.on('connection', (peer) => {
console.log(`[INFO] Connected to peer for topic: ${topicHex}`);
// Prevent duplicate connections
if (connections[topicId].peer) {
console.warn(`[WARN] Duplicate connection detected for topic: ${topicId}. Closing.`);
peer.destroy();
return;
}
connections[topicId].peer = peer;
updateConnectionStatus(topicId, true);
peer.on('data', (data) => {
try {
const response = JSON.parse(data.toString());
console.log(`[DEBUG] Received data from server: ${JSON.stringify(response)}`);
if (response.type === 'containers') {
if (window.activePeer === peer) {
renderContainers(response.data);
}
} else if (response.type === 'terminalOutput') {
appendTerminalOutput(response.data, response.containerId, response.encoding);
} else if (response.type === 'containerConfig') {
if (window.inspectContainerCallback) {
window.inspectContainerCallback(response.data);
window.inspectContainerCallback = null; // Reset the callback
}
} else if (response.type === 'stats') {
updateContainerStats(response.data);
} else if (response.error) {
console.log(`Error: ${response.error}`);
}
} catch (err) {
console.error(`[ERROR] Failed to parse data from server: ${err.message}`);
}
});
peer.on('close', () => {
console.log(`[INFO] Disconnected from peer for topic: ${topicHex}`);
updateConnectionStatus(topicId, false);
connections[topicId].peer = null; // Clear the peer reference
if (window.activePeer === peer) {
window.activePeer = null;
connectionTitle.textContent = 'Disconnected';
dashboard.classList.add('hidden');
containerList.innerHTML = '';
}
});
// If this is the first connection, switch to it
if (!window.activePeer) {
switchConnection(topicId);
}
});
}
// Update connection status
function updateConnectionStatus(topicId, isConnected) {
const connectionItem = document.querySelector(`[data-topic-id="${topicId}"] .connection-status`);
if (connectionItem) {
connectionItem.className = `connection-status ${isConnected ? 'status-connected' : 'status-disconnected'}`;
}
}
// Switch between connections
function switchConnection(topicId) {
const connection = connections[topicId];
if (!connection) {
console.error(`[ERROR] No connection found for topicId: ${topicId}`);
return;
}
if (!connection.peer) {
console.error('[ERROR] No active peer for this connection.');
return;
}
window.activePeer = connection.peer;
connectionTitle.textContent = `Connection: ${topicId}`;
dashboard.classList.remove('hidden');
console.log('[INFO] Sending "listContainers" command');
sendCommand('listContainers');
}
// Attach switchConnection to the global window object
window.switchConnection = switchConnection;
// Send a command to the active peer
function sendCommand(command, args = {}) {
if (window.activePeer) {
const message = JSON.stringify({ command, args });
console.log(`[DEBUG] Sending command to server: ${message}`);
window.activePeer.write(message);
} else {
console.error('[ERROR] No active peer to send command.');
}
}
// Attach sendCommand to the global window object
window.sendCommand = sendCommand;
// Render the container list
function renderContainers(containers) {
console.log(`[INFO] Rendering ${containers.length} containers`);
containerList.innerHTML = ''; // Clear the current list
containers.forEach((container) => {
const name = container.Names[0].replace(/^\//, ''); // Remove leading slash from container names
const image = container.Image;
const containerId = container.Id;
const row = document.createElement('tr');
row.dataset.containerId = containerId; // Store container ID for reference
row.innerHTML = `
<td>${name}</td>
<td>${image}</td>
<td>${container.State}</td>
<td class="cpu">0</td>
<td class="memory">0</td>
<td class="ip-address">-</td>
<td>
<button class="btn btn-success btn-sm action-start" ${container.State === 'running' ? 'disabled' : ''}>Start</button>
<button class="btn btn-warning btn-sm action-stop" ${container.State !== 'running' ? 'disabled' : ''}>Stop</button>
<button class="btn btn-danger btn-sm action-remove">Remove</button>
<button class="btn btn-primary btn-sm action-terminal" ${container.State !== 'running' ? 'disabled' : ''}>Terminal</button>
<button class="btn btn-secondary btn-sm action-duplicate">Duplicate</button>
</td>
`;
containerList.appendChild(row);
// 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 listeners to action buttons
function addActionListeners(row, container) {
const startBtn = row.querySelector('.action-start');
const stopBtn = row.querySelector('.action-stop');
const removeBtn = row.querySelector('.action-remove');
const terminalBtn = row.querySelector('.action-terminal');
startBtn.addEventListener('click', () => {
sendCommand('startContainer', { id: container.Id });
});
stopBtn.addEventListener('click', () => {
sendCommand('stopContainer', { id: container.Id });
});
removeBtn.addEventListener('click', () => {
sendCommand('removeContainer', { id: container.Id });
});
terminalBtn.addEventListener('click', () => {
startTerminal(container.Id, container.Names[0] || container.Id);
});
}
// Function to update container statistics
function updateContainerStats(stats) {
console.log(`[DEBUG] Updating stats for container ID: ${stats.id}`);
let row = containerList.querySelector(`tr[data-container-id="${stats.id}"]`);
if (!row) {
console.warn(`[WARN] No row found for container ID: ${stats.id}. Adding a placeholder.`);
// Create a placeholder row if it doesn't exist
row = document.createElement('tr');
row.dataset.containerId = stats.id;
row.innerHTML = `
<td>Unknown</td>
<td>-</td>
<td>-</td>
<td class="cpu">0</td>
<td class="memory">0</td>
<td class="ip-address">-</td>
<td>-</td>
`;
containerList.appendChild(row);
}
row.querySelector('.cpu').textContent = stats.cpu.toFixed(2);
row.querySelector('.memory').textContent = (stats.memory / (1024 * 1024)).toFixed(2);
row.querySelector('.ip-address').textContent = stats.ip || '-';
}
// Function to open the Duplicate Modal with container configurations
function openDuplicateModal(container) {
console.log(`[INFO] Opening Duplicate Modal for container: ${container.Id}`);
// Send a command to get container configurations
sendCommand('inspectContainer', { id: container.Id });
// Listen for the response
window.inspectContainerCallback = (config) => {
if (!config) {
alert('Failed to retrieve container configuration.');
return;
}
// Populate the modal fields with the current configurations
document.getElementById('container-name').value = config.Name.replace(/^\//, '');
document.getElementById('container-image').value = config.Config.Image;
document.getElementById('container-config').value = JSON.stringify(config, null, 2);
// Show the modal
duplicateModal.show();
};
}
// Handle the Duplicate Container Form Submission
duplicateContainerForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = document.getElementById('container-name').value.trim();
const image = document.getElementById('container-image').value.trim();
const configJSON = document.getElementById('container-config').value.trim();
let config;
try {
config = JSON.parse(configJSON);
} catch (err) {
alert('Invalid JSON in configuration.');
return;
}
console.log(`[INFO] Sending duplicateContainer command for name: ${name}`);
// Send the duplicate command to the server
sendCommand('duplicateContainer', { name, config });
// Close the modal
duplicateModal.hide();
// Notify the user
alert('Duplicate command sent.');
// Trigger container list update after a short delay
setTimeout(() => {
console.log('[INFO] Fetching updated container list after duplication');
sendCommand('listContainers');
}, 2000); // Wait for duplication to complete
});
// Attach startTerminal to the global window object
window.startTerminal = startTerminal;
// Handle window unload to clean up swarms and peers
window.addEventListener('beforeunload', () => {
for (const topicId in connections) {
const connection = connections[topicId];
if (connection.peer) {
connection.peer.destroy();
}
if (connection.swarm) {
connection.swarm.destroy();
}
}
});