Support multiple deploy template list URLs with cross-list dedupe
CI / test (push) Successful in 9m55s
CI / test (push) Successful in 9m55s
Add Settings UI to configure many Portainer-format template catalogs (not just one), merge them on fetch, drop duplicates by image/stack/title, and show source counts on the Deploy view.
This commit is contained in:
@@ -1290,6 +1290,39 @@ textarea::placeholder {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Template list URLs in Settings */
|
||||
.settings-template-urls {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.settings-template-urls .list-group-item {
|
||||
background: transparent;
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.settings-template-urls .list-group-item .url-text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-template-urls .btn-remove-url {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Density prefs */
|
||||
body[data-density='compact'] .table > :not(caption) > * > * {
|
||||
padding: 0.35rem 0.5rem;
|
||||
|
||||
+131
@@ -25,6 +25,14 @@ import {
|
||||
import { openCommandPalette, confirmDialog, statusBadge } from './components.js'
|
||||
import { showAlert } from '../libs/uiUtils.js'
|
||||
import notificationManager from '../libs/notifications.js'
|
||||
import {
|
||||
DEFAULT_TEMPLATE_LIST_URLS,
|
||||
getTemplateListUrls,
|
||||
setTemplateListUrls,
|
||||
normalizeTemplateListUrls,
|
||||
fetchMergedTemplates,
|
||||
clearMergedTemplateCache,
|
||||
} from '../client/templateLists.js'
|
||||
|
||||
const SETTINGS_KEY = 'peardock.settings.v1'
|
||||
|
||||
@@ -446,6 +454,9 @@ export function appendLiveEvent(evt) {
|
||||
host.prepend(row)
|
||||
}
|
||||
|
||||
/** @type {string[]} in-memory editor state for template list URLs */
|
||||
let templateUrlsDraft = []
|
||||
|
||||
export function loadSettingsView() {
|
||||
const s = loadSettings()
|
||||
const dens = document.getElementById('settings-density')
|
||||
@@ -464,6 +475,99 @@ export function loadSettingsView() {
|
||||
<div>Docker: ${c.dockerHealth?.ok ? 'ok' : '—'}</div>`
|
||||
: 'Not connected'
|
||||
}
|
||||
templateUrlsDraft = getTemplateListUrls()
|
||||
renderTemplateUrlsEditor()
|
||||
}
|
||||
|
||||
function renderTemplateUrlsEditor() {
|
||||
const host = document.getElementById('settings-template-urls')
|
||||
if (!host) return
|
||||
if (!templateUrlsDraft.length) {
|
||||
host.innerHTML =
|
||||
'<li class="list-group-item text-muted">No lists configured — default will be used on save.</li>'
|
||||
return
|
||||
}
|
||||
host.innerHTML = templateUrlsDraft
|
||||
.map(
|
||||
(url, idx) => `
|
||||
<li class="list-group-item" data-idx="${idx}">
|
||||
<span class="url-text" title="${escape(url)}">${escape(url)}</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger btn-remove-url" data-idx="${idx}" title="Remove">
|
||||
<i class="fas fa-xmark"></i>
|
||||
</button>
|
||||
</li>`
|
||||
)
|
||||
.join('')
|
||||
host.querySelectorAll('.btn-remove-url').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const i = Number(btn.dataset.idx)
|
||||
if (!Number.isFinite(i)) return
|
||||
templateUrlsDraft = templateUrlsDraft.filter((_, j) => j !== i)
|
||||
renderTemplateUrlsEditor()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function addTemplateUrlFromInput() {
|
||||
const input = document.getElementById('settings-template-url-input')
|
||||
const raw = input?.value?.trim() || ''
|
||||
if (!raw) {
|
||||
showAlert('warning', 'Enter a template list URL')
|
||||
return
|
||||
}
|
||||
let href
|
||||
try {
|
||||
const u = new URL(raw)
|
||||
if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('http(s) only')
|
||||
href = u.href
|
||||
} catch {
|
||||
showAlert('danger', 'Invalid URL — use http(s)://…/templates.json')
|
||||
return
|
||||
}
|
||||
if (templateUrlsDraft.includes(href)) {
|
||||
showAlert('info', 'That list is already configured')
|
||||
return
|
||||
}
|
||||
templateUrlsDraft = normalizeTemplateListUrls([...templateUrlsDraft, href])
|
||||
if (input) input.value = ''
|
||||
renderTemplateUrlsEditor()
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist draft URLs and optionally force template re-fetch.
|
||||
* @param {{ reload?: boolean }} [opts]
|
||||
*/
|
||||
export async function saveTemplateListSettings(opts = {}) {
|
||||
const urls = setTemplateListUrls(templateUrlsDraft)
|
||||
templateUrlsDraft = urls
|
||||
renderTemplateUrlsEditor()
|
||||
// Clear deploy-view cache so next open refetches
|
||||
clearMergedTemplateCache()
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__peardockClearDeployTemplateCache?.()
|
||||
}
|
||||
if (opts.reload) {
|
||||
const status = document.getElementById('settings-template-lists-status')
|
||||
if (status) status.textContent = 'Fetching catalogs…'
|
||||
try {
|
||||
const result = await fetchMergedTemplates(urls, { force: true })
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__peardockSetDeployTemplates?.(result.templates, result.stats)
|
||||
}
|
||||
const st = result.stats
|
||||
const msg = `Loaded ${st.unique} templates from ${st.sources} list(s)` +
|
||||
(st.duplicates ? ` · removed ${st.duplicates} duplicate(s)` : '') +
|
||||
(st.errors?.length ? ` · ${st.errors.length} list(s) failed` : '')
|
||||
if (status) status.textContent = msg
|
||||
showAlert(st.errors?.length && !st.unique ? 'danger' : 'success', msg)
|
||||
return result
|
||||
} catch (err) {
|
||||
if (status) status.textContent = err.message || 'Reload failed'
|
||||
showAlert('danger', err.message || 'Failed to reload templates')
|
||||
return null
|
||||
}
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
export function openPalette(navigateToView) {
|
||||
@@ -555,17 +659,40 @@ export function initOpsApp({ navigateToView, sendCommand }) {
|
||||
document.getElementById('events-filter')?.addEventListener('input', () => loadEventsView())
|
||||
|
||||
document.getElementById('settings-save-btn')?.addEventListener('click', () => {
|
||||
// Include multi template list URLs
|
||||
setTemplateListUrls(templateUrlsDraft)
|
||||
const next = saveSettings({
|
||||
density: document.getElementById('settings-density')?.value,
|
||||
confirmDestructive: document.getElementById('settings-confirm')?.value !== '0',
|
||||
refreshSeconds: Number(document.getElementById('settings-refresh')?.value) || 0,
|
||||
templateListUrls: getTemplateListUrls(),
|
||||
})
|
||||
startListAutoRefresh(next.refreshSeconds)
|
||||
if (typeof window !== 'undefined') window.__peardockClearDeployTemplateCache?.()
|
||||
showAlert('success', 'Preferences saved')
|
||||
})
|
||||
|
||||
document.getElementById('settings-template-url-add')?.addEventListener('click', () => {
|
||||
addTemplateUrlFromInput()
|
||||
})
|
||||
document.getElementById('settings-template-url-input')?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
addTemplateUrlFromInput()
|
||||
}
|
||||
})
|
||||
document.getElementById('settings-template-url-reset')?.addEventListener('click', () => {
|
||||
templateUrlsDraft = [...DEFAULT_TEMPLATE_LIST_URLS]
|
||||
renderTemplateUrlsEditor()
|
||||
showAlert('info', 'Default template list restored (save preferences to apply)')
|
||||
})
|
||||
document.getElementById('settings-template-reload')?.addEventListener('click', () => {
|
||||
saveTemplateListSettings({ reload: true })
|
||||
})
|
||||
|
||||
// Apply density + refresh on boot
|
||||
applySettings(loadSettings())
|
||||
templateUrlsDraft = getTemplateListUrls()
|
||||
|
||||
subscribeJobs((job) => showJob(job))
|
||||
|
||||
@@ -641,6 +768,10 @@ export function initOpsApp({ navigateToView, sendCommand }) {
|
||||
loadSettings,
|
||||
applySettings,
|
||||
startListAutoRefresh,
|
||||
getTemplateListUrls,
|
||||
setTemplateListUrls,
|
||||
saveTemplateListSettings,
|
||||
fetchMergedTemplates,
|
||||
loadHostView,
|
||||
loadEventsView,
|
||||
loadSettingsView,
|
||||
|
||||
Reference in New Issue
Block a user