This commit is contained in:
@@ -3535,8 +3535,10 @@ function renderStacks(stacks) {
|
||||
function setupDeployStackHandler() {
|
||||
const deployStackBtn = document.getElementById('deploy-stack-btn');
|
||||
const deployStackForm = document.getElementById('deploy-stack-form');
|
||||
|
||||
if (deployStackBtn && deployStackForm) {
|
||||
const gitOpsBtn = document.getElementById('stack-gitops-btn');
|
||||
|
||||
if (deployStackBtn && deployStackForm && deployStackBtn.dataset.handlerBound !== '1') {
|
||||
deployStackBtn.dataset.handlerBound = '1';
|
||||
deployStackBtn.addEventListener('click', async () => {
|
||||
const stackName = document.getElementById('stack-name').value.trim();
|
||||
const composeContent = document.getElementById('compose-content').value.trim();
|
||||
@@ -3574,41 +3576,44 @@ function setupDeployStackHandler() {
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('stack-gitops-btn')?.addEventListener('click', async () => {
|
||||
const stackName = document.getElementById('stack-name')?.value?.trim();
|
||||
const repoUrl = document.getElementById('stack-git-url')?.value?.trim();
|
||||
const ref = document.getElementById('stack-git-ref')?.value?.trim() || 'main';
|
||||
const composePath =
|
||||
document.getElementById('stack-git-path')?.value?.trim() || 'docker-compose.yml';
|
||||
if (!stackName || !repoUrl) {
|
||||
showAlert('warning', 'Stack name and repository URL are required for GitOps sync');
|
||||
return;
|
||||
}
|
||||
showStatusIndicator(`GitOps sync ${stackName} from ${repoUrl}…`);
|
||||
try {
|
||||
const response = await manager.request(Methods.syncStackFromGit, {
|
||||
stackName,
|
||||
repoUrl,
|
||||
ref,
|
||||
composePath,
|
||||
});
|
||||
if (response?.composeContent == null && response?.success) {
|
||||
// deployed server-side
|
||||
if (gitOpsBtn && gitOpsBtn.dataset.handlerBound !== '1') {
|
||||
gitOpsBtn.dataset.handlerBound = '1';
|
||||
gitOpsBtn.addEventListener('click', async () => {
|
||||
const stackName = document.getElementById('stack-name')?.value?.trim();
|
||||
const repoUrl = document.getElementById('stack-git-url')?.value?.trim();
|
||||
const ref = document.getElementById('stack-git-ref')?.value?.trim() || 'main';
|
||||
const composePath =
|
||||
document.getElementById('stack-git-path')?.value?.trim() || 'docker-compose.yml';
|
||||
if (!stackName || !repoUrl) {
|
||||
showAlert('warning', 'Stack name and repository URL are required for GitOps sync');
|
||||
return;
|
||||
}
|
||||
showAlert(
|
||||
'success',
|
||||
response?.message || `Stack "${stackName}" synced from git`
|
||||
);
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('deploy-stack-modal'));
|
||||
if (modal) modal.hide();
|
||||
navigateToView('stacks');
|
||||
loadStacks();
|
||||
} catch (error) {
|
||||
presentError(error, 'syncStackFromGit', { showAlert });
|
||||
} finally {
|
||||
hideStatusIndicator();
|
||||
}
|
||||
});
|
||||
showStatusIndicator(`GitOps sync ${stackName} from ${repoUrl}…`);
|
||||
try {
|
||||
const response = await manager.request(Methods.syncStackFromGit, {
|
||||
stackName,
|
||||
repoUrl,
|
||||
ref,
|
||||
composePath,
|
||||
});
|
||||
if (response?.composeContent == null && response?.success) {
|
||||
// deployed server-side
|
||||
}
|
||||
showAlert(
|
||||
'success',
|
||||
response?.message || `Stack "${stackName}" synced from git`
|
||||
);
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('deploy-stack-modal'));
|
||||
if (modal) modal.hide();
|
||||
navigateToView('stacks');
|
||||
loadStacks();
|
||||
} catch (error) {
|
||||
presentError(error, 'syncStackFromGit', { showAlert });
|
||||
} finally {
|
||||
hideStatusIndicator();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Subscription for volumes store to auto-update UI
|
||||
@@ -7580,18 +7585,70 @@ function setupDeployResourceSliders() {
|
||||
}
|
||||
window.setupDeployResourceSliders = setupDeployResourceSliders;
|
||||
|
||||
/**
|
||||
* Open the Stacks deploy modal pre-filled from a Portainer type 2/3 template.
|
||||
* @param {object} resolved — after resolveTemplateForDeploy
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function openStackDeployFromTemplate(resolved) {
|
||||
const build =
|
||||
typeof window.buildStackDeployPayload === 'function'
|
||||
? window.buildStackDeployPayload
|
||||
: null;
|
||||
if (!build) return false;
|
||||
const payload = build(resolved);
|
||||
if (!payload?.ok) {
|
||||
showAlert('warning', payload?.error || 'Could not build stack deploy payload');
|
||||
return false;
|
||||
}
|
||||
|
||||
setupDeployStackHandler();
|
||||
const nameEl = document.getElementById('stack-name');
|
||||
const composeEl = document.getElementById('compose-content');
|
||||
const envEl = document.getElementById('stack-env-file');
|
||||
const gitUrlEl = document.getElementById('stack-git-url');
|
||||
const gitRefEl = document.getElementById('stack-git-ref');
|
||||
const gitPathEl = document.getElementById('stack-git-path');
|
||||
if (nameEl) nameEl.value = payload.stackName;
|
||||
if (composeEl) composeEl.value = payload.composeContent;
|
||||
if (envEl) envEl.value = payload.envFileContent || '';
|
||||
if (gitUrlEl) gitUrlEl.value = payload.repoUrl || '';
|
||||
if (gitRefEl) gitRefEl.value = payload.ref || 'main';
|
||||
if (gitPathEl) gitPathEl.value = payload.composePath || 'docker-compose.yml';
|
||||
|
||||
// Optional note strip at top of modal body
|
||||
const modalBody = document.querySelector('#deploy-stack-modal .modal-body');
|
||||
if (modalBody) {
|
||||
modalBody.querySelector('.stack-template-note')?.remove();
|
||||
if (payload.note) {
|
||||
const note = document.createElement('div');
|
||||
note.className = 'stack-template-note alert alert-info py-2 px-3 mb-3 small';
|
||||
note.innerHTML = `<div class="fw-semibold mb-1"><i class="fas fa-info-circle me-1"></i>${escapeHtmlLite(resolved.title || payload.stackName)} notes</div><div>${payload.note}</div>`;
|
||||
modalBody.insertBefore(note, modalBody.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
navigateToView('stacks');
|
||||
const modalEl = document.getElementById('deploy-stack-modal');
|
||||
if (modalEl && typeof bootstrap !== 'undefined') {
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||
modal.show();
|
||||
}
|
||||
const svcN = payload.serviceCount || 0;
|
||||
showAlert(
|
||||
'info',
|
||||
`Loaded stack template “${resolved.title || payload.stackName}”${svcN ? ` (${svcN} services)` : ''} — review compose and deploy.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
window.openStackDeployFromTemplate = openStackDeployFromTemplate;
|
||||
|
||||
async function selectTemplateForDeploy(template) {
|
||||
if (!template || typeof template !== 'object') {
|
||||
showAlert('danger', 'Invalid template');
|
||||
return;
|
||||
}
|
||||
|
||||
const formSection = document.getElementById('deploy-form-section');
|
||||
if (formSection) {
|
||||
formSection.style.display = 'block';
|
||||
formSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
initTemplateDeployer();
|
||||
setupDeployViewFormHandler();
|
||||
setupDeployResourceSliders();
|
||||
@@ -7601,9 +7658,7 @@ async function selectTemplateForDeploy(template) {
|
||||
if (typeof window.setDeployFormScope === 'function') {
|
||||
window.setDeployFormScope(form);
|
||||
}
|
||||
if (form) form.reset();
|
||||
|
||||
// Full Portainer template → form mapping (ports, volumes, env, network, labels, …)
|
||||
const populate =
|
||||
typeof window.populateDeployFormFromTemplate === 'function'
|
||||
? window.populateDeployFormFromTemplate
|
||||
@@ -7618,13 +7673,16 @@ async function selectTemplateForDeploy(template) {
|
||||
return;
|
||||
}
|
||||
|
||||
const typeNum = Number(template.type);
|
||||
const isCatalogStack =
|
||||
typeNum === 2 ||
|
||||
typeNum === 3 ||
|
||||
(!String(template.image || '').trim() &&
|
||||
(template.repository?.url || template.repository));
|
||||
|
||||
let resolved = template;
|
||||
const needsCompose =
|
||||
!String(template.image || template.Image || '').trim() &&
|
||||
(Number(template.type) === 2 ||
|
||||
Number(template.type) === 3 ||
|
||||
template.repository);
|
||||
if (needsCompose && resolve) {
|
||||
// Always resolve stack catalogs (and type-1 never needs compose)
|
||||
if (isCatalogStack && resolve) {
|
||||
showStatusIndicator(
|
||||
`Loading compose for ${template.title || template.name || 'template'}…`
|
||||
);
|
||||
@@ -7635,12 +7693,25 @@ async function selectTemplateForDeploy(template) {
|
||||
}
|
||||
}
|
||||
|
||||
// Type 2/3 (and multi-service compose) → full stack deploy path (Portainer parity)
|
||||
if (isCatalogStack && resolved._composeText) {
|
||||
if (openStackDeployFromTemplate(resolved)) return;
|
||||
}
|
||||
|
||||
// Container path (type 1, or stack fallback when compose missing but image resolved)
|
||||
const formSection = document.getElementById('deploy-form-section');
|
||||
if (formSection) {
|
||||
formSection.style.display = 'block';
|
||||
formSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
if (form) form.reset();
|
||||
|
||||
const result = populate(resolved);
|
||||
if (result?.isStack) {
|
||||
showAlert(
|
||||
'warning',
|
||||
result.warnings?.[0] ||
|
||||
`“${template.title || template.name || 'Template'}” is a Compose/stack template. Use Stacks with the compose file from its repository, or pick a container (type 1) template.`
|
||||
`“${template.title || template.name || 'Template'}” is a Compose/stack template. Could not load its compose file — check the repository URL or deploy manually under Stacks.`
|
||||
);
|
||||
} else if (result?.warnings?.length) {
|
||||
showAlert(resolved._composeResolved ? 'info' : 'warning', result.warnings.join(' '));
|
||||
|
||||
Reference in New Issue
Block a user