more progress

This commit is contained in:
Raven Scott 2024-12-02 03:29:59 -05:00
parent 2839fd7a7d
commit 71992f004c
5 changed files with 442 additions and 280 deletions

49
app.js
View File

@ -408,23 +408,26 @@ function addConnection(topicHex) {
connectionItem.className = 'list-group-item d-flex align-items-center justify-content-between'; connectionItem.className = 'list-group-item d-flex align-items-center justify-content-between';
connectionItem.dataset.topicId = topicId; connectionItem.dataset.topicId = topicId;
connectionItem.innerHTML = ` connectionItem.innerHTML = `
<div class="connection-item d-flex align-items-center justify-content-between p-2"> <div class="connection-item row align-items-center px-2 py-1 border-bottom bg-dark text-light">
<div class="connection-info"> <!-- Connection Info -->
<span class="topic-id">${topicId}</span> <div class="col-8 connection-info text-truncate">
<span class="connection-status status-disconnected ms-2"></span> <span class="topic-id d-block text-primary fw-bold" title="${topicId}">${topicId}</span>
</div><BR> </div>
<div class="btn-group"> <!-- Action Buttons -->
<button class="btn btn-sm btn-primary docker-terminal-btn" title="Open Terminal"> <div class="col-4 d-flex justify-content-end">
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary docker-terminal-btn p-1" title="Open Terminal">
<i class="fas fa-terminal"></i> <i class="fas fa-terminal"></i>
</button> </button>
<button class="btn btn-sm btn-secondary deploy-template-btn" title="Deploy Template"> <button class="btn btn-outline-secondary deploy-template-btn p-1" title="Deploy Template">
<i class="fas fa-cubes"></i> <i class="fas fa-cubes"></i>
</button> </button>
<button class="btn btn-sm btn-danger disconnect-btn" title="Disconnect"> <button class="btn btn-outline-danger disconnect-btn p-1" title="Disconnect">
<i class="fas fa-plug"></i> <i class="fas fa-plug"></i>
</button> </button>
</div> </div>
</div> </div>
</div>
`; `;
// Add event listener for "Deploy Template" button // Add event listener for "Deploy Template" button
connectionItem.querySelector('.deploy-template-btn').addEventListener('click', () => { connectionItem.querySelector('.deploy-template-btn').addEventListener('click', () => {
@ -739,30 +742,32 @@ function renderContainers(containers, topicId) {
<td class="memory">0</td> <td class="memory">0</td>
<td class="ip-address">${ipAddress}</td> <td class="ip-address">${ipAddress}</td>
<td> <td>
<button class="btn btn-success btn-sm action-start" ${container.State === 'running' ? 'disabled' : ''}> <div class="btn-group btn-group-sm">
<button class="btn btn-outline-success action-start p-1" title="Start" ${container.State === 'running' ? 'disabled' : ''}>
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
</button> </button>
<button class="btn btn-info btn-sm action-restart" ${container.State !== 'running' ? 'disabled' : ''}> <button class="btn btn-outline-info action-restart p-1" title="Restart" ${container.State !== 'running' ? 'disabled' : ''}>
<i class="fas fa-redo"></i> <i class="fas fa-redo"></i>
</button> </button>
<button class="btn btn-warning btn-sm action-stop" ${container.State !== 'running' ? 'disabled' : ''}> <button class="btn btn-outline-warning action-stop p-1" title="Stop" ${container.State !== 'running' ? 'disabled' : ''}>
<i class="fas fa-stop"></i> <i class="fas fa-stop"></i>
</button> </button>
<button class="btn btn-dark btn-sm action-logs"> <button class="btn btn-outline-primary action-logs p-1" title="Logs">
<i class="fas fa-binoculars"></i> <i class="fas fa-list-alt"></i>
</button> </button>
<button class="btn btn-danger btn-sm action-remove"> <button class="btn btn-outline-primary action-terminal p-1" title="Terminal" ${container.State !== 'running' ? 'disabled' : ''}>
<i class="fas fa-trash"></i>
</button>
<button class="btn btn-primary btn-sm action-terminal" ${container.State !== 'running' ? 'disabled' : ''}>
<i class="fas fa-terminal"></i> <i class="fas fa-terminal"></i>
</button> </button>
<button class="btn btn-secondary btn-sm action-duplicate"> <button class="btn btn-outline-secondary action-duplicate p-1" title="Duplicate">
<i class="fas fa-clone"></i> <i class="fas fa-clone"></i>
</button> </button>
<button class="btn btn-outline-danger action-remove p-1" title="Remove">
<i class="fas fa-trash"></i>
</button>
</div>
</td> </td>
`; `;
containerList.appendChild(row); containerList.appendChild(row);
// Add event listener for duplicate button // Add event listener for duplicate button
const duplicateBtn = row.querySelector('.action-duplicate'); const duplicateBtn = row.querySelector('.action-duplicate');

View File

@ -376,6 +376,57 @@
/* Even lighter color when active */ /* Even lighter color when active */
} }
.list-group-item {
position: relative;
display: block;
padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);
color: var(--bs-list-group-color);
text-decoration: none;
background-color: #2c2c2c
;
}
.list-group-item {
position: relative;
display: block;
padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);
color: #ffffff;
text-decoration: none;
background-color: #2c2c2c;
}
.text-primary {
--bs-text-opacity: 1;
color: rgb(254 254 254) !important;
}
.list-group {
--bs-list-group-color: var(--bs-body-color);
--bs-list-group-bg: var(--bs-body-bg);
--bs-list-group-border-color: transparent;
--bs-list-group-border-width: var(--bs-border-width);
--bs-list-group-border-radius: var(--bs-border-radius);
--bs-list-group-item-padding-x: 1rem;
--bs-list-group-item-padding-y: 0.5rem;
--bs-list-group-action-color: var(--bs-secondary-color);
--bs-list-group-action-hover-color: var(--bs-emphasis-color);
--bs-list-group-action-hover-bg: var(--bs-tertiary-bg);
--bs-list-group-action-active-color: var(--bs-body-color);
--bs-list-group-action-active-bg: var(--bs-secondary-bg);
--bs-list-group-disabled-color: var(--bs-secondary-color);
--bs-list-group-disabled-bg: var(--bs-body-bg);
--bs-list-group-active-color: #fff;
--bs-list-group-active-bg: #0d6efd;
--bs-list-group-active-border-color: #0d6efd;
display: flex;
flex-direction: column;
padding-left: 0;
margin-bottom: 0;
border-radius: var(--bs-list-group-border-radius);
}
</style> </style>
</head> </head>
@ -390,14 +441,16 @@
<div class="content"> <div class="content">
<h4 class="text-center mt-3">Connections</h4> <h4 class="text-center mt-3">Connections</h4>
<ul id="connection-list" class="list-group mb-3"></ul> <ul id="connection-list" class="list-group mb-3"></ul>
<form id="add-connection-form" class="px-3"> <form id="add-connection-form" class="px-3 d-flex align-items-center">
<input type="text" id="new-connection-topic" class="form-control mb-2" placeholder="Enter server topic" <input type="text" id="new-connection-topic" class="form-control me-2" placeholder="Enter server topic" required>
required> <button type="submit" class="btn btn-primary">
<button type="submit" class="btn btn-primary w-100">Add Connection</button> <i class="fas fa-plug"></i> Add
</button>
</form> </form>
</div> </div>
</div> </div>
<div id="content"> <div id="content">
<div id="welcome-page"> <div id="welcome-page">
<h1>Welcome to Peartainer</h1> <h1>Welcome to Peartainer</h1>
@ -589,6 +642,10 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="deploy-form"> <form id="deploy-form">
<div class="form-group mb-3">
<label for="deploy-container-name">Container Name</label>
<input type="text" id="deploy-container-name" class="form-control" placeholder="Enter container name">
</div>
<div class="mb-3"> <div class="mb-3">
<label for="deploy-image" class="form-label">Image</label> <label for="deploy-image" class="form-label">Image</label>
<input type="text" id="deploy-image" class="form-control" required /> <input type="text" id="deploy-image" class="form-control" required />

View File

@ -3,6 +3,7 @@ const templateList = document.getElementById('template-list');
const templateSearchInput = document.getElementById('template-search-input'); const templateSearchInput = document.getElementById('template-search-input');
const templateDeployModal = new bootstrap.Modal(document.getElementById('templateDeployModalUnique')); const templateDeployModal = new bootstrap.Modal(document.getElementById('templateDeployModalUnique'));
const deployForm = document.getElementById('deploy-form'); const deployForm = document.getElementById('deploy-form');
let templates = [];
// Function to close all modals // Function to close all modals
function closeAllModals() { function closeAllModals() {
@ -61,12 +62,12 @@ function showAlert(type, message) {
// Fetch templates from the URL // Fetch templates from the URL
async function fetchTemplates() { async function fetchTemplates() {
try { try {
const response = await fetch('https://raw.githubusercontent.com/technorabilia/portainer-templates/main/lsio/templates/templates-2.0.json'); const response = await fetch('https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json');
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
const templates = data.templates || []; templates = data.templates || []; // Update global templates
displayTemplateList(templates); displayTemplateList(templates);
} catch (error) { } catch (error) {
console.error('[ERROR] Failed to fetch templates:', error.message); console.error('[ERROR] Failed to fetch templates:', error.message);
@ -74,6 +75,16 @@ async function fetchTemplates() {
} }
} }
// Filter templates by search input
templateSearchInput.addEventListener('input', () => {
const searchQuery = templateSearchInput.value.toLowerCase();
const filteredTemplates = templates.filter(template =>
template.title.toLowerCase().includes(searchQuery) ||
template.description.toLowerCase().includes(searchQuery)
);
displayTemplateList(filteredTemplates);
});
// Display templates in the list // Display templates in the list
function displayTemplateList(templates) { function displayTemplateList(templates) {
templateList.innerHTML = ''; templateList.innerHTML = '';
@ -106,20 +117,25 @@ templateSearchInput.addEventListener('input', () => {
function openDeployModal(template) { function openDeployModal(template) {
console.log('[DEBUG] Opening deploy modal for:', template); console.log('[DEBUG] Opening deploy modal for:', template);
// Set the modal title
const deployTitle = document.getElementById('deploy-title'); const deployTitle = document.getElementById('deploy-title');
deployTitle.textContent = `Deploy ${template.title}`; deployTitle.textContent = `Deploy ${template.title}`;
// Populate the image name
const deployImage = document.getElementById('deploy-image'); const deployImage = document.getElementById('deploy-image');
deployImage.value = template.image || ''; deployImage.value = template.image || '';
// Populate ports
const deployPorts = document.getElementById('deploy-ports'); const deployPorts = document.getElementById('deploy-ports');
deployPorts.value = (template.ports || []).join(', '); deployPorts.value = (template.ports || []).join(', ');
// Populate volumes
const deployVolumes = document.getElementById('deploy-volumes'); const deployVolumes = document.getElementById('deploy-volumes');
deployVolumes.value = (template.volumes || []) deployVolumes.value = (template.volumes || [])
.map(volume => `${volume.bind}:${volume.container}`) .map(volume => `${volume.bind}:${volume.container}`)
.join(', '); .join(', ');
// Add environment variables
const deployEnv = document.getElementById('deploy-env'); const deployEnv = document.getElementById('deploy-env');
deployEnv.innerHTML = ''; deployEnv.innerHTML = '';
(template.env || []).forEach(env => { (template.env || []).forEach(env => {
@ -132,12 +148,18 @@ function openDeployModal(template) {
deployEnv.appendChild(envRow); deployEnv.appendChild(envRow);
}); });
// Add Container Name field
const containerNameField = document.getElementById('deploy-container-name');
containerNameField.value = ''; // Clear previous value, if any
// Show the modal
templateDeployModal.show(); templateDeployModal.show();
} }
// Deploy Docker container
// Deploy Docker container // Deploy Docker container
async function deployDockerContainer(payload) { async function deployDockerContainer(payload) {
const { imageName, ports = [], volumes = [], envVars = [] } = payload; const { containerName, imageName, ports = [], volumes = [], envVars = [] } = payload;
const validPorts = ports.filter(port => { const validPorts = ports.filter(port => {
if (!port || !port.includes('/')) { if (!port || !port.includes('/')) {
@ -157,6 +179,7 @@ async function deployDockerContainer(payload) {
console.log('[INFO] Sending deployment command to the server...'); console.log('[INFO] Sending deployment command to the server...');
sendCommand('deployContainer', { sendCommand('deployContainer', {
containerName,
image: imageName, image: imageName,
ports: validPorts, ports: validPorts,
volumes: validVolumes, volumes: validVolumes,
@ -164,10 +187,27 @@ async function deployDockerContainer(payload) {
}); });
} }
// Example of how to dispatch the event when the server response is received
function handleServerResponse(serverResponse) {
console.log('[DEBUG] Dispatching server response:', serverResponse);
const responseEvent = new CustomEvent('responseReceived', { detail: serverResponse });
window.dispatchEvent(responseEvent);
}
// Integration for server response handling
// Ensure this function is called whenever a server response is received
async function processServerMessage(response) {
if (response.type === 'deployResult') {
handleServerResponse(response);
}
}
// Handle form submission for deployment
// Handle form submission for deployment // Handle form submission for deployment
deployForm.addEventListener('submit', async (e) => { deployForm.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const containerName = document.getElementById('deploy-container-name').value.trim();
const imageName = document.getElementById('deploy-image').value.trim(); const imageName = document.getElementById('deploy-image').value.trim();
const ports = document.getElementById('deploy-ports').value.split(',').map(port => port.trim()); const ports = document.getElementById('deploy-ports').value.split(',').map(port => port.trim());
const volumes = document.getElementById('deploy-volumes').value.split(',').map(volume => volume.trim()); const volumes = document.getElementById('deploy-volumes').value.split(',').map(volume => volume.trim());
@ -177,22 +217,72 @@ deployForm.addEventListener('submit', async (e) => {
value: input.value.trim(), value: input.value.trim(),
})); }));
const deployPayload = { imageName, ports, volumes, envVars }; const deployPayload = { containerName, imageName, ports, volumes, envVars };
console.log('[DEBUG] Deploy payload:', deployPayload); console.log('[DEBUG] Deploy payload:', deployPayload);
try { try {
showStatusIndicator('Deploying container...'); // showStatusIndicator('Deploying container...');
// Send the deployment request
await deployDockerContainer(deployPayload); await deployDockerContainer(deployPayload);
// Wait for a specific response
// Wait for the specific response
const successResponse = await waitForSpecificResponse("deployed successfully", 90000);
console.log('[INFO] Waiting for the deployment response...' + successResponse);
console.log('[INFO] Deployment success:', successResponse);
hideStatusIndicator(); hideStatusIndicator();
closeAllModals(); closeAllModals();
showAlert('success', `Container deployed successfully from image ${imageName}.`); showAlert('success', successResponse.message);
} catch (error) { } catch (error) {
console.error('[ERROR] Failed to deploy container:', error.message); console.error('[ERROR] Failed to deploy container:', error.message);
hideStatusIndicator(); hideStatusIndicator();
showAlert('danger', 'Failed to deploy container.'); showAlert('danger', error.message);
} }
}); });
// Utility function to wait for a specific response
function waitForSpecificResponse(expectedMessageFragment, timeout = 90000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function handleResponse(event) {
const response = event.detail; // Extract the response data
console.log('[DEBUG] Received response:', response);
if (response?.success && response.message.includes(expectedMessageFragment)) {
console.log('[DEBUG] Expected response received:', response.message);
window.removeEventListener('responseReceived', handleResponse); // Remove listener
resolve(response); // Resolve with the response
}
}
// Timeout handler
const timeoutId = setTimeout(() => {
console.warn('[WARN] Timeout while waiting for the expected response.');
window.removeEventListener('responseReceived', handleResponse); // Cleanup
reject(new Error('Timeout waiting for the expected response'));
}, timeout);
// Attach listener
window.addEventListener('responseReceived', handleResponse);
// Ensure cleanup on successful resolution
const wrappedResolve = (response) => {
clearTimeout(timeoutId);
resolve(response);
};
// Replace `resolve` in `handleResponse` for proper cleanup
handleResponse.wrappedResolve = wrappedResolve;
});
}
// Initialize templates on load // Initialize templates on load
document.addEventListener('DOMContentLoaded', fetchTemplates); document.addEventListener('DOMContentLoaded', fetchTemplates);

View File

@ -9,16 +9,12 @@
"height": "400", "height": "400",
"width": "950" "width": "950"
}, },
"links": [ "links": [
"http://127.0.0.1", "http://*",
"http://localhost", "https://*",
"https://ka-f.fontawesome.com", "ws://*",
"https://cdn.jsdelivr.net", "wss://*"
"https://cdnjs.cloudflare.com", ]
"ws://localhost:8080",
"https://raw.githubusercontent.com",
"https://portainer-io-assets.sfo2.digitaloceanspaces.com"
]
}, },
"type": "module", "type": "module",
"license": "Apache-2.0", "license": "Apache-2.0",

View File

@ -227,9 +227,19 @@ swarm.on('connection', (peer) => {
case 'deployContainer': case 'deployContainer':
console.log('[INFO] Handling "deployContainer" command'); console.log('[INFO] Handling "deployContainer" command');
const { image: imageToDeploy, ports = [], volumes = [], env = [] } = parsedData.args; const { containerName, image: imageToDeploy, ports = [], volumes = [], env = [] } = parsedData.args;
try { try {
// Validate and sanitize container name
if (!containerName || typeof containerName !== 'string') {
throw new Error('Invalid or missing container name.');
}
// Ensure the name is alphanumeric with optional dashes/underscores
if (!/^[a-zA-Z0-9-_]+$/.test(containerName)) {
throw new Error('Container name must be alphanumeric and may include dashes or underscores.');
}
// Validate and sanitize image // Validate and sanitize image
if (!imageToDeploy || typeof imageToDeploy !== 'string') { if (!imageToDeploy || typeof imageToDeploy !== 'string') {
throw new Error('Invalid or missing Docker image.'); throw new Error('Invalid or missing Docker image.');
@ -256,14 +266,16 @@ swarm.on('connection', (peer) => {
}); });
// Validate and sanitize environment variables // Validate and sanitize environment variables
const validEnv = env.map(({ name, value }) => { const validEnv = env
.map(({ name, value }) => {
if (name && value) { if (name && value) {
return `${name}=${value}`; return `${name}=${value}`;
} else { } else {
console.warn(`[WARN] Invalid environment variable skipped: name=${name}, value=${value}`); console.warn(`[WARN] Invalid environment variable skipped: name=${name}, value=${value}`);
return null; return null;
} }
}).filter(Boolean); })
.filter(Boolean);
console.log(`[INFO] Pulling Docker image "${imageToDeploy}"`); console.log(`[INFO] Pulling Docker image "${imageToDeploy}"`);
@ -286,9 +298,10 @@ swarm.on('connection', (peer) => {
hostConfig.PortBindings[`${containerPort}/${protocol}`] = [{ HostPort: containerPort }]; hostConfig.PortBindings[`${containerPort}/${protocol}`] = [{ HostPort: containerPort }];
}); });
// Create and start the container // Create and start the container with a custom name
console.log('[INFO] Creating the container...'); console.log('[INFO] Creating the container...');
const container = await docker.createContainer({ const container = await docker.createContainer({
name: containerName, // Include the container name
Image: imageToDeploy, Image: imageToDeploy,
Env: validEnv, Env: validEnv,
HostConfig: hostConfig, HostConfig: hostConfig,
@ -297,13 +310,13 @@ swarm.on('connection', (peer) => {
console.log('[INFO] Starting the container...'); console.log('[INFO] Starting the container...');
await container.start(); await container.start();
console.log(`[INFO] Container deployed successfully from image "${imageToDeploy}"`); console.log(`[INFO] Container "${containerName}" deployed successfully from image "${imageToDeploy}"`);
// Respond with success message // Respond with success message
peer.write( peer.write(
JSON.stringify({ JSON.stringify({
success: true, success: true,
message: `Container deployed successfully from image "${imageToDeploy}"`, message: `Container "${containerName}" deployed successfully from image "${imageToDeploy}"`,
}) })
); );
@ -325,6 +338,7 @@ swarm.on('connection', (peer) => {
break; break;
case 'startTerminal': case 'startTerminal':
console.log(`[INFO] Starting terminal for container: ${parsedData.args.containerId}`); console.log(`[INFO] Starting terminal for container: ${parsedData.args.containerId}`);
handleTerminal(parsedData.args.containerId, peer); handleTerminal(parsedData.args.containerId, peer);