This commit is contained in:
+67
-10
@@ -4,6 +4,7 @@ import { fetchMergedTemplates, getTemplateListUrls } from '../client/templateLis
|
||||
import {
|
||||
isStackTemplate,
|
||||
resolveTemplateForDeploy,
|
||||
buildStackDeployPayload,
|
||||
} from '../client/templateResolve.js';
|
||||
|
||||
// DOM Elements - Lazy loaded (initialized when modal opens)
|
||||
@@ -801,11 +802,25 @@ function createEnvVarInput(envVar, id) {
|
||||
const presetIcon = isPreset ? '<i class="fas fa-lock text-muted ms-2" title="Preset value"></i>' : '';
|
||||
|
||||
switch (inputType) {
|
||||
case 'select':
|
||||
case 'select': {
|
||||
// Portainer: option may set default:true when env.default is absent
|
||||
let effectiveDefault = defaultStr;
|
||||
if (effectiveDefault === '' || effectiveDefault == null) {
|
||||
const marked = selectOptions.find(
|
||||
(o) => o && typeof o === 'object' && (o.default === true || o.default === 'true')
|
||||
);
|
||||
if (marked) effectiveDefault = String(marked.value ?? '');
|
||||
}
|
||||
const options = selectOptions.map(opt => {
|
||||
const optValue = typeof opt === 'object' ? opt.value : opt;
|
||||
const optLabel = typeof opt === 'object' ? (opt.text || opt.label || opt.value) : opt;
|
||||
const selected = String(optValue) === defaultStr ? 'selected' : '';
|
||||
const isDefaultOpt =
|
||||
opt && typeof opt === 'object' && (opt.default === true || opt.default === 'true');
|
||||
const selected =
|
||||
String(optValue) === String(effectiveDefault) ||
|
||||
(effectiveDefault === '' && isDefaultOpt)
|
||||
? 'selected'
|
||||
: '';
|
||||
return `<option value="${escapeAttrValue(optValue)}" ${selected}>${escapeAttrValue(optLabel)}</option>`;
|
||||
}).join('');
|
||||
valueInput = `
|
||||
@@ -818,8 +833,9 @@ function createEnvVarInput(envVar, id) {
|
||||
</select>
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'checkbox':
|
||||
case 'checkbox': {
|
||||
const checked = (defaultValue === true || defaultValue === 'true' || String(defaultValue).toLowerCase() === 'true') ? 'checked' : '';
|
||||
valueInput = `
|
||||
<div class="form-check form-switch">
|
||||
@@ -832,6 +848,7 @@ function createEnvVarInput(envVar, id) {
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'password':
|
||||
valueInput = `
|
||||
@@ -2409,13 +2426,40 @@ function populateDeployFormFromTemplate(template, opts = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Template note (setup instructions from the catalog — HTML from maintainers)
|
||||
renderTemplateNote(template.note || template.Note || '');
|
||||
|
||||
try {
|
||||
updatePreview();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return { ok: true, isStack, warnings };
|
||||
return { ok: true, isStack, warnings, catalogIsStack, hasImage };
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the Portainer template note panel above the form.
|
||||
* @param {string} noteHtml
|
||||
*/
|
||||
function renderTemplateNote(noteHtml) {
|
||||
const text = String(noteHtml || '').trim();
|
||||
for (const form of [formScope, document.getElementById('deploy-view-form'), document.getElementById('deploy-form')].filter(Boolean)) {
|
||||
if (!form || !form.querySelector) continue;
|
||||
let panel = form.querySelector('.deploy-template-note');
|
||||
if (!text) {
|
||||
if (panel) panel.remove();
|
||||
continue;
|
||||
}
|
||||
if (!panel) {
|
||||
panel = document.createElement('div');
|
||||
panel.className = 'deploy-template-note alert alert-info py-2 px-3 mb-3 small';
|
||||
panel.setAttribute('role', 'note');
|
||||
form.insertBefore(panel, form.firstChild);
|
||||
}
|
||||
// Catalog notes are trusted HTML from the template list (same as Portainer)
|
||||
panel.innerHTML = `<div class="fw-semibold mb-1"><i class="fas fa-info-circle me-1"></i>Template notes</div><div class="deploy-template-note-body">${text}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Open deploy modal and populate the form dynamically
|
||||
@@ -2444,9 +2488,12 @@ async function openDeployModal(template) {
|
||||
|
||||
if (form) form.reset();
|
||||
|
||||
// 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)) {
|
||||
const typeNum = Number(template.type);
|
||||
const isCatalogStack =
|
||||
typeNum === 2 || typeNum === 3 || isStackTemplate(template);
|
||||
|
||||
if (isCatalogStack || !String(template.image || template.Image || '').trim()) {
|
||||
showStatusIndicator(`Loading compose for ${template.title || template.name || 'template'}…`);
|
||||
try {
|
||||
resolved = await resolveTemplateForDeploy(template);
|
||||
@@ -2455,6 +2502,13 @@ async function openDeployModal(template) {
|
||||
}
|
||||
}
|
||||
|
||||
// Stack templates with compose → Stacks modal (full multi-service support)
|
||||
if (isCatalogStack && resolved._composeText && typeof window.openStackDeployFromTemplate === 'function') {
|
||||
if (window.openStackDeployFromTemplate(resolved)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
currentTemplate = resolved;
|
||||
const result = populateDeployFormFromTemplate(resolved);
|
||||
if (result.isStack) {
|
||||
@@ -2515,13 +2569,15 @@ function validateFormData(data) {
|
||||
errors.push('Container name must be 63 characters or less.');
|
||||
}
|
||||
|
||||
// Image validation
|
||||
// Image validation — allow registry hosts, ports, tags, and digests
|
||||
if (!data.image || !data.image.trim()) {
|
||||
errors.push('Image name is required.');
|
||||
} else {
|
||||
// Basic image name validation
|
||||
const imagePattern = /^([a-z0-9._-]+\/)*[a-z0-9._-]+(:[a-zA-Z0-9._-]+)?$/;
|
||||
if (!imagePattern.test(data.image.trim())) {
|
||||
const img = data.image.trim();
|
||||
// e.g. nginx, nginx:latest, ghcr.io/org/app:1.2, host:5000/ns/img@sha256:…
|
||||
const imagePattern =
|
||||
/^(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*(?::[0-9]+)?\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*(?::[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})?(?:@sha256:[a-f0-9]{64})?$/i;
|
||||
if (!imagePattern.test(img) || img.length > 256) {
|
||||
errors.push('Invalid Docker image name format.');
|
||||
}
|
||||
}
|
||||
@@ -4140,3 +4196,4 @@ window.populateDuplicateForm = populateDuplicateForm;
|
||||
window.populateDeployFormFromTemplate = populateDeployFormFromTemplate;
|
||||
window.setDeployFormScope = setDeployFormScope;
|
||||
window.resolveTemplateForDeploy = resolveTemplateForDeploy;
|
||||
window.buildStackDeployPayload = buildStackDeployPayload;
|
||||
|
||||
Reference in New Issue
Block a user