This commit is contained in:
+165
-98
@@ -1,6 +1,10 @@
|
||||
// Import dependencies first (ES6 imports must be at top)
|
||||
import { closeAllModals, showStatusIndicator, hideStatusIndicator, updateStatusIndicator, showAlert } from './uiUtils.js';
|
||||
import { fetchMergedTemplates, getTemplateListUrls } from '../client/templateLists.js';
|
||||
import {
|
||||
isStackTemplate,
|
||||
resolveTemplateForDeploy,
|
||||
} from '../client/templateResolve.js';
|
||||
|
||||
// DOM Elements - Lazy loaded (initialized when modal opens)
|
||||
let templateList = null;
|
||||
@@ -11,6 +15,40 @@ let templates = [];
|
||||
let searchInputListenerSetup = false; // Track if search input listener is set up
|
||||
let formSubmitListenerSetup = false; // Track if form submit listener is set up
|
||||
|
||||
/**
|
||||
* Active form root (view or modal). Both UIs reuse the same element ids, so
|
||||
* document.getElementById always hits the *first* (view) form and leaves the
|
||||
* modal empty — scope all deploy-* lookups here.
|
||||
* @type {ParentNode|null}
|
||||
*/
|
||||
let formScope = null;
|
||||
|
||||
/**
|
||||
* @param {ParentNode|null|undefined} root
|
||||
*/
|
||||
export function setDeployFormScope(root) {
|
||||
formScope = root || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scoped lookup for deploy form controls (prefer active form root).
|
||||
* @param {string} id
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
function deployEl(id) {
|
||||
const key = String(id || '');
|
||||
if (!key) return null;
|
||||
if (formScope && typeof formScope.querySelector === 'function') {
|
||||
try {
|
||||
const el = formScope.querySelector(`#${key}`);
|
||||
if (el) return /** @type {HTMLElement} */ (el);
|
||||
} catch {
|
||||
// ignore invalid selector
|
||||
}
|
||||
}
|
||||
return document.getElementById(key);
|
||||
}
|
||||
|
||||
// Array item counters for unique IDs
|
||||
let portCounter = 0;
|
||||
let volumeCounter = 0;
|
||||
@@ -140,6 +178,7 @@ function setupFormSubmitListener() {
|
||||
|
||||
deployForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
setDeployFormScope(deployForm);
|
||||
|
||||
let formData;
|
||||
try {
|
||||
@@ -296,7 +335,7 @@ function displayTemplateList(templates) {
|
||||
|
||||
// Array management functions
|
||||
function addPortMapping(portData = null) {
|
||||
const container = document.getElementById('deploy-ports-container');
|
||||
const container = deployEl('deploy-ports-container');
|
||||
if (!container) {
|
||||
console.error('[ERROR] Ports container not found - element with id "deploy-ports-container" does not exist');
|
||||
return;
|
||||
@@ -471,7 +510,7 @@ function validatePortMapping(portId) {
|
||||
}
|
||||
|
||||
function addVolumeMount(volumeData = null) {
|
||||
const container = document.getElementById('deploy-volumes-container');
|
||||
const container = deployEl('deploy-volumes-container');
|
||||
if (!container) return;
|
||||
const id = `volume-${volumeCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
@@ -681,7 +720,7 @@ function validateVolumeMount(volumeId) {
|
||||
}
|
||||
|
||||
function addTmpfsMount() {
|
||||
const container = document.getElementById('deploy-tmpfs-container');
|
||||
const container = deployEl('deploy-tmpfs-container');
|
||||
const id = `tmpfs-${tmpfsCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
@@ -905,7 +944,7 @@ function createEnvVarInput(envVar, id) {
|
||||
}
|
||||
|
||||
function addEnvVar(envVar = null) {
|
||||
const container = document.getElementById('deploy-env');
|
||||
const container = deployEl('deploy-env');
|
||||
const id = `env-${envCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item mb-2';
|
||||
@@ -939,7 +978,7 @@ function addEnvVar(envVar = null) {
|
||||
}
|
||||
|
||||
function addLabel() {
|
||||
const container = document.getElementById('deploy-labels-container');
|
||||
const container = deployEl('deploy-labels-container');
|
||||
const id = `label-${labelCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
@@ -955,7 +994,7 @@ function addLabel() {
|
||||
}
|
||||
|
||||
function addDnsServer() {
|
||||
const container = document.getElementById('deploy-dns-container');
|
||||
const container = deployEl('deploy-dns-container');
|
||||
const id = `dns-${dnsCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
@@ -971,7 +1010,7 @@ function addDnsServer() {
|
||||
}
|
||||
|
||||
function addExtraHost() {
|
||||
const container = document.getElementById('deploy-extra-hosts-container');
|
||||
const container = deployEl('deploy-extra-hosts-container');
|
||||
const id = `host-${extraHostCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
@@ -987,7 +1026,7 @@ function addExtraHost() {
|
||||
}
|
||||
|
||||
function addDeviceMapping() {
|
||||
const container = document.getElementById('deploy-devices-container');
|
||||
const container = deployEl('deploy-devices-container');
|
||||
const id = `device-${deviceCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
@@ -1003,7 +1042,7 @@ function addDeviceMapping() {
|
||||
}
|
||||
|
||||
function addCapability() {
|
||||
const container = document.getElementById('deploy-capabilities-container');
|
||||
const container = deployEl('deploy-capabilities-container');
|
||||
const id = `cap-${capabilityCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
@@ -1019,7 +1058,7 @@ function addCapability() {
|
||||
}
|
||||
|
||||
function addSecurityOpt() {
|
||||
const container = document.getElementById('deploy-security-opts-container');
|
||||
const container = deployEl('deploy-security-opts-container');
|
||||
const id = `secopt-${securityOptCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
@@ -1035,7 +1074,7 @@ function addSecurityOpt() {
|
||||
}
|
||||
|
||||
function addLogOpt() {
|
||||
const container = document.getElementById('deploy-log-opts-container');
|
||||
const container = deployEl('deploy-log-opts-container');
|
||||
const id = `logopt-${logOptCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
@@ -1052,7 +1091,7 @@ function addLogOpt() {
|
||||
}
|
||||
|
||||
function addSysctl() {
|
||||
const container = document.getElementById('deploy-sysctls-container');
|
||||
const container = deployEl('deploy-sysctls-container');
|
||||
const id = `sysctl-${sysctlCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
@@ -1068,7 +1107,7 @@ function addSysctl() {
|
||||
}
|
||||
|
||||
function addUlimit() {
|
||||
const container = document.getElementById('deploy-ulimits-container');
|
||||
const container = deployEl('deploy-ulimits-container');
|
||||
const id = `ulimit-${ulimitCounter++}`;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'array-item';
|
||||
@@ -1613,8 +1652,8 @@ window.updateSliderRange = updateSliderRange;
|
||||
|
||||
// Network mode change handler
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const networkMode = document.getElementById('deploy-network-mode');
|
||||
const customNetworkContainer = document.getElementById('deploy-custom-network-container');
|
||||
const networkMode = deployEl('deploy-network-mode');
|
||||
const customNetworkContainer = deployEl('deploy-custom-network-container');
|
||||
if (networkMode && customNetworkContainer) {
|
||||
networkMode.addEventListener('change', (e) => {
|
||||
if (e.target.value === 'container') {
|
||||
@@ -1643,24 +1682,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Collect all form data
|
||||
function collectFormData() {
|
||||
const data = {
|
||||
containerName: document.getElementById('deploy-container-name')?.value.trim() || '',
|
||||
image: document.getElementById('deploy-image')?.value.trim() || '',
|
||||
command: document.getElementById('deploy-command')?.value.trim() || null,
|
||||
entrypoint: document.getElementById('deploy-entrypoint')?.value.trim() || null,
|
||||
workingDir: document.getElementById('deploy-workdir')?.value.trim() || null,
|
||||
containerName: deployEl('deploy-container-name')?.value.trim() || '',
|
||||
image: deployEl('deploy-image')?.value.trim() || '',
|
||||
command: deployEl('deploy-command')?.value.trim() || null,
|
||||
entrypoint: deployEl('deploy-entrypoint')?.value.trim() || null,
|
||||
workingDir: deployEl('deploy-workdir')?.value.trim() || null,
|
||||
|
||||
// Networking
|
||||
networkMode: document.getElementById('deploy-network-mode')?.value || 'bridge',
|
||||
networkMode: deployEl('deploy-network-mode')?.value || 'bridge',
|
||||
customNetwork: (() => {
|
||||
const mode = document.getElementById('deploy-network-mode')?.value || 'bridge';
|
||||
const customNet = document.getElementById('deploy-custom-network')?.value.trim();
|
||||
const mode = deployEl('deploy-network-mode')?.value || 'bridge';
|
||||
const customNet = deployEl('deploy-custom-network')?.value.trim();
|
||||
if (!customNet) return null;
|
||||
// host/none: no extra attach; container: peer container name; bridge: optional user network
|
||||
if (mode === 'host' || mode === 'none') return null;
|
||||
return customNet;
|
||||
})(),
|
||||
hostname: document.getElementById('deploy-hostname')?.value.trim() || null,
|
||||
domainname: document.getElementById('deploy-domainname')?.value.trim() || null,
|
||||
hostname: deployEl('deploy-hostname')?.value.trim() || null,
|
||||
domainname: deployEl('deploy-domainname')?.value.trim() || null,
|
||||
ports: collectPortMappings(),
|
||||
dns: collectArrayItems('deploy-dns-container', 'data-dns-id'),
|
||||
extraHosts: collectArrayItems('deploy-extra-hosts-container', 'data-host-id'),
|
||||
@@ -1672,23 +1711,23 @@ function collectFormData() {
|
||||
// Resources
|
||||
// Range sliders use 0 for “unset / unlimited”
|
||||
cpuLimit: (() => {
|
||||
const v = parseFloat(document.getElementById('deploy-cpu-limit')?.value)
|
||||
const v = parseFloat(deployEl('deploy-cpu-limit')?.value)
|
||||
return Number.isFinite(v) && v > 0 ? v : null
|
||||
})(),
|
||||
cpuReservation: (() => {
|
||||
const v = parseFloat(document.getElementById('deploy-cpu-reservation')?.value)
|
||||
const v = parseFloat(deployEl('deploy-cpu-reservation')?.value)
|
||||
return Number.isFinite(v) && v > 0 ? v : null
|
||||
})(),
|
||||
cpuShares: parseInt(document.getElementById('deploy-cpu-shares')?.value) || null,
|
||||
cpuShares: parseInt(deployEl('deploy-cpu-shares')?.value) || null,
|
||||
memoryLimit: (() => {
|
||||
const v = parseInt(document.getElementById('deploy-memory-limit')?.value, 10)
|
||||
const v = parseInt(deployEl('deploy-memory-limit')?.value, 10)
|
||||
return Number.isFinite(v) && v > 0 ? v : null
|
||||
})(),
|
||||
memoryReservation: (() => {
|
||||
const v = parseInt(document.getElementById('deploy-memory-reservation')?.value, 10)
|
||||
const v = parseInt(deployEl('deploy-memory-reservation')?.value, 10)
|
||||
return Number.isFinite(v) && v > 0 ? v : null
|
||||
})(),
|
||||
memorySwap: parseInt(document.getElementById('deploy-memory-swap')?.value) || null,
|
||||
memorySwap: parseInt(deployEl('deploy-memory-swap')?.value) || null,
|
||||
devices: collectArrayItems('deploy-devices-container', 'data-device-id'),
|
||||
|
||||
// Environment & Labels
|
||||
@@ -1696,37 +1735,37 @@ function collectFormData() {
|
||||
labels: collectLabels(),
|
||||
|
||||
// Security
|
||||
user: document.getElementById('deploy-user')?.value.trim() || null,
|
||||
group: document.getElementById('deploy-group')?.value.trim() || null,
|
||||
privileged: document.getElementById('deploy-privileged')?.checked || false,
|
||||
readonlyRootfs: document.getElementById('deploy-readonly-rootfs')?.checked || false,
|
||||
user: deployEl('deploy-user')?.value.trim() || null,
|
||||
group: deployEl('deploy-group')?.value.trim() || null,
|
||||
privileged: deployEl('deploy-privileged')?.checked || false,
|
||||
readonlyRootfs: deployEl('deploy-readonly-rootfs')?.checked || false,
|
||||
capabilities: collectArrayItems('deploy-capabilities-container', 'data-cap-id'),
|
||||
securityOpts: collectArrayItems('deploy-security-opts-container', 'data-secopt-id'),
|
||||
|
||||
// Runtime
|
||||
restartPolicy: document.getElementById('deploy-restart-policy')?.value || 'no',
|
||||
restartMaxRetries: parseInt(document.getElementById('deploy-restart-max-retries')?.value) || null,
|
||||
autoRemove: document.getElementById('deploy-auto-remove')?.checked || false,
|
||||
tty: document.getElementById('deploy-tty')?.checked || false,
|
||||
stdinOpen: document.getElementById('deploy-stdin-open')?.checked || false,
|
||||
detach: document.getElementById('deploy-detach')?.checked !== false, // Default true
|
||||
init: document.getElementById('deploy-init')?.checked || false,
|
||||
restartPolicy: deployEl('deploy-restart-policy')?.value || 'no',
|
||||
restartMaxRetries: parseInt(deployEl('deploy-restart-max-retries')?.value) || null,
|
||||
autoRemove: deployEl('deploy-auto-remove')?.checked || false,
|
||||
tty: deployEl('deploy-tty')?.checked || false,
|
||||
stdinOpen: deployEl('deploy-stdin-open')?.checked || false,
|
||||
detach: deployEl('deploy-detach')?.checked !== false, // Default true
|
||||
init: deployEl('deploy-init')?.checked || false,
|
||||
|
||||
// Health & Logging
|
||||
healthCmd: document.getElementById('deploy-health-cmd')?.value.trim() || null,
|
||||
healthInterval: parseInt(document.getElementById('deploy-health-interval')?.value) || null,
|
||||
healthTimeout: parseInt(document.getElementById('deploy-health-timeout')?.value) || null,
|
||||
healthRetries: parseInt(document.getElementById('deploy-health-retries')?.value) || null,
|
||||
healthStartPeriod: parseInt(document.getElementById('deploy-health-start-period')?.value) || null,
|
||||
logDriver: document.getElementById('deploy-log-driver')?.value || null,
|
||||
healthCmd: deployEl('deploy-health-cmd')?.value.trim() || null,
|
||||
healthInterval: parseInt(deployEl('deploy-health-interval')?.value) || null,
|
||||
healthTimeout: parseInt(deployEl('deploy-health-timeout')?.value) || null,
|
||||
healthRetries: parseInt(deployEl('deploy-health-retries')?.value) || null,
|
||||
healthStartPeriod: parseInt(deployEl('deploy-health-start-period')?.value) || null,
|
||||
logDriver: deployEl('deploy-log-driver')?.value || null,
|
||||
logOpts: collectLogOpts(),
|
||||
|
||||
// Advanced
|
||||
sysctls: collectSysctls(),
|
||||
ulimits: collectUlimits(),
|
||||
oomKillDisable: document.getElementById('deploy-oom-kill-disable')?.checked || false,
|
||||
pidsLimit: parseInt(document.getElementById('deploy-pids-limit')?.value) || null,
|
||||
shmSize: parseInt(document.getElementById('deploy-shm-size')?.value) || null,
|
||||
oomKillDisable: deployEl('deploy-oom-kill-disable')?.checked || false,
|
||||
pidsLimit: parseInt(deployEl('deploy-pids-limit')?.value) || null,
|
||||
shmSize: parseInt(deployEl('deploy-shm-size')?.value) || null,
|
||||
};
|
||||
|
||||
// Remove null/empty values
|
||||
@@ -1752,7 +1791,7 @@ function collectArrayItems(containerId, dataAttr) {
|
||||
|
||||
// Collect port mappings from structured inputs
|
||||
function collectPortMappings() {
|
||||
const container = document.getElementById('deploy-ports-container');
|
||||
const container = deployEl('deploy-ports-container');
|
||||
if (!container) return [];
|
||||
const ports = [];
|
||||
|
||||
@@ -1777,7 +1816,7 @@ function collectPortMappings() {
|
||||
|
||||
// Collect volume mounts from structured inputs
|
||||
function collectVolumeMounts() {
|
||||
const container = document.getElementById('deploy-volumes-container');
|
||||
const container = deployEl('deploy-volumes-container');
|
||||
if (!container) return [];
|
||||
const volumes = [];
|
||||
|
||||
@@ -1814,7 +1853,7 @@ function collectVolumeMounts() {
|
||||
}
|
||||
|
||||
function collectEnvVars() {
|
||||
const container = document.getElementById('deploy-env');
|
||||
const container = deployEl('deploy-env');
|
||||
if (!container) return [];
|
||||
const envVars = [];
|
||||
|
||||
@@ -1877,7 +1916,7 @@ function collectLabels() {
|
||||
}
|
||||
|
||||
function collectLogOpts() {
|
||||
const container = document.getElementById('deploy-log-opts-container');
|
||||
const container = deployEl('deploy-log-opts-container');
|
||||
if (!container) return null;
|
||||
const opts = {};
|
||||
container.querySelectorAll('[data-logopt-key]').forEach(keyInput => {
|
||||
@@ -1922,9 +1961,9 @@ function collectUlimits() {
|
||||
|
||||
// Update preview
|
||||
function updatePreview() {
|
||||
const previewContainer = document.getElementById('deploy-preview-container');
|
||||
const preview = document.getElementById('deploy-preview');
|
||||
const showPreview = document.getElementById('deploy-show-preview')?.checked;
|
||||
const previewContainer = deployEl('deploy-preview-container');
|
||||
const preview = deployEl('deploy-preview');
|
||||
const showPreview = deployEl('deploy-show-preview')?.checked;
|
||||
|
||||
if (!previewContainer || !preview) return;
|
||||
|
||||
@@ -2030,9 +2069,9 @@ function portainerVolumeToString(volume, nameHint = 'data') {
|
||||
* @param {object} template
|
||||
*/
|
||||
function applyTemplateNetwork(template) {
|
||||
const modeEl = document.getElementById('deploy-network-mode');
|
||||
const customEl = document.getElementById('deploy-custom-network');
|
||||
const customWrap = document.getElementById('deploy-custom-network-container');
|
||||
const modeEl = deployEl('deploy-network-mode');
|
||||
const customEl = deployEl('deploy-custom-network');
|
||||
const customWrap = deployEl('deploy-custom-network-container');
|
||||
if (!modeEl) return;
|
||||
|
||||
const raw =
|
||||
@@ -2089,13 +2128,12 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
logOptCounter = sysctlCounter = ulimitCounter = tmpfsCounter = 0;
|
||||
|
||||
const typeNum = Number(template.type);
|
||||
const isStack =
|
||||
typeNum === 2 ||
|
||||
typeNum === 3 ||
|
||||
(!template.image &&
|
||||
(template.repository?.url || template.repository?.URL || template.repository));
|
||||
const catalogIsStack = isStackTemplate(template) || typeNum === 2 || typeNum === 3;
|
||||
// After compose resolve, treat as deployable container when image is present
|
||||
const hasImage = Boolean(String(template.image || template.Image || '').trim());
|
||||
const isStack = catalogIsStack && !hasImage && !template._composeResolved;
|
||||
|
||||
// Clear dynamic lists first
|
||||
// Clear dynamic lists first (scoped to active form root)
|
||||
[
|
||||
'deploy-ports-container',
|
||||
'deploy-volumes-container',
|
||||
@@ -2111,7 +2149,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
'deploy-ulimits-container',
|
||||
'deploy-tmpfs-container',
|
||||
].forEach((id) => {
|
||||
const el = document.getElementById(id);
|
||||
const el = deployEl(id);
|
||||
if (el) el.innerHTML = '';
|
||||
});
|
||||
|
||||
@@ -2123,7 +2161,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
|
||||
// Container name
|
||||
if (!opts.skipName) {
|
||||
const nameEl = document.getElementById('deploy-container-name');
|
||||
const nameEl = deployEl('deploy-container-name');
|
||||
if (nameEl) {
|
||||
const preferred = String(template.name || template.title || nameHint)
|
||||
.toLowerCase()
|
||||
@@ -2135,7 +2173,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
}
|
||||
|
||||
// Image (+ optional registry prefix when image is bare and registry set)
|
||||
const imageEl = document.getElementById('deploy-image');
|
||||
const imageEl = deployEl('deploy-image');
|
||||
if (imageEl) {
|
||||
let image = String(template.image || template.Image || '').trim();
|
||||
const registry = String(template.registry || template.Registry || '').trim();
|
||||
@@ -2144,15 +2182,22 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
image = `${registry.replace(/\/$/, '')}/${image}`;
|
||||
}
|
||||
imageEl.value = image;
|
||||
if (!image && isStack) {
|
||||
if (!image && catalogIsStack) {
|
||||
warnings.push(
|
||||
'Compose/stack template has no single image — deploy via Stacks with the compose file from the repository.'
|
||||
template._composeWarning ||
|
||||
'Compose/stack template has no single image — could not resolve compose from the repository. Deploy the full stack via Stacks, or pick a container (type 1) template.'
|
||||
);
|
||||
} else if (template._composeResolved && template._composeService) {
|
||||
warnings.push(
|
||||
`Resolved image from compose service “${template._composeService}” (multi-service stacks may need Stacks for full deploy).`
|
||||
);
|
||||
} else if (template._composeWarning) {
|
||||
warnings.push(template._composeWarning);
|
||||
}
|
||||
}
|
||||
|
||||
// Command
|
||||
const cmdEl = document.getElementById('deploy-command');
|
||||
const cmdEl = deployEl('deploy-command');
|
||||
if (cmdEl) {
|
||||
const cmd = template.command ?? template.Command ?? template.cmd;
|
||||
if (Array.isArray(cmd)) cmdEl.value = cmd.join(' ');
|
||||
@@ -2161,7 +2206,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
}
|
||||
|
||||
// Entrypoint (rare in Portainer templates)
|
||||
const epEl = document.getElementById('deploy-entrypoint');
|
||||
const epEl = deployEl('deploy-entrypoint');
|
||||
if (epEl) {
|
||||
const ep = template.entrypoint ?? template.Entrypoint;
|
||||
if (Array.isArray(ep)) epEl.value = ep.join(' ');
|
||||
@@ -2170,17 +2215,17 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
}
|
||||
|
||||
// Working dir
|
||||
const wdEl = document.getElementById('deploy-workdir');
|
||||
const wdEl = deployEl('deploy-workdir');
|
||||
if (wdEl) {
|
||||
wdEl.value = template.workingDir || template.working_dir || template.WorkingDir || '';
|
||||
}
|
||||
|
||||
// Hostname / domain
|
||||
const hostnameEl = document.getElementById('deploy-hostname');
|
||||
const hostnameEl = deployEl('deploy-hostname');
|
||||
if (hostnameEl) {
|
||||
hostnameEl.value = template.hostname || template.Hostname || '';
|
||||
}
|
||||
const domainEl = document.getElementById('deploy-domainname');
|
||||
const domainEl = deployEl('deploy-domainname');
|
||||
if (domainEl) {
|
||||
domainEl.value = template.domainname || template.domainName || template.Domainname || '';
|
||||
}
|
||||
@@ -2190,8 +2235,8 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
|
||||
// Interactive / TTY
|
||||
const interactive = template.interactive === true || template.Interactive === true;
|
||||
const ttyEl = document.getElementById('deploy-tty');
|
||||
const stdinEl = document.getElementById('deploy-stdin-open');
|
||||
const ttyEl = deployEl('deploy-tty');
|
||||
const stdinEl = deployEl('deploy-stdin-open');
|
||||
if (ttyEl) ttyEl.checked = interactive || template.tty === true || template.Tty === true;
|
||||
if (stdinEl) {
|
||||
stdinEl.checked =
|
||||
@@ -2199,7 +2244,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
}
|
||||
|
||||
// Privileged
|
||||
const privEl = document.getElementById('deploy-privileged');
|
||||
const privEl = deployEl('deploy-privileged');
|
||||
if (privEl) {
|
||||
privEl.checked =
|
||||
template.privileged === true ||
|
||||
@@ -2208,7 +2253,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
}
|
||||
|
||||
// Read-only rootfs
|
||||
const roEl = document.getElementById('deploy-readonly-rootfs');
|
||||
const roEl = deployEl('deploy-readonly-rootfs');
|
||||
if (roEl) {
|
||||
roEl.checked =
|
||||
template.readonly === true ||
|
||||
@@ -2217,7 +2262,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
}
|
||||
|
||||
// Restart policy
|
||||
const rpEl = document.getElementById('deploy-restart-policy');
|
||||
const rpEl = deployEl('deploy-restart-policy');
|
||||
if (rpEl) {
|
||||
const rp =
|
||||
template.restart_policy ||
|
||||
@@ -2305,7 +2350,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
}
|
||||
for (const pair of pairs) {
|
||||
addLabel();
|
||||
const container = document.getElementById('deploy-labels-container');
|
||||
const container = deployEl('deploy-labels-container');
|
||||
const last = container?.lastElementChild?.querySelector('input');
|
||||
if (last) last.value = pair;
|
||||
}
|
||||
@@ -2318,7 +2363,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
for (const h of hosts) {
|
||||
if (h == null || h === '') continue;
|
||||
addExtraHost();
|
||||
const container = document.getElementById('deploy-extra-hosts-container');
|
||||
const container = deployEl('deploy-extra-hosts-container');
|
||||
const last = container?.lastElementChild?.querySelector('input');
|
||||
if (last) last.value = String(h);
|
||||
}
|
||||
@@ -2330,7 +2375,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
for (const d of devicesRaw) {
|
||||
if (!d) continue;
|
||||
addDeviceMapping();
|
||||
const container = document.getElementById('deploy-devices-container');
|
||||
const container = deployEl('deploy-devices-container');
|
||||
const last = container?.lastElementChild?.querySelector('input');
|
||||
if (last) {
|
||||
last.value =
|
||||
@@ -2347,7 +2392,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
for (const c of caps) {
|
||||
if (!c) continue;
|
||||
addCapability();
|
||||
const container = document.getElementById('deploy-capabilities-container');
|
||||
const container = deployEl('deploy-capabilities-container');
|
||||
const last = container?.lastElementChild?.querySelector('input');
|
||||
if (last) last.value = String(c);
|
||||
}
|
||||
@@ -2358,7 +2403,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
if (sysctls && typeof sysctls === 'object' && !Array.isArray(sysctls)) {
|
||||
for (const [k, v] of Object.entries(sysctls)) {
|
||||
addSysctl();
|
||||
const container = document.getElementById('deploy-sysctls-container');
|
||||
const container = deployEl('deploy-sysctls-container');
|
||||
const last = container?.lastElementChild?.querySelector('input');
|
||||
if (last) last.value = `${k}=${v}`;
|
||||
}
|
||||
@@ -2374,7 +2419,7 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
}
|
||||
|
||||
// Open deploy modal and populate the form dynamically
|
||||
function openDeployModal(template) {
|
||||
async function openDeployModal(template) {
|
||||
if (!template || typeof template !== 'object') {
|
||||
console.error('[ERROR] Invalid template provided to openDeployModal:', template);
|
||||
showAlert('danger', 'Invalid template data. Please try again.');
|
||||
@@ -2383,6 +2428,10 @@ function openDeployModal(template) {
|
||||
|
||||
// Initialize template deployer lazily (DOM elements, modal, event listeners)
|
||||
initTemplateDeployer();
|
||||
|
||||
// Scope all deploy-* lookups to the modal form (not the hidden deploy-view form)
|
||||
const form = document.getElementById('deploy-form');
|
||||
setDeployFormScope(form);
|
||||
|
||||
// Store current template for validation
|
||||
currentTemplate = template;
|
||||
@@ -2393,11 +2442,21 @@ function openDeployModal(template) {
|
||||
deployTitle.textContent = `Deploy ${template.title || template.name || 'Template'}`;
|
||||
}
|
||||
|
||||
// Clear form fields (modal form — may share ids with deploy-view; first id wins)
|
||||
const form = document.getElementById('deploy-form') || document.getElementById('deploy-view-form');
|
||||
if (form) form.reset();
|
||||
|
||||
const result = populateDeployFormFromTemplate(template);
|
||||
// Resolve stack compose → primary service image/ports/volumes/env when catalog omits image
|
||||
let resolved = template;
|
||||
if (!String(template.image || template.Image || '').trim() && isStackTemplate(template)) {
|
||||
showStatusIndicator(`Loading compose for ${template.title || template.name || 'template'}…`);
|
||||
try {
|
||||
resolved = await resolveTemplateForDeploy(template);
|
||||
} finally {
|
||||
hideStatusIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
currentTemplate = resolved;
|
||||
const result = populateDeployFormFromTemplate(resolved);
|
||||
if (result.isStack) {
|
||||
showAlert(
|
||||
'warning',
|
||||
@@ -2405,7 +2464,10 @@ function openDeployModal(template) {
|
||||
'This is a Compose/stack template. Use Stacks to deploy the compose file from its repository.'
|
||||
);
|
||||
} else if (result.warnings.length) {
|
||||
showAlert('warning', result.warnings.join(' '));
|
||||
showAlert(
|
||||
resolved._composeResolved ? 'info' : 'warning',
|
||||
result.warnings.join(' ')
|
||||
);
|
||||
}
|
||||
|
||||
if (templateDeployModal) {
|
||||
@@ -2416,14 +2478,17 @@ function openDeployModal(template) {
|
||||
modalElement.addEventListener(
|
||||
'shown.bs.modal',
|
||||
() => {
|
||||
const portsContainer = document.getElementById('deploy-ports-container');
|
||||
const empty =
|
||||
setDeployFormScope(document.getElementById('deploy-form'));
|
||||
const portsContainer = deployEl('deploy-ports-container');
|
||||
const imageEl = deployEl('deploy-image');
|
||||
const emptyPorts =
|
||||
!portsContainer ||
|
||||
portsContainer.querySelectorAll('.port-mapping-item').length === 0;
|
||||
const emptyImage = !imageEl?.value?.trim();
|
||||
const hasPorts =
|
||||
Array.isArray(template.ports) && template.ports.length > 0;
|
||||
if (empty && hasPorts) {
|
||||
populateDeployFormFromTemplate(template, { skipName: true });
|
||||
Array.isArray(resolved.ports) && resolved.ports.length > 0;
|
||||
if ((emptyPorts && hasPorts) || (emptyImage && resolved.image)) {
|
||||
populateDeployFormFromTemplate(resolved, { skipName: true });
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
@@ -2510,7 +2575,7 @@ function validateFormData(data) {
|
||||
|
||||
// Validate environment variables
|
||||
if (data.env && Array.isArray(data.env)) {
|
||||
const envContainer = document.getElementById('deploy-env');
|
||||
const envContainer = deployEl('deploy-env');
|
||||
if (envContainer && currentTemplate && currentTemplate.env) {
|
||||
// Create a map of template env vars for validation
|
||||
const templateEnvMap = {};
|
||||
@@ -4073,3 +4138,5 @@ export {
|
||||
window.collectDuplicateFormData = collectDuplicateFormData;
|
||||
window.populateDuplicateForm = populateDuplicateForm;
|
||||
window.populateDeployFormFromTemplate = populateDeployFormFromTemplate;
|
||||
window.setDeployFormScope = setDeployFormScope;
|
||||
window.resolveTemplateForDeploy = resolveTemplateForDeploy;
|
||||
|
||||
Reference in New Issue
Block a user