fixes
This commit is contained in:
@@ -4,6 +4,7 @@ import { startTerminal, appendTerminalOutput } from './libs/terminal.js';
|
|||||||
import { startDockerTerminal, cleanUpDockerTerminal } from './libs/dockerTerminal.js';
|
import { startDockerTerminal, cleanUpDockerTerminal } from './libs/dockerTerminal.js';
|
||||||
import { fetchTemplates, displayTemplateList, openDeployModal } from './libs/templateDeploy.js';
|
import { fetchTemplates, displayTemplateList, openDeployModal } from './libs/templateDeploy.js';
|
||||||
import { showContainerSkeleton, createProgressBar, updateProgressBar, removeProgressBar, showOperationStatus } from './libs/loadingStates.js';
|
import { showContainerSkeleton, createProgressBar, updateProgressBar, removeProgressBar, showOperationStatus } from './libs/loadingStates.js';
|
||||||
|
import { closeAllModals, showStatusIndicator, hideStatusIndicator, showAlert } from './libs/uiUtils.js';
|
||||||
|
|
||||||
// DOM Elements - Cache frequently accessed elements
|
// DOM Elements - Cache frequently accessed elements
|
||||||
const containerList = document.getElementById('container-list');
|
const containerList = document.getElementById('container-list');
|
||||||
@@ -39,14 +40,7 @@ function stopStatsInterval() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAllModals() {
|
// Utility functions are now imported from uiUtils.js
|
||||||
// Find and hide all open modals
|
|
||||||
const modals = document.querySelectorAll('.modal.show'); // Adjust selector if necessary
|
|
||||||
modals.forEach(modal => {
|
|
||||||
const modalInstance = bootstrap.Modal.getInstance(modal); // Get Bootstrap modal instance
|
|
||||||
modalInstance.hide(); // Close the modal
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -346,52 +340,7 @@ function initContainerFiltering() {
|
|||||||
// Initialize the app
|
// Initialize the app
|
||||||
console.log('[INFO] Client app initialized');
|
console.log('[INFO] Client app initialized');
|
||||||
|
|
||||||
// Show Status Indicator
|
// Utility functions are now imported from uiUtils.js
|
||||||
// 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
|
|
||||||
// Show alert message
|
|
||||||
function showAlert(type, message) {
|
|
||||||
const alertBox = document.createElement('div');
|
|
||||||
alertBox.className = `alert alert-${type}`;
|
|
||||||
alertBox.textContent = message;
|
|
||||||
|
|
||||||
// Use cached DOM element
|
|
||||||
if (alertContainer) {
|
|
||||||
alertContainer.appendChild(alertBox);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (alertContainer.contains(alertBox)) {
|
|
||||||
alertContainer.removeChild(alertBox);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
} else {
|
|
||||||
console.warn('[WARN] Alert container not found.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -999,6 +948,67 @@ function debounce(func, wait) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Container filtering and sorting state
|
||||||
|
let containerFilterState = {
|
||||||
|
search: '',
|
||||||
|
status: 'all',
|
||||||
|
sort: 'name-asc',
|
||||||
|
allContainers: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter and sort containers
|
||||||
|
function filterAndSortContainers(containers) {
|
||||||
|
let filtered = [...containers];
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
if (containerFilterState.search) {
|
||||||
|
const searchLower = containerFilterState.search.toLowerCase();
|
||||||
|
filtered = filtered.filter(container => {
|
||||||
|
const name = container.Names[0]?.replace(/^\//, '') || '';
|
||||||
|
const image = container.Image || '';
|
||||||
|
return name.toLowerCase().includes(searchLower) ||
|
||||||
|
image.toLowerCase().includes(searchLower);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply status filter
|
||||||
|
if (containerFilterState.status !== 'all') {
|
||||||
|
filtered = filtered.filter(container => {
|
||||||
|
const state = container.State?.toLowerCase() || '';
|
||||||
|
return state === containerFilterState.status.toLowerCase();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
const [sortField, sortOrder] = containerFilterState.sort.split('-');
|
||||||
|
filtered.sort((a, b) => {
|
||||||
|
let aVal, bVal;
|
||||||
|
|
||||||
|
switch (sortField) {
|
||||||
|
case 'name':
|
||||||
|
aVal = (a.Names[0]?.replace(/^\//, '') || '').toLowerCase();
|
||||||
|
bVal = (b.Names[0]?.replace(/^\//, '') || '').toLowerCase();
|
||||||
|
break;
|
||||||
|
case 'cpu':
|
||||||
|
aVal = smoothedStats[a.Id]?.cpu || 0;
|
||||||
|
bVal = smoothedStats[b.Id]?.cpu || 0;
|
||||||
|
break;
|
||||||
|
case 'memory':
|
||||||
|
aVal = smoothedStats[a.Id]?.memory || 0;
|
||||||
|
bVal = smoothedStats[b.Id]?.memory || 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1;
|
||||||
|
if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
// Render the container list with optimized DOM manipulation
|
// Render the container list with optimized DOM manipulation
|
||||||
function renderContainers(containers, topicId) {
|
function renderContainers(containers, topicId) {
|
||||||
if (!window.activePeer || !connections[topicId] || window.activePeer !== connections[topicId].peer) {
|
if (!window.activePeer || !connections[topicId] || window.activePeer !== connections[topicId].peer) {
|
||||||
@@ -1041,8 +1051,22 @@ function renderContainers(containers, topicId) {
|
|||||||
<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">
|
||||||
<td class="memory">0</td>
|
<div class="stats-container">
|
||||||
|
<span class="stats-value">0.00%</span>
|
||||||
|
<div class="stats-bar-container">
|
||||||
|
<div class="stats-bar cpu-bar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="memory">
|
||||||
|
<div class="stats-container">
|
||||||
|
<span class="stats-value">0.00 MB</span>
|
||||||
|
<div class="stats-bar-container">
|
||||||
|
<div class="stats-bar memory-bar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td class="ip-address">${ipAddress}</td>
|
<td class="ip-address">${ipAddress}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ const BLOCKED_PATTERNS = [
|
|||||||
/&&/, // Command chaining
|
/&&/, // Command chaining
|
||||||
/\|\|/, // OR operator
|
/\|\|/, // OR operator
|
||||||
/;/, // Command separator
|
/;/, // Command separator
|
||||||
/>/.*</, // Redirection
|
/>.*</, // Redirection (e.g., >file or <file)
|
||||||
/2>&1/, // Stderr redirection
|
/2>&1/, // Stderr redirection
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -158,3 +158,5 @@ function getStatusColor(status) {
|
|||||||
return colors[status] || 'secondary';
|
return colors[status] || 'secondary';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+178
-150
@@ -1,13 +1,13 @@
|
|||||||
|
// Import dependencies first (ES6 imports must be at top)
|
||||||
|
import { showOperationStatus } from './loadingStates.js';
|
||||||
|
import { closeAllModals, showStatusIndicator, hideStatusIndicator, showAlert } from './uiUtils.js';
|
||||||
|
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
const templateList = document.getElementById('template-list');
|
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 = [];
|
let templates = [];
|
||||||
let localTemplates = [];
|
|
||||||
|
|
||||||
// Import template storage
|
|
||||||
import * as templateStorage from './templateStorage.js';
|
|
||||||
|
|
||||||
// Array item counters for unique IDs
|
// Array item counters for unique IDs
|
||||||
let portCounter = 0;
|
let portCounter = 0;
|
||||||
@@ -24,96 +24,27 @@ let sysctlCounter = 0;
|
|||||||
let ulimitCounter = 0;
|
let ulimitCounter = 0;
|
||||||
let tmpfsCounter = 0;
|
let tmpfsCounter = 0;
|
||||||
|
|
||||||
function closeAllModals() {
|
// Utility functions are now imported from uiUtils.js
|
||||||
const modals = document.querySelectorAll('.modal.show');
|
// Also explicitly close the deploy modal if needed
|
||||||
modals.forEach(modal => {
|
function closeDeployModal() {
|
||||||
const modalInstance = bootstrap.Modal.getInstance(modal);
|
|
||||||
if (modalInstance) {
|
|
||||||
modalInstance.hide();
|
|
||||||
} else {
|
|
||||||
// If no instance exists, create one and hide it
|
|
||||||
const newInstance = new bootstrap.Modal(modal);
|
|
||||||
newInstance.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also explicitly close the deploy modal if it exists
|
|
||||||
if (templateDeployModal) {
|
if (templateDeployModal) {
|
||||||
templateDeployModal.hide();
|
templateDeployModal.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show status indicator
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide status indicator
|
|
||||||
function hideStatusIndicator() {
|
|
||||||
const statusIndicator = document.getElementById('status-indicator');
|
|
||||||
if (statusIndicator) {
|
|
||||||
statusIndicator.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show alert message
|
|
||||||
function showAlert(type, message) {
|
|
||||||
const alertBox = document.createElement('div');
|
|
||||||
alertBox.className = `alert alert-${type}`;
|
|
||||||
alertBox.textContent = message;
|
|
||||||
|
|
||||||
const container = document.querySelector('#alert-container');
|
|
||||||
if (container) {
|
|
||||||
container.appendChild(alertBox);
|
|
||||||
setTimeout(() => {
|
|
||||||
if (container.contains(alertBox)) {
|
|
||||||
container.removeChild(alertBox);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch templates from the URL
|
// Fetch templates from the URL
|
||||||
async function fetchTemplates() {
|
async function fetchTemplates() {
|
||||||
try {
|
try {
|
||||||
// Load local templates
|
|
||||||
localTemplates = Object.entries(templateStorage.loadTemplates()).map(([name, config]) => ({
|
|
||||||
title: name,
|
|
||||||
description: `Local template saved on ${new Date(config.savedAt).toLocaleDateString()}`,
|
|
||||||
logo: '',
|
|
||||||
image: config.image || '',
|
|
||||||
ports: config.ports || [],
|
|
||||||
volumes: config.volumes || [],
|
|
||||||
env: config.env || [],
|
|
||||||
...config
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Fetch remote templates
|
|
||||||
const response = await fetch('https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.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();
|
||||||
templates = data.templates || [];
|
templates = data.templates || []; // Update global templates
|
||||||
|
displayTemplateList(templates);
|
||||||
// Combine local and remote templates
|
|
||||||
displayTemplateList([...localTemplates, ...templates]);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ERROR] Failed to fetch templates:', error.message);
|
console.error('[ERROR] Failed to fetch templates:', error.message);
|
||||||
// Still show local templates even if remote fetch fails
|
showAlert('danger', 'Failed to load templates.');
|
||||||
displayTemplateList(localTemplates);
|
|
||||||
showAlert('warning', 'Failed to load remote templates. Showing local templates only.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,52 +61,19 @@ templateSearchInput.addEventListener('input', () => {
|
|||||||
// Display templates in the list
|
// Display templates in the list
|
||||||
function displayTemplateList(templates) {
|
function displayTemplateList(templates) {
|
||||||
templateList.innerHTML = '';
|
templateList.innerHTML = '';
|
||||||
|
templates.forEach(template => {
|
||||||
// Add section header for local templates
|
|
||||||
if (localTemplates.length > 0) {
|
|
||||||
const localHeader = document.createElement('li');
|
|
||||||
localHeader.className = 'list-group-item bg-secondary';
|
|
||||||
localHeader.innerHTML = '<strong>Local Templates</strong>';
|
|
||||||
templateList.appendChild(localHeader);
|
|
||||||
}
|
|
||||||
|
|
||||||
templates.forEach((template, index) => {
|
|
||||||
const isLocal = index < localTemplates.length;
|
|
||||||
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>
|
||||||
${template.logo ? `<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>
|
||||||
${isLocal ? '<span class="badge bg-info ms-2">Local</span>' : ''}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
${isLocal ? `<button class="btn btn-danger btn-sm me-2 delete-template-btn" data-template="${template.title}">Delete</button>` : ''}
|
|
||||||
<button class="btn btn-primary btn-sm deploy-btn">Deploy</button>
|
<button class="btn btn-primary btn-sm deploy-btn">Deploy</button>
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
listItem.querySelector('.deploy-btn').addEventListener('click', () => openDeployModal(template));
|
listItem.querySelector('.deploy-btn').addEventListener('click', () => openDeployModal(template));
|
||||||
|
|
||||||
if (isLocal) {
|
|
||||||
listItem.querySelector('.delete-template-btn').addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (confirm(`Delete template "${template.title}"?`)) {
|
|
||||||
templateStorage.deleteTemplate(template.title);
|
|
||||||
fetchTemplates(); // Refresh list
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
templateList.appendChild(listItem);
|
templateList.appendChild(listItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add section header for remote templates
|
|
||||||
if (localTemplates.length > 0 && templates.length > localTemplates.length) {
|
|
||||||
const remoteHeader = document.createElement('li');
|
|
||||||
remoteHeader.className = 'list-group-item bg-secondary mt-2';
|
|
||||||
remoteHeader.innerHTML = '<strong>Remote Templates</strong>';
|
|
||||||
templateList.appendChild(remoteHeader);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Array management functions
|
// Array management functions
|
||||||
@@ -734,19 +632,80 @@ async function deployDockerContainer(payload) {
|
|||||||
console.log('[INFO] Sending deployment command to the server...');
|
console.log('[INFO] Sending deployment command to the server...');
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
window.handlePeerResponse = (response) => {
|
// Store the original handler to restore it later
|
||||||
if (response.success && response.message.includes('deployed successfully')) {
|
const originalHandler = window.handlePeerResponse;
|
||||||
|
let timeoutId = null;
|
||||||
|
let isResolved = false;
|
||||||
|
|
||||||
|
// Create a deployment-specific handler
|
||||||
|
const deploymentHandler = (response) => {
|
||||||
|
// Only process responses related to deployment
|
||||||
|
if (isResolved) {
|
||||||
|
// If already resolved, pass to original handler if it exists
|
||||||
|
if (typeof originalHandler === 'function') {
|
||||||
|
originalHandler(response);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a deployment response
|
||||||
|
const isDeploymentResponse =
|
||||||
|
(response.success && response.message && response.message.includes('deployed successfully')) ||
|
||||||
|
(response.error && (response.message || response.error).includes('deploy') || response.error.includes('Container'));
|
||||||
|
|
||||||
|
if (isDeploymentResponse) {
|
||||||
|
// Clear timeout
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore original handler
|
||||||
|
window.handlePeerResponse = originalHandler;
|
||||||
|
isResolved = true;
|
||||||
|
|
||||||
|
if (response.success && response.message && response.message.includes('deployed successfully')) {
|
||||||
console.log('[INFO] Deployment response received:', response.message);
|
console.log('[INFO] Deployment response received:', response.message);
|
||||||
resolve(response);
|
resolve(response);
|
||||||
} else if (response.error) {
|
} else if (response.error) {
|
||||||
reject(new Error(response.error));
|
// Safely extract error message
|
||||||
|
const errorMessage = typeof response.error === 'string'
|
||||||
|
? response.error
|
||||||
|
: (response.error?.message || response.error?.toString() || 'Unknown deployment error');
|
||||||
|
reject(new Error(errorMessage));
|
||||||
|
} else {
|
||||||
|
// Unexpected response format
|
||||||
|
reject(new Error('Unexpected response format from server'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not a deployment response, pass to original handler if it exists
|
||||||
|
if (typeof originalHandler === 'function') {
|
||||||
|
originalHandler(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sendCommand('deployContainer', payload);
|
// Set the deployment handler
|
||||||
|
window.handlePeerResponse = deploymentHandler;
|
||||||
|
|
||||||
setTimeout(() => {
|
// Use window.sendCommand to avoid TDZ issues
|
||||||
|
if (typeof window.sendCommand === 'function') {
|
||||||
|
window.sendCommand('deployContainer', payload);
|
||||||
|
} else {
|
||||||
|
// Restore original handler before rejecting
|
||||||
|
window.handlePeerResponse = originalHandler;
|
||||||
|
reject(new Error('sendCommand is not available. Please ensure app.js is loaded.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set timeout with cleanup
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
if (!isResolved) {
|
||||||
|
// Restore original handler
|
||||||
|
window.handlePeerResponse = originalHandler;
|
||||||
|
isResolved = true;
|
||||||
reject(new Error('Deployment timed out. No response from server.'));
|
reject(new Error('Deployment timed out. No response from server.'));
|
||||||
|
}
|
||||||
}, 60000); // Increased timeout for complex deployments
|
}, 60000); // Increased timeout for complex deployments
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -755,50 +714,119 @@ async function deployDockerContainer(payload) {
|
|||||||
deployForm.addEventListener('submit', async (e) => {
|
deployForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const formData = collectFormData();
|
let formData;
|
||||||
|
try {
|
||||||
|
formData = collectFormData();
|
||||||
|
} catch (collectError) {
|
||||||
|
console.error('[ERROR] Failed to collect form data:', collectError);
|
||||||
|
console.error('[ERROR] Collect error stack:', collectError.stack);
|
||||||
|
showAlert('danger', 'Failed to collect form data. Check console for details.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Validate
|
// Validate
|
||||||
const errors = validateFormData(formData);
|
let errors = [];
|
||||||
|
try {
|
||||||
|
errors = validateFormData(formData);
|
||||||
|
} catch (validateError) {
|
||||||
|
console.error('[ERROR] Failed to validate form data:', validateError);
|
||||||
|
console.error('[ERROR] Validate error stack:', validateError.stack);
|
||||||
|
showAlert('danger', 'Failed to validate form data. Check console for details.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
showAlert('danger', errors.join(' '));
|
showAlert('danger', errors.join(' '));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Ensure formData is initialized before use
|
||||||
showStatusIndicator('Deploying container...');
|
if (!formData || typeof formData !== 'object') {
|
||||||
showOperationStatus(`Deploying ${formData.containerName}`, 'processing');
|
console.error('[ERROR] Invalid form data:', formData);
|
||||||
const successResponse = await deployDockerContainer(formData);
|
showAlert('danger', 'Invalid form data. Please check your input and try again.');
|
||||||
hideStatusIndicator();
|
return;
|
||||||
showOperationStatus(`Deploying ${formData.containerName}`, 'completed');
|
}
|
||||||
|
|
||||||
// Close all modals including the deploy modal
|
try {
|
||||||
closeAllModals();
|
// Safely get container name with fallback
|
||||||
if (templateDeployModal) {
|
const containerName = (formData && formData.containerName) ? String(formData.containerName) : 'container';
|
||||||
templateDeployModal.hide();
|
|
||||||
|
showStatusIndicator('Deploying container...');
|
||||||
|
try {
|
||||||
|
if (typeof showOperationStatus === 'function') {
|
||||||
|
showOperationStatus(`Deploying ${containerName}`, 'processing');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[WARN] Failed to show operation status:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deploy container with proper error handling
|
||||||
|
const successResponse = await deployDockerContainer(formData);
|
||||||
|
|
||||||
|
// Ensure we have a valid response
|
||||||
|
if (!successResponse || typeof successResponse !== 'object') {
|
||||||
|
throw new Error('Invalid response from deployment function');
|
||||||
}
|
}
|
||||||
|
|
||||||
showAlert('success', successResponse.message || 'Container deployed successfully!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[ERROR] Failed to deploy container:', error.message);
|
|
||||||
hideStatusIndicator();
|
hideStatusIndicator();
|
||||||
showOperationStatus(`Deploying ${formData.containerName}`, 'failed');
|
|
||||||
showAlert('danger', error.message);
|
try {
|
||||||
|
if (typeof showOperationStatus === 'function') {
|
||||||
|
showOperationStatus(`Deploying ${containerName}`, 'completed');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[WARN] Failed to show operation status:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all modals including the deploy modal
|
||||||
|
if (typeof closeAllModals === 'function') {
|
||||||
|
closeAllModals();
|
||||||
|
}
|
||||||
|
closeDeployModal();
|
||||||
|
|
||||||
|
// Safely extract success message
|
||||||
|
const successMessage = (successResponse && successResponse.message)
|
||||||
|
? String(successResponse.message)
|
||||||
|
: 'Container deployed successfully!';
|
||||||
|
showAlert('success', successMessage);
|
||||||
|
} catch (error) {
|
||||||
|
// Safely extract error message
|
||||||
|
const errorMessage = (error && error.message)
|
||||||
|
? String(error.message)
|
||||||
|
: 'Failed to deploy container. Check console for details.';
|
||||||
|
|
||||||
|
console.error('[ERROR] Failed to deploy container:', errorMessage);
|
||||||
|
if (error && error.stack) {
|
||||||
|
console.error('[ERROR] Full error stack:', error.stack);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
console.error('[ERROR] Error details:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure status indicator is hidden
|
||||||
|
try {
|
||||||
|
hideStatusIndicator();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[WARN] Failed to hide status indicator:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safely get container name for error status
|
||||||
|
try {
|
||||||
|
const containerName = (formData && formData.containerName)
|
||||||
|
? String(formData.containerName)
|
||||||
|
: 'container';
|
||||||
|
if (typeof showOperationStatus === 'function') {
|
||||||
|
showOperationStatus(`Deploying ${containerName}`, 'failed');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[WARN] Failed to show operation status:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlert('danger', errorMessage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add save template functionality
|
// Save template functionality removed to match working version
|
||||||
window.saveCurrentTemplate = function() {
|
|
||||||
const formData = collectFormData();
|
|
||||||
const templateName = prompt('Enter a name for this template:');
|
|
||||||
if (templateName && templateName.trim()) {
|
|
||||||
if (templateStorage.saveTemplate(templateName.trim(), formData)) {
|
|
||||||
showAlert('success', `Template "${templateName}" saved successfully!`);
|
|
||||||
fetchTemplates(); // Refresh template list
|
|
||||||
} else {
|
|
||||||
showAlert('danger', 'Failed to save template.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize templates on load
|
// Initialize templates on load
|
||||||
document.addEventListener('DOMContentLoaded', fetchTemplates);
|
document.addEventListener('DOMContentLoaded', fetchTemplates);
|
||||||
|
|||||||
+13
-6
@@ -2,9 +2,16 @@
|
|||||||
* Local template storage and management
|
* Local template storage and management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CONFIG } from '../config.js';
|
// Storage key for templates (removed CONFIG dependency to avoid TDZ errors)
|
||||||
|
const STORAGE_KEY = 'peardock_templates';
|
||||||
|
|
||||||
const STORAGE_KEY = CONFIG.STORAGE.TEMPLATES_KEY;
|
/**
|
||||||
|
* Get the storage key for templates
|
||||||
|
* @returns {string} - Storage key
|
||||||
|
*/
|
||||||
|
function getStorageKey() {
|
||||||
|
return STORAGE_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save template to local storage
|
* Save template to local storage
|
||||||
@@ -19,7 +26,7 @@ export function saveTemplate(name, template) {
|
|||||||
savedAt: new Date().toISOString(),
|
savedAt: new Date().toISOString(),
|
||||||
version: templates[name]?.version ? templates[name].version + 1 : 1
|
version: templates[name]?.version ? templates[name].version + 1 : 1
|
||||||
};
|
};
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(templates));
|
localStorage.setItem(getStorageKey(), JSON.stringify(templates));
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ERROR] Failed to save template:', err);
|
console.error('[ERROR] Failed to save template:', err);
|
||||||
@@ -33,7 +40,7 @@ export function saveTemplate(name, template) {
|
|||||||
*/
|
*/
|
||||||
export function loadTemplates() {
|
export function loadTemplates() {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
const stored = localStorage.getItem(getStorageKey());
|
||||||
return stored ? JSON.parse(stored) : {};
|
return stored ? JSON.parse(stored) : {};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ERROR] Failed to load templates:', err);
|
console.error('[ERROR] Failed to load templates:', err);
|
||||||
@@ -51,7 +58,7 @@ export function deleteTemplate(name) {
|
|||||||
const templates = loadTemplates();
|
const templates = loadTemplates();
|
||||||
if (templates[name]) {
|
if (templates[name]) {
|
||||||
delete templates[name];
|
delete templates[name];
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(templates));
|
localStorage.setItem(getStorageKey(), JSON.stringify(templates));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -93,7 +100,7 @@ export function importTemplates(json) {
|
|||||||
|
|
||||||
const existing = loadTemplates();
|
const existing = loadTemplates();
|
||||||
const merged = { ...existing, ...imported };
|
const merged = { ...existing, ...imported };
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(merged));
|
localStorage.setItem(getStorageKey(), JSON.stringify(merged));
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[ERROR] Failed to import templates:', err);
|
console.error('[ERROR] Failed to import templates:', err);
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Shared UI utility functions
|
||||||
|
* Used by both app.js and templateDeploy.js to avoid code duplication
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close all open Bootstrap modals
|
||||||
|
*/
|
||||||
|
export function closeAllModals() {
|
||||||
|
// Find and hide all open modals
|
||||||
|
const modals = document.querySelectorAll('.modal.show');
|
||||||
|
modals.forEach(modal => {
|
||||||
|
const modalInstance = bootstrap.Modal.getInstance(modal);
|
||||||
|
if (modalInstance) {
|
||||||
|
modalInstance.hide();
|
||||||
|
} else {
|
||||||
|
// If no instance exists, create one and hide it
|
||||||
|
const newInstance = new bootstrap.Modal(modal);
|
||||||
|
newInstance.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show status indicator overlay
|
||||||
|
* @param {string} message - Message to display
|
||||||
|
*/
|
||||||
|
export 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide status indicator overlay
|
||||||
|
*/
|
||||||
|
export 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 message
|
||||||
|
* @param {string} type - Alert type (success, danger, warning, info)
|
||||||
|
* @param {string} message - Message to display
|
||||||
|
*/
|
||||||
|
export function showAlert(type, message) {
|
||||||
|
const alertBox = document.createElement('div');
|
||||||
|
alertBox.className = `alert alert-${type}`;
|
||||||
|
alertBox.textContent = message;
|
||||||
|
|
||||||
|
// Get alert container dynamically to avoid dependency on module-specific variables
|
||||||
|
const container = document.getElementById('alert-container') || document.querySelector('#alert-container');
|
||||||
|
if (container) {
|
||||||
|
container.appendChild(alertBox);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (container.contains(alertBox)) {
|
||||||
|
container.removeChild(alertBox);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
console.warn('[WARN] Alert container not found.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+1
-2
@@ -10,7 +10,6 @@ import dotenv from 'dotenv';
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import * as validation from './utils/validation.js';
|
import * as validation from './utils/validation.js';
|
||||||
import rateLimiter from './utils/rateLimiter.js';
|
import rateLimiter from './utils/rateLimiter.js';
|
||||||
import * as containerConfig from './utils/containerConfig.js';
|
|
||||||
import { createErrorResponse, sanitizeErrorMessage } from '../utils/errorHandler.js';
|
import { createErrorResponse, sanitizeErrorMessage } from '../utils/errorHandler.js';
|
||||||
import logger from './utils/logger.js';
|
import logger from './utils/logger.js';
|
||||||
|
|
||||||
@@ -577,7 +576,7 @@ swarm.on('connection', (peer) => {
|
|||||||
|
|
||||||
// Create the container
|
// Create the container
|
||||||
logger.info('Creating container', { name: args.containerName });
|
logger.info('Creating container', { name: args.containerName });
|
||||||
const container = await docker.createContainer(config);
|
const container = await docker.createContainer(containerConfig);
|
||||||
|
|
||||||
// Connect to custom network if specified
|
// Connect to custom network if specified
|
||||||
if (args.customNetwork && args.networkMode !== 'container' && args.networkMode !== 'host' && args.networkMode !== 'none') {
|
if (args.customNetwork && args.networkMode !== 'container' && args.networkMode !== 'host' && args.networkMode !== 'none') {
|
||||||
|
|||||||
@@ -156,3 +156,5 @@ const logger = new Logger({
|
|||||||
|
|
||||||
export default logger;
|
export default logger;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -134,3 +134,5 @@ const rateLimiter = new RateLimiter();
|
|||||||
|
|
||||||
export default rateLimiter;
|
export default rateLimiter;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -234,3 +234,5 @@ export {
|
|||||||
validateNumber
|
validateNumber
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,3 +48,5 @@ test('sanitizeErrorMessage - truncates long messages', (t) => {
|
|||||||
t.ok(sanitized.endsWith('...'));
|
t.ok(sanitized.endsWith('...'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -74,3 +74,5 @@ test('validateNumber - invalid numbers', (t) => {
|
|||||||
t.equal(validation.validateNumber(null, 0, 10), null);
|
t.equal(validation.validateNumber(null, 0, 10), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -2,7 +2,16 @@
|
|||||||
* Centralized error handling utility
|
* Centralized error handling utility
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CONFIG } from '../config.js';
|
// Removed CONFIG import to avoid TDZ errors - use constant directly
|
||||||
|
const UNKNOWN_ERROR_CODE = 'UNKNOWN_ERROR';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the unknown error code
|
||||||
|
* @returns {string} - Unknown error code
|
||||||
|
*/
|
||||||
|
function getUnknownErrorCode() {
|
||||||
|
return UNKNOWN_ERROR_CODE;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error types
|
* Error types
|
||||||
@@ -23,7 +32,7 @@ export class AppError extends Error {
|
|||||||
super(message);
|
super(message);
|
||||||
this.name = 'AppError';
|
this.name = 'AppError';
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.code = code || CONFIG.ERROR_CODES.UNKNOWN_ERROR;
|
this.code = code || getUnknownErrorCode();
|
||||||
this.details = details;
|
this.details = details;
|
||||||
this.timestamp = new Date().toISOString();
|
this.timestamp = new Date().toISOString();
|
||||||
}
|
}
|
||||||
@@ -69,7 +78,7 @@ export function createErrorResponse(error, includeDetails = false) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
error: error.message || 'An unexpected error occurred',
|
error: error.message || 'An unexpected error occurred',
|
||||||
code: error.code || CONFIG.ERROR_CODES.UNKNOWN_ERROR,
|
code: error.code || getUnknownErrorCode(),
|
||||||
type: errorType,
|
type: errorType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user