forked from snxraven/peardock
more progress
This commit is contained in:
parent
2839fd7a7d
commit
71992f004c
97
app.js
97
app.js
@ -350,7 +350,7 @@ function handlePeerData(data, topicId, peer) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'logs':
|
case 'logs':
|
||||||
console.log('[INFO] Handling logs output...');
|
console.log('[INFO] Handling logs output...');
|
||||||
if (window.handleLogOutput) {
|
if (window.handleLogOutput) {
|
||||||
window.handleLogOutput(response);
|
window.handleLogOutput(response);
|
||||||
@ -408,21 +408,24 @@ 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">
|
||||||
<i class="fas fa-terminal"></i>
|
<div class="btn-group btn-group-sm">
|
||||||
</button>
|
<button class="btn btn-outline-primary docker-terminal-btn p-1" title="Open Terminal">
|
||||||
<button class="btn btn-sm btn-secondary deploy-template-btn" title="Deploy Template">
|
<i class="fas fa-terminal"></i>
|
||||||
<i class="fas fa-cubes"></i>
|
</button>
|
||||||
</button>
|
<button class="btn btn-outline-secondary deploy-template-btn p-1" title="Deploy Template">
|
||||||
<button class="btn btn-sm btn-danger disconnect-btn" title="Disconnect">
|
<i class="fas fa-cubes"></i>
|
||||||
<i class="fas fa-plug"></i>
|
</button>
|
||||||
</button>
|
<button class="btn btn-outline-danger disconnect-btn p-1" title="Disconnect">
|
||||||
|
<i class="fas fa-plug"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -732,37 +735,39 @@ function renderContainers(containers, topicId) {
|
|||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.dataset.containerId = containerId; // Store container ID for reference
|
row.dataset.containerId = containerId; // Store container ID for reference
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${name}</td>
|
<td>${name}</td>
|
||||||
<td>${image}</td>
|
<td>${image}</td>
|
||||||
<td>${container.State || 'Unknown'}</td>
|
<td>${container.State || 'Unknown'}</td>
|
||||||
<td class="cpu">0</td>
|
<td class="cpu">0</td>
|
||||||
<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">
|
||||||
<i class="fas fa-play"></i>
|
<button class="btn btn-outline-success action-start p-1" title="Start" ${container.State === 'running' ? 'disabled' : ''}>
|
||||||
</button>
|
<i class="fas fa-play"></i>
|
||||||
<button class="btn btn-info btn-sm action-restart" ${container.State !== 'running' ? 'disabled' : ''}>
|
</button>
|
||||||
<i class="fas fa-redo"></i>
|
<button class="btn btn-outline-info action-restart p-1" title="Restart" ${container.State !== 'running' ? 'disabled' : ''}>
|
||||||
</button>
|
<i class="fas fa-redo"></i>
|
||||||
<button class="btn btn-warning btn-sm action-stop" ${container.State !== 'running' ? 'disabled' : ''}>
|
</button>
|
||||||
<i class="fas fa-stop"></i>
|
<button class="btn btn-outline-warning action-stop p-1" title="Stop" ${container.State !== 'running' ? 'disabled' : ''}>
|
||||||
</button>
|
<i class="fas fa-stop"></i>
|
||||||
<button class="btn btn-dark btn-sm action-logs">
|
</button>
|
||||||
<i class="fas fa-binoculars"></i>
|
<button class="btn btn-outline-primary action-logs p-1" title="Logs">
|
||||||
</button>
|
<i class="fas fa-list-alt"></i>
|
||||||
<button class="btn btn-danger btn-sm action-remove">
|
</button>
|
||||||
<i class="fas fa-trash"></i>
|
<button class="btn btn-outline-primary action-terminal p-1" title="Terminal" ${container.State !== 'running' ? 'disabled' : ''}>
|
||||||
</button>
|
<i class="fas fa-terminal"></i>
|
||||||
<button class="btn btn-primary btn-sm action-terminal" ${container.State !== 'running' ? 'disabled' : ''}>
|
</button>
|
||||||
<i class="fas fa-terminal"></i>
|
<button class="btn btn-outline-secondary action-duplicate p-1" title="Duplicate">
|
||||||
</button>
|
<i class="fas fa-clone"></i>
|
||||||
<button class="btn btn-secondary btn-sm action-duplicate">
|
</button>
|
||||||
<i class="fas fa-clone"></i>
|
<button class="btn btn-outline-danger action-remove p-1" title="Remove">
|
||||||
</button>
|
<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');
|
||||||
|
65
index.html
65
index.html
@ -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 />
|
||||||
|
@ -3,22 +3,23 @@ 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() {
|
||||||
const modals = document.querySelectorAll('.modal.show');
|
const modals = document.querySelectorAll('.modal.show');
|
||||||
modals.forEach(modal => {
|
modals.forEach(modal => {
|
||||||
const modalInstance = bootstrap.Modal.getInstance(modal);
|
const modalInstance = bootstrap.Modal.getInstance(modal);
|
||||||
if (modalInstance) modalInstance.hide();
|
if (modalInstance) modalInstance.hide();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show status indicator
|
// Show status indicator
|
||||||
function showStatusIndicator(message = 'Processing...') {
|
function showStatusIndicator(message = 'Processing...') {
|
||||||
const statusIndicator = document.createElement('div');
|
const statusIndicator = document.createElement('div');
|
||||||
statusIndicator.id = 'status-indicator';
|
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.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 = `
|
statusIndicator.innerHTML = `
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="spinner-border text-light" role="status">
|
<div class="spinner-border text-light" role="status">
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
@ -26,173 +27,262 @@ function showStatusIndicator(message = 'Processing...') {
|
|||||||
<p class="mt-3 text-light">${message}</p>
|
<p class="mt-3 text-light">${message}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(statusIndicator);
|
document.body.appendChild(statusIndicator);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide status indicator
|
// Hide status indicator
|
||||||
function hideStatusIndicator() {
|
function hideStatusIndicator() {
|
||||||
const statusIndicator = document.getElementById('status-indicator');
|
const statusIndicator = document.getElementById('status-indicator');
|
||||||
if (statusIndicator) {
|
if (statusIndicator) {
|
||||||
console.log('[DEBUG] Hiding status indicator');
|
console.log('[DEBUG] Hiding status indicator');
|
||||||
statusIndicator.remove();
|
statusIndicator.remove();
|
||||||
} else {
|
} else {
|
||||||
console.error('[ERROR] Status indicator element not found!');
|
console.error('[ERROR] Status indicator element not found!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show alert message
|
// Show alert message
|
||||||
function showAlert(type, message) {
|
function showAlert(type, message) {
|
||||||
const alertBox = document.createElement('div');
|
const alertBox = document.createElement('div');
|
||||||
alertBox.className = `alert alert-${type}`;
|
alertBox.className = `alert alert-${type}`;
|
||||||
alertBox.textContent = message;
|
alertBox.textContent = message;
|
||||||
|
|
||||||
const container = document.querySelector('#alert-container');
|
const container = document.querySelector('#alert-container');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.appendChild(alertBox);
|
container.appendChild(alertBox);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
container.removeChild(alertBox);
|
container.removeChild(alertBox);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
} else {
|
} else {
|
||||||
console.warn('[WARN] Alert container not found.');
|
console.warn('[WARN] Alert container not found.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
||||||
|
templates = data.templates || []; // Update global templates
|
||||||
|
displayTemplateList(templates);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ERROR] Failed to fetch templates:', error.message);
|
||||||
|
showAlert('danger', 'Failed to load templates.');
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
|
||||||
const templates = data.templates || [];
|
|
||||||
displayTemplateList(templates);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[ERROR] Failed to fetch templates:', error.message);
|
|
||||||
showAlert('danger', 'Failed to load templates.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 = '';
|
||||||
templates.forEach(template => {
|
templates.forEach(template => {
|
||||||
const listItem = document.createElement('li');
|
const listItem = document.createElement('li');
|
||||||
listItem.className = 'list-group-item d-flex justify-content-between align-items-center';
|
listItem.className = 'list-group-item d-flex justify-content-between align-items-center';
|
||||||
listItem.innerHTML = `
|
listItem.innerHTML = `
|
||||||
<div>
|
<div>
|
||||||
<img src="${template.logo || ''}" alt="Logo" class="me-2" style="width: 24px; height: 24px;">
|
<img src="${template.logo || ''}" alt="Logo" class="me-2" style="width: 24px; height: 24px;">
|
||||||
<span>${template.title}</span>
|
<span>${template.title}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary btn-sm deploy-btn">Deploy</button>
|
<button class="btn btn-primary btn-sm deploy-btn">Deploy</button>
|
||||||
`;
|
`;
|
||||||
listItem.querySelector('.deploy-btn').addEventListener('click', () => openDeployModal(template));
|
listItem.querySelector('.deploy-btn').addEventListener('click', () => openDeployModal(template));
|
||||||
templateList.appendChild(listItem);
|
templateList.appendChild(listItem);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter templates by search input
|
// Filter templates by search input
|
||||||
templateSearchInput.addEventListener('input', () => {
|
templateSearchInput.addEventListener('input', () => {
|
||||||
const searchQuery = templateSearchInput.value.toLowerCase();
|
const searchQuery = templateSearchInput.value.toLowerCase();
|
||||||
const filteredTemplates = templates.filter(template =>
|
const filteredTemplates = templates.filter(template =>
|
||||||
template.title.toLowerCase().includes(searchQuery) ||
|
template.title.toLowerCase().includes(searchQuery) ||
|
||||||
template.description.toLowerCase().includes(searchQuery)
|
template.description.toLowerCase().includes(searchQuery)
|
||||||
);
|
);
|
||||||
displayTemplateList(filteredTemplates);
|
displayTemplateList(filteredTemplates);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open deploy modal and populate the form dynamically
|
// Open deploy modal and populate the form dynamically
|
||||||
function openDeployModal(template) {
|
function openDeployModal(template) {
|
||||||
console.log('[DEBUG] Opening deploy modal for:', template);
|
console.log('[DEBUG] Opening deploy modal for:', template);
|
||||||
|
|
||||||
const deployTitle = document.getElementById('deploy-title');
|
// Set the modal title
|
||||||
deployTitle.textContent = `Deploy ${template.title}`;
|
const deployTitle = document.getElementById('deploy-title');
|
||||||
|
deployTitle.textContent = `Deploy ${template.title}`;
|
||||||
|
|
||||||
const deployImage = document.getElementById('deploy-image');
|
// Populate the image name
|
||||||
deployImage.value = template.image || '';
|
const deployImage = document.getElementById('deploy-image');
|
||||||
|
deployImage.value = template.image || '';
|
||||||
|
|
||||||
const deployPorts = document.getElementById('deploy-ports');
|
// Populate ports
|
||||||
deployPorts.value = (template.ports || []).join(', ');
|
const deployPorts = document.getElementById('deploy-ports');
|
||||||
|
deployPorts.value = (template.ports || []).join(', ');
|
||||||
|
|
||||||
const deployVolumes = document.getElementById('deploy-volumes');
|
// Populate volumes
|
||||||
deployVolumes.value = (template.volumes || [])
|
const deployVolumes = document.getElementById('deploy-volumes');
|
||||||
.map(volume => `${volume.bind}:${volume.container}`)
|
deployVolumes.value = (template.volumes || [])
|
||||||
.join(', ');
|
.map(volume => `${volume.bind}:${volume.container}`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
const deployEnv = document.getElementById('deploy-env');
|
// Add environment variables
|
||||||
deployEnv.innerHTML = '';
|
const deployEnv = document.getElementById('deploy-env');
|
||||||
(template.env || []).forEach(env => {
|
deployEnv.innerHTML = '';
|
||||||
const envRow = document.createElement('div');
|
(template.env || []).forEach(env => {
|
||||||
envRow.className = 'mb-3';
|
const envRow = document.createElement('div');
|
||||||
envRow.innerHTML = `
|
envRow.className = 'mb-3';
|
||||||
<label>${env.label || env.name}</label>
|
envRow.innerHTML = `
|
||||||
<input type="text" class="form-control" data-env-name="${env.name}" value="${env.default || ''}">
|
<label>${env.label || env.name}</label>
|
||||||
`;
|
<input type="text" class="form-control" data-env-name="${env.name}" value="${env.default || ''}">
|
||||||
deployEnv.appendChild(envRow);
|
`;
|
||||||
});
|
deployEnv.appendChild(envRow);
|
||||||
|
});
|
||||||
|
|
||||||
templateDeployModal.show();
|
// Add Container Name field
|
||||||
|
const containerNameField = document.getElementById('deploy-container-name');
|
||||||
|
containerNameField.value = ''; // Clear previous value, if any
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
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('/')) {
|
||||||
console.warn(`[WARN] Invalid port entry skipped: ${port}`);
|
console.warn(`[WARN] Invalid port entry skipped: ${port}`);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const validVolumes = volumes.filter(volume => {
|
||||||
|
if (!volume || !volume.includes(':')) {
|
||||||
|
console.warn(`[WARN] Invalid volume entry skipped: ${volume}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[INFO] Sending deployment command to the server...');
|
||||||
|
sendCommand('deployContainer', {
|
||||||
|
containerName,
|
||||||
|
image: imageName,
|
||||||
|
ports: validPorts,
|
||||||
|
volumes: validVolumes,
|
||||||
|
env: envVars.map(({ name, value }) => ({ name, value })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const validVolumes = volumes.filter(volume => {
|
|
||||||
if (!volume || !volume.includes(':')) {
|
|
||||||
console.warn(`[WARN] Invalid volume entry skipped: ${volume}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[INFO] Sending deployment command to the server...');
|
|
||||||
sendCommand('deployContainer', {
|
|
||||||
image: imageName,
|
|
||||||
ports: validPorts,
|
|
||||||
volumes: validVolumes,
|
|
||||||
env: envVars.map(({ name, value }) => ({ name, value })),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 imageName = document.getElementById('deploy-image').value.trim();
|
const containerName = document.getElementById('deploy-container-name').value.trim();
|
||||||
const ports = document.getElementById('deploy-ports').value.split(',').map(port => port.trim());
|
const imageName = document.getElementById('deploy-image').value.trim();
|
||||||
const volumes = document.getElementById('deploy-volumes').value.split(',').map(volume => volume.trim());
|
const ports = document.getElementById('deploy-ports').value.split(',').map(port => port.trim());
|
||||||
const envInputs = document.querySelectorAll('#deploy-env input');
|
const volumes = document.getElementById('deploy-volumes').value.split(',').map(volume => volume.trim());
|
||||||
const envVars = Array.from(envInputs).map(input => ({
|
const envInputs = document.querySelectorAll('#deploy-env input');
|
||||||
name: input.getAttribute('data-env-name'),
|
const envVars = Array.from(envInputs).map(input => ({
|
||||||
value: input.value.trim(),
|
name: input.getAttribute('data-env-name'),
|
||||||
}));
|
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 {
|
|
||||||
showStatusIndicator('Deploying container...');
|
try {
|
||||||
await deployDockerContainer(deployPayload);
|
// showStatusIndicator('Deploying container...');
|
||||||
hideStatusIndicator();
|
|
||||||
closeAllModals();
|
// Send the deployment request
|
||||||
showAlert('success', `Container deployed successfully from image ${imageName}.`);
|
await deployDockerContainer(deployPayload);
|
||||||
} catch (error) {
|
|
||||||
console.error('[ERROR] Failed to deploy container:', error.message);
|
// Wait for a specific response
|
||||||
hideStatusIndicator();
|
// Wait for the specific response
|
||||||
showAlert('danger', 'Failed to deploy container.');
|
const successResponse = await waitForSpecificResponse("deployed successfully", 90000);
|
||||||
}
|
|
||||||
|
console.log('[INFO] Waiting for the deployment response...' + successResponse);
|
||||||
|
|
||||||
|
console.log('[INFO] Deployment success:', successResponse);
|
||||||
|
hideStatusIndicator();
|
||||||
|
closeAllModals();
|
||||||
|
showAlert('success', successResponse.message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ERROR] Failed to deploy container:', error.message);
|
||||||
|
hideStatusIndicator();
|
||||||
|
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);
|
||||||
|
|
||||||
|
16
package.json
16
package.json
@ -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",
|
||||||
|
204
server/server.js
204
server/server.js
@ -225,104 +225,118 @@ swarm.on('connection', (peer) => {
|
|||||||
response = { success: true, message: `Container ${parsedData.args.id} removed` };
|
response = { success: true, message: `Container ${parsedData.args.id} removed` };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
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 image
|
// Validate and sanitize container name
|
||||||
if (!imageToDeploy || typeof imageToDeploy !== 'string') {
|
if (!containerName || typeof containerName !== 'string') {
|
||||||
throw new Error('Invalid or missing Docker image.');
|
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
|
||||||
|
if (!imageToDeploy || typeof imageToDeploy !== 'string') {
|
||||||
|
throw new Error('Invalid or missing Docker image.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and sanitize ports
|
||||||
|
const validPorts = ports.filter((port) => {
|
||||||
|
if (typeof port === 'string' && /^\d+\/(tcp|udp)$/.test(port)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.warn(`[WARN] Invalid port entry skipped: ${port}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate and sanitize volumes
|
||||||
|
const validVolumes = volumes.filter((volume) => {
|
||||||
|
if (typeof volume === 'string' && volume.includes(':')) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.warn(`[WARN] Invalid volume entry skipped: ${volume}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate and sanitize environment variables
|
||||||
|
const validEnv = env
|
||||||
|
.map(({ name, value }) => {
|
||||||
|
if (name && value) {
|
||||||
|
return `${name}=${value}`;
|
||||||
|
} else {
|
||||||
|
console.warn(`[WARN] Invalid environment variable skipped: name=${name}, value=${value}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
console.log(`[INFO] Pulling Docker image "${imageToDeploy}"`);
|
||||||
|
|
||||||
|
// Pull the Docker image
|
||||||
|
const pullStream = await docker.pull(imageToDeploy);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
docker.modem.followProgress(pullStream, (err) => (err ? reject(err) : resolve()));
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[INFO] Image "${imageToDeploy}" pulled successfully`);
|
||||||
|
|
||||||
|
// Configure container creation settings
|
||||||
|
const hostConfig = {
|
||||||
|
PortBindings: {},
|
||||||
|
Binds: validVolumes, // Use validated volumes in Docker's expected format
|
||||||
|
NetworkMode: 'bridge', // Set the network mode to bridge
|
||||||
|
};
|
||||||
|
validPorts.forEach((port) => {
|
||||||
|
const [containerPort, protocol] = port.split('/');
|
||||||
|
hostConfig.PortBindings[`${containerPort}/${protocol}`] = [{ HostPort: containerPort }];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create and start the container with a custom name
|
||||||
|
console.log('[INFO] Creating the container...');
|
||||||
|
const container = await docker.createContainer({
|
||||||
|
name: containerName, // Include the container name
|
||||||
|
Image: imageToDeploy,
|
||||||
|
Env: validEnv,
|
||||||
|
HostConfig: hostConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[INFO] Starting the container...');
|
||||||
|
await container.start();
|
||||||
|
|
||||||
|
console.log(`[INFO] Container "${containerName}" deployed successfully from image "${imageToDeploy}"`);
|
||||||
|
|
||||||
|
// Respond with success message
|
||||||
|
peer.write(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: `Container "${containerName}" deployed successfully from image "${imageToDeploy}"`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update all peers with the latest container list
|
||||||
|
const containers = await docker.listContainers({ all: true });
|
||||||
|
const update = { type: 'containers', data: containers };
|
||||||
|
|
||||||
|
for (const connectedPeer of connectedPeers) {
|
||||||
|
connectedPeer.write(JSON.stringify(update));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[ERROR] Failed to deploy container: ${err.message}`);
|
||||||
|
peer.write(
|
||||||
|
JSON.stringify({
|
||||||
|
error: `Failed to deploy container: ${err.message}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
// Validate and sanitize ports
|
|
||||||
const validPorts = ports.filter((port) => {
|
|
||||||
if (typeof port === 'string' && /^\d+\/(tcp|udp)$/.test(port)) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
console.warn(`[WARN] Invalid port entry skipped: ${port}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate and sanitize volumes
|
|
||||||
const validVolumes = volumes.filter((volume) => {
|
|
||||||
if (typeof volume === 'string' && volume.includes(':')) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
console.warn(`[WARN] Invalid volume entry skipped: ${volume}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate and sanitize environment variables
|
|
||||||
const validEnv = env.map(({ name, value }) => {
|
|
||||||
if (name && value) {
|
|
||||||
return `${name}=${value}`;
|
|
||||||
} else {
|
|
||||||
console.warn(`[WARN] Invalid environment variable skipped: name=${name}, value=${value}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).filter(Boolean);
|
|
||||||
|
|
||||||
console.log(`[INFO] Pulling Docker image "${imageToDeploy}"`);
|
|
||||||
|
|
||||||
// Pull the Docker image
|
|
||||||
const pullStream = await docker.pull(imageToDeploy);
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
docker.modem.followProgress(pullStream, (err) => (err ? reject(err) : resolve()));
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`[INFO] Image "${imageToDeploy}" pulled successfully`);
|
|
||||||
|
|
||||||
// Configure container creation settings
|
|
||||||
const hostConfig = {
|
|
||||||
PortBindings: {},
|
|
||||||
Binds: validVolumes, // Use validated volumes in Docker's expected format
|
|
||||||
NetworkMode: 'bridge', // Set the network mode to bridge
|
|
||||||
};
|
|
||||||
validPorts.forEach((port) => {
|
|
||||||
const [containerPort, protocol] = port.split('/');
|
|
||||||
hostConfig.PortBindings[`${containerPort}/${protocol}`] = [{ HostPort: containerPort }];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create and start the container
|
|
||||||
console.log('[INFO] Creating the container...');
|
|
||||||
const container = await docker.createContainer({
|
|
||||||
Image: imageToDeploy,
|
|
||||||
Env: validEnv,
|
|
||||||
HostConfig: hostConfig,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[INFO] Starting the container...');
|
|
||||||
await container.start();
|
|
||||||
|
|
||||||
console.log(`[INFO] Container deployed successfully from image "${imageToDeploy}"`);
|
|
||||||
|
|
||||||
// Respond with success message
|
|
||||||
peer.write(
|
|
||||||
JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
message: `Container deployed successfully from image "${imageToDeploy}"`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update all peers with the latest container list
|
|
||||||
const containers = await docker.listContainers({ all: true });
|
|
||||||
const update = { type: 'containers', data: containers };
|
|
||||||
|
|
||||||
for (const connectedPeer of connectedPeers) {
|
|
||||||
connectedPeer.write(JSON.stringify(update));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`[ERROR] Failed to deploy container: ${err.message}`);
|
|
||||||
peer.write(
|
|
||||||
JSON.stringify({
|
|
||||||
error: `Failed to deploy container: ${err.message}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
|
|
||||||
case 'startTerminal':
|
case 'startTerminal':
|
||||||
|
Loading…
Reference in New Issue
Block a user