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 { fetchTemplates, displayTemplateList, openDeployModal } from './libs/templateDeploy.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
|
||||
const containerList = document.getElementById('container-list');
|
||||
@@ -39,14 +40,7 @@ function stopStatsInterval() {
|
||||
}
|
||||
}
|
||||
|
||||
function closeAllModals() {
|
||||
// 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
|
||||
});
|
||||
}
|
||||
// Utility functions are now imported from uiUtils.js
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -346,52 +340,7 @@ function initContainerFiltering() {
|
||||
// Initialize the app
|
||||
console.log('[INFO] Client app initialized');
|
||||
|
||||
// Show Status Indicator
|
||||
// Modify showStatusIndicator to recreate it dynamically
|
||||
function showStatusIndicator(message = 'Processing...') {
|
||||
const statusIndicator = document.createElement('div');
|
||||
statusIndicator.id = 'status-indicator';
|
||||
statusIndicator.className = 'position-fixed top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-dark bg-opacity-75';
|
||||
statusIndicator.innerHTML = `
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-3 text-light">${message}</p>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(statusIndicator);
|
||||
}
|
||||
|
||||
function hideStatusIndicator() {
|
||||
const statusIndicator = document.getElementById('status-indicator');
|
||||
if (statusIndicator) {
|
||||
console.log('[DEBUG] Hiding status indicator');
|
||||
statusIndicator.remove();
|
||||
} else {
|
||||
console.error('[ERROR] Status indicator element not found!');
|
||||
}
|
||||
}
|
||||
// Show Alert
|
||||
// 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.');
|
||||
}
|
||||
}
|
||||
// Utility functions are now imported from uiUtils.js
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
function renderContainers(containers, topicId) {
|
||||
if (!window.activePeer || !connections[topicId] || window.activePeer !== connections[topicId].peer) {
|
||||
@@ -1041,8 +1051,22 @@ function renderContainers(containers, topicId) {
|
||||
<td>${name}</td>
|
||||
<td>${image}</td>
|
||||
<td>${container.State || 'Unknown'}</td>
|
||||
<td class="cpu">0</td>
|
||||
<td class="memory">0</td>
|
||||
<td class="cpu">
|
||||
<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>
|
||||
<div class="btn-group btn-group-sm">
|
||||
|
||||
@@ -178,7 +178,7 @@ const BLOCKED_PATTERNS = [
|
||||
/&&/, // Command chaining
|
||||
/\|\|/, // OR operator
|
||||
/;/, // Command separator
|
||||
/>/.*</, // Redirection
|
||||
/>.*</, // Redirection (e.g., >file or <file)
|
||||
/2>&1/, // Stderr redirection
|
||||
];
|
||||
|
||||
|
||||
@@ -158,3 +158,5 @@ function getStatusColor(status) {
|
||||
return colors[status] || 'secondary';
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+181
-153
@@ -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
|
||||
const templateList = document.getElementById('template-list');
|
||||
const templateSearchInput = document.getElementById('template-search-input');
|
||||
const templateDeployModal = new bootstrap.Modal(document.getElementById('templateDeployModalUnique'));
|
||||
const deployForm = document.getElementById('deploy-form');
|
||||
let templates = [];
|
||||
let localTemplates = [];
|
||||
|
||||
// Import template storage
|
||||
import * as templateStorage from './templateStorage.js';
|
||||
|
||||
// Array item counters for unique IDs
|
||||
let portCounter = 0;
|
||||
@@ -24,96 +24,27 @@ let sysctlCounter = 0;
|
||||
let ulimitCounter = 0;
|
||||
let tmpfsCounter = 0;
|
||||
|
||||
function closeAllModals() {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
// Also explicitly close the deploy modal if it exists
|
||||
// Utility functions are now imported from uiUtils.js
|
||||
// Also explicitly close the deploy modal if needed
|
||||
function closeDeployModal() {
|
||||
if (templateDeployModal) {
|
||||
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
|
||||
async function fetchTemplates() {
|
||||
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');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
templates = data.templates || [];
|
||||
|
||||
// Combine local and remote templates
|
||||
displayTemplateList([...localTemplates, ...templates]);
|
||||
templates = data.templates || []; // Update global templates
|
||||
displayTemplateList(templates);
|
||||
} catch (error) {
|
||||
console.error('[ERROR] Failed to fetch templates:', error.message);
|
||||
// Still show local templates even if remote fetch fails
|
||||
displayTemplateList(localTemplates);
|
||||
showAlert('warning', 'Failed to load remote templates. Showing local templates only.');
|
||||
showAlert('danger', 'Failed to load templates.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,52 +61,19 @@ templateSearchInput.addEventListener('input', () => {
|
||||
// Display templates in the list
|
||||
function displayTemplateList(templates) {
|
||||
templateList.innerHTML = '';
|
||||
|
||||
// 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;
|
||||
templates.forEach(template => {
|
||||
const listItem = document.createElement('li');
|
||||
listItem.className = 'list-group-item d-flex justify-content-between align-items-center';
|
||||
listItem.innerHTML = `
|
||||
<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>
|
||||
${isLocal ? '<span class="badge bg-info ms-2">Local</span>' : ''}
|
||||
</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>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm deploy-btn">Deploy</button>
|
||||
`;
|
||||
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);
|
||||
});
|
||||
|
||||
// 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
|
||||
@@ -734,19 +632,80 @@ async function deployDockerContainer(payload) {
|
||||
console.log('[INFO] Sending deployment command to the server...');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
window.handlePeerResponse = (response) => {
|
||||
if (response.success && response.message.includes('deployed successfully')) {
|
||||
console.log('[INFO] Deployment response received:', response.message);
|
||||
resolve(response);
|
||||
} else if (response.error) {
|
||||
reject(new Error(response.error));
|
||||
// Store the original handler to restore it later
|
||||
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);
|
||||
resolve(response);
|
||||
} else if (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(() => {
|
||||
reject(new Error('Deployment timed out. No response from server.'));
|
||||
// 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.'));
|
||||
}
|
||||
}, 60000); // Increased timeout for complex deployments
|
||||
});
|
||||
}
|
||||
@@ -755,50 +714,119 @@ async function deployDockerContainer(payload) {
|
||||
deployForm.addEventListener('submit', async (e) => {
|
||||
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
|
||||
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) {
|
||||
showAlert('danger', errors.join(' '));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure formData is initialized before use
|
||||
if (!formData || typeof formData !== 'object') {
|
||||
console.error('[ERROR] Invalid form data:', formData);
|
||||
showAlert('danger', 'Invalid form data. Please check your input and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showStatusIndicator('Deploying container...');
|
||||
showOperationStatus(`Deploying ${formData.containerName}`, 'processing');
|
||||
const successResponse = await deployDockerContainer(formData);
|
||||
hideStatusIndicator();
|
||||
showOperationStatus(`Deploying ${formData.containerName}`, 'completed');
|
||||
// Safely get container name with fallback
|
||||
const containerName = (formData && formData.containerName) ? String(formData.containerName) : 'container';
|
||||
|
||||
// Close all modals including the deploy modal
|
||||
closeAllModals();
|
||||
if (templateDeployModal) {
|
||||
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();
|
||||
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
|
||||
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.');
|
||||
}
|
||||
}
|
||||
};
|
||||
// Save template functionality removed to match working version
|
||||
|
||||
// Initialize templates on load
|
||||
document.addEventListener('DOMContentLoaded', fetchTemplates);
|
||||
|
||||
+13
-6
@@ -2,9 +2,16 @@
|
||||
* 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
|
||||
@@ -19,7 +26,7 @@ export function saveTemplate(name, template) {
|
||||
savedAt: new Date().toISOString(),
|
||||
version: templates[name]?.version ? templates[name].version + 1 : 1
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(templates));
|
||||
localStorage.setItem(getStorageKey(), JSON.stringify(templates));
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[ERROR] Failed to save template:', err);
|
||||
@@ -33,7 +40,7 @@ export function saveTemplate(name, template) {
|
||||
*/
|
||||
export function loadTemplates() {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
const stored = localStorage.getItem(getStorageKey());
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
} catch (err) {
|
||||
console.error('[ERROR] Failed to load templates:', err);
|
||||
@@ -51,7 +58,7 @@ export function deleteTemplate(name) {
|
||||
const templates = loadTemplates();
|
||||
if (templates[name]) {
|
||||
delete templates[name];
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(templates));
|
||||
localStorage.setItem(getStorageKey(), JSON.stringify(templates));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -93,7 +100,7 @@ export function importTemplates(json) {
|
||||
|
||||
const existing = loadTemplates();
|
||||
const merged = { ...existing, ...imported };
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(merged));
|
||||
localStorage.setItem(getStorageKey(), JSON.stringify(merged));
|
||||
return true;
|
||||
} catch (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 * as validation from './utils/validation.js';
|
||||
import rateLimiter from './utils/rateLimiter.js';
|
||||
import * as containerConfig from './utils/containerConfig.js';
|
||||
import { createErrorResponse, sanitizeErrorMessage } from '../utils/errorHandler.js';
|
||||
import logger from './utils/logger.js';
|
||||
|
||||
@@ -577,7 +576,7 @@ swarm.on('connection', (peer) => {
|
||||
|
||||
// Create the container
|
||||
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
|
||||
if (args.customNetwork && args.networkMode !== 'container' && args.networkMode !== 'host' && args.networkMode !== 'none') {
|
||||
|
||||
@@ -156,3 +156,5 @@ const logger = new Logger({
|
||||
|
||||
export default logger;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -134,3 +134,5 @@ const rateLimiter = new RateLimiter();
|
||||
|
||||
export default rateLimiter;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -234,3 +234,5 @@ export {
|
||||
validateNumber
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -48,3 +48,5 @@ test('sanitizeErrorMessage - truncates long messages', (t) => {
|
||||
t.ok(sanitized.endsWith('...'));
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -74,3 +74,5 @@ test('validateNumber - invalid numbers', (t) => {
|
||||
t.equal(validation.validateNumber(null, 0, 10), null);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
+12
-3
@@ -2,7 +2,16 @@
|
||||
* 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
|
||||
@@ -23,7 +32,7 @@ export class AppError extends Error {
|
||||
super(message);
|
||||
this.name = 'AppError';
|
||||
this.type = type;
|
||||
this.code = code || CONFIG.ERROR_CODES.UNKNOWN_ERROR;
|
||||
this.code = code || getUnknownErrorCode();
|
||||
this.details = details;
|
||||
this.timestamp = new Date().toISOString();
|
||||
}
|
||||
@@ -69,7 +78,7 @@ export function createErrorResponse(error, includeDetails = false) {
|
||||
|
||||
return {
|
||||
error: error.message || 'An unexpected error occurred',
|
||||
code: error.code || CONFIG.ERROR_CODES.UNKNOWN_ERROR,
|
||||
code: error.code || getUnknownErrorCode(),
|
||||
type: errorType,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user