diff --git a/hbd/server/templates/settings.html b/hbd/server/templates/settings.html index d910644..1f35e89 100644 --- a/hbd/server/templates/settings.html +++ b/hbd/server/templates/settings.html @@ -382,9 +382,65 @@ .modal-box h3 { margin: 0 0 12px; font-size: 1em; } .backup-row { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #f0f0f0; font-size: .87em; } .backup-row:last-child { border-bottom: none; } + + /* ---- Multi-picker ---- */ + .mpick-wrapper { display: block; } + .mpick-display { + display: flex; align-items: center; gap: 4px; flex-wrap: nowrap; + cursor: pointer; padding: 3px 7px; border: 1px solid #ccc; border-radius: 4px; + min-height: 26px; min-width: 80px; width: 100%; box-sizing: border-box; + background: #fff; user-select: none; overflow: hidden; + } + .mpick-display:hover { border-color: #0066cc; background: #f8fbff; } + .mpick-tag { + padding: 1px 6px; background: #e8eaf6; color: #283593; + border-radius: 10px; font-size: 0.82em; white-space: nowrap; flex-shrink: 0; + } + .mpick-more { color: #888; font-size: 0.82em; white-space: nowrap; flex-shrink: 0; } + .mpick-empty { color: #bbb; font-style: italic; font-size: 0.82em; } + .mpick-panel { + position: fixed; background: #fff; border: 1px solid #d0d0d0; + border-radius: 6px; box-shadow: 0 4px 20px rgba(0,0,0,.18); + z-index: 2000; width: 360px; overflow: hidden; + } + .mpick-panel-header { + padding: 6px 12px; font-size: 0.78em; font-weight: 700; + text-transform: uppercase; letter-spacing: 0.04em; color: #555; + border-bottom: 1px solid #eee; display: flex; + justify-content: space-between; align-items: center; background: #f5f5f5; + } + .mpick-panel-body { display: flex; } + .mpick-col { flex: 1; min-width: 0; max-height: 200px; overflow-y: auto; } + .mpick-col-header { + padding: 4px 10px; font-size: 0.72em; font-weight: 700; + text-transform: uppercase; letter-spacing: 0.04em; color: #888; + border-bottom: 1px solid #f0f0f0; background: #fafafa; + position: sticky; top: 0; z-index: 1; + } + .mpick-col:first-child { border-right: 1px solid #eee; } + .mpick-item { + padding: 5px 10px; font-size: 0.85em; cursor: pointer; + display: flex; align-items: center; justify-content: space-between; + border-bottom: 1px solid #f8f8f8; gap: 4px; + } + .mpick-item:last-child { border-bottom: none; } + .mpick-item-avail:hover { background: #e8f5e9; } + .mpick-item-sel:hover { background: #fce4ec; } + .mpick-arrow { font-size: 1.1em; opacity: 0.4; flex-shrink: 0; line-height: 1; } + .mpick-item:hover .mpick-arrow { opacity: 1; } + .mpick-item-avail .mpick-arrow { color: #2a7a2a; } + .mpick-item-sel .mpick-arrow { color: #c62828; } + .mpick-panel-footer { + padding: 6px 10px; border-top: 1px solid #eee; + display: flex; justify-content: flex-end; background: #f8f8f8; + } + .mpick-none { padding: 10px; font-size: .82em; color: #aaa; text-align: center; } + {%- macro mpick(all_items, sel, cls) -%} +
{%- if sel -%}{%- for v in sel[:2] -%}{{ v | e }}{%- endfor -%}{%- if sel|length > 2 %}+{{ sel|length - 2 }}{%- endif -%}{%- else -%}(none){%- endif -%}
+ {%- endmacro %} {% include 'nav.html' %}
@@ -501,11 +557,7 @@ - + {{ mpick(all_channel_names, u.notification_channels, 'user-ch-select') }} @@ -576,34 +628,10 @@ - - - - - - - - - - - - + {{ mpick(all_usernames, h.managers, 'host-managers') }} + {{ mpick(all_usernames, h.monitors, 'host-monitors') }} + {{ mpick(all_threshold_configs, h.threshold_configs, 'host-tc') }} + {{ mpick(all_channel_names, h.notification_channels, 'host-channels') }} {% endfor %} @@ -1008,10 +1036,6 @@ function addHostRow() { const tbody = document.getElementById('hosts-tbody'); - const sz = n => Math.min(Math.max(n, 1), 4); - const usersOpts = _allUsers.map(u => ``).join(''); - const tcOpts = _allThresholdConfigs.map(t => ``).join(''); - const chOpts = _allChannels.map(c => ``).join(''); const row = document.createElement('tr'); row.setAttribute('data-new-host', 'true'); row.innerHTML = ` @@ -1019,10 +1043,10 @@ - - - - + ${makeMpickHTML(_allUsers, [], 'host-managers')} + ${makeMpickHTML(_allUsers, [], 'host-monitors')} + ${makeMpickHTML(_allThresholdConfigs, [], 'host-tc')} + ${makeMpickHTML(_allChannels, [], 'host-channels')} `; tbody.appendChild(row); } @@ -1097,8 +1121,6 @@ function addUserRow() { const tbody = document.getElementById('users-tbody'); - const opts = _allChannels.map(ch => ``).join(''); - const chHtml = ``; const row = document.createElement('tr'); row.setAttribute('data-new-user', 'true'); row.innerHTML = ` @@ -1106,7 +1128,7 @@ - ${chHtml} + ${makeMpickHTML(_allChannels, [], 'user-ch-select')} `; tbody.appendChild(row); @@ -1186,6 +1208,121 @@ return s.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,'''); } + // ---- Multi-picker ---- + let _mpickPanel = null; + let _mpickTarget = null; + + function _initMpickPanel() { + if (_mpickPanel) return; + const p = document.createElement('div'); + p.className = 'mpick-panel'; + p.style.display = 'none'; + p.innerHTML = ` +
+ Select items + +
+
+
+
Available
+
+
+
+
Selected
+
+
+
+ `; + document.body.appendChild(p); + _mpickPanel = p; + document.addEventListener('mousedown', e => { + if (!_mpickPanel || _mpickPanel.style.display === 'none') return; + if (!_mpickPanel.contains(e.target) && !e.target.closest('.mpick-display')) closeMpick(); + }, true); + document.addEventListener('keydown', e => { + if (e.key === 'Escape' && _mpickPanel && _mpickPanel.style.display !== 'none') closeMpick(); + }); + } + + function openMpick(displayEl) { + if (displayEl.closest('tr')?.dataset.deleted === 'true') return; + _initMpickPanel(); + _mpickTarget = displayEl.closest('.mpick-wrapper'); + _rerenderMpick(); + _mpickPanel.style.display = 'block'; + const rect = displayEl.getBoundingClientRect(); + const pw = _mpickPanel.offsetWidth || 360; + const ph = _mpickPanel.offsetHeight || 280; + let top = rect.bottom + 4; + let left = rect.left; + if (left + pw > window.innerWidth - 8) left = Math.max(8, window.innerWidth - pw - 8); + if (top + ph > window.innerHeight - 8) top = Math.max(8, rect.top - ph - 4); + _mpickPanel.style.top = top + 'px'; + _mpickPanel.style.left = left + 'px'; + } + + function _rerenderMpick() { + const sel = _mpickTarget.querySelector('select'); + const allOpts = [...sel.options]; + const selVals = new Set([...sel.selectedOptions].map(o => o.value)); + const avail = allOpts.filter(o => !selVals.has(o.value)); + const chosen = allOpts.filter(o => selVals.has(o.value)); + document.getElementById('mpick-avail').innerHTML = avail.length + ? avail.map(o => `
${escHtml(o.value)}+
`).join('') + : '
All selected
'; + document.getElementById('mpick-sel').innerHTML = chosen.length + ? chosen.map(o => `
${escHtml(o.value)}
`).join('') + : '
None selected
'; + } + + function _mpickToggle(itemEl, toSelected) { + const val = itemEl.dataset.val; + const sel = _mpickTarget.querySelector('select'); + const opt = [...sel.options].find(o => o.value === val); + if (opt) opt.selected = toSelected; + _updateMpickDisplay(_mpickTarget); + _rerenderMpick(); + } + + function _updateMpickDisplay(wrapper) { + const sel = wrapper.querySelector('select'); + const display = wrapper.querySelector('.mpick-display'); + const selected = [...sel.selectedOptions].map(o => o.value); + if (!selected.length) { + display.innerHTML = '(none)'; + display.title = ''; + return; + } + const MAX = 2; + let html = selected.slice(0, MAX).map(v => `${escHtml(v)}`).join(''); + if (selected.length > MAX) html += `+${selected.length - MAX}`; + display.innerHTML = html; + display.title = selected.join(', '); + } + + function closeMpick() { + if (_mpickPanel) _mpickPanel.style.display = 'none'; + _mpickTarget = null; + } + + function makeMpickHTML(allItems, selectedItems, cls) { + const selSet = new Set(selectedItems); + const opts = allItems.map(v => ``).join(''); + const selected = allItems.filter(v => selSet.has(v)); + const MAX = 2; + let dispHtml; + if (!selected.length) { + dispHtml = '(none)'; + } else { + dispHtml = selected.slice(0, MAX).map(v => `${escHtml(v)}`).join(''); + if (selected.length > MAX) dispHtml += `+${selected.length - MAX}`; + } + const title = escHtml(selected.join(', ')); + return `
${dispHtml}
`; + } + // Highlight sidebar link for the section currently in view const sections = document.querySelectorAll('.section'); const navLinks = document.querySelectorAll('.sidebar-nav a');