Add search/filter bar to large ListChoice checkbox lists
Checkmk's native checkbox-list widget renders one row per item with no grouping or pagination, making rules like "Deploy custom files with agent" hard to work with when only a few items need to be checked among hundreds. Adds a search box, a live selected-count, and a "Selected only" toggle above any such list with 15+ items.
This commit is contained in:
parent
45d6187150
commit
69e872569c
3 changed files with 133 additions and 1 deletions
|
|
@ -5,6 +5,11 @@ All notable changes to **Checkmk SwissKnife** are documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.17.0] - 2026-07-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Search box, live "N / total selected" counter and a "Selected only" toggle above large checkbox lists (Checkmk's native ListChoice widget, e.g. "Deploy custom files with agent"), making it easy to find the few relevant checkboxes among hundreds without scrolling. Purely visual filtering — checkbox state and form submission are unaffected.
|
||||||
|
|
||||||
## [2.16.2] - 2026-07-29
|
## [2.16.2] - 2026-07-29
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ A [Tampermonkey](https://www.tampermonkey.net/) userscript that enhances the Che
|
||||||
| **Auto-check Foreign Changes** | Automatically ticks the "Activate foreign changes" checkbox on the pending changes page | `tryAutoCheckForeignActivation` |
|
| **Auto-check Foreign Changes** | Automatically ticks the "Activate foreign changes" checkbox on the pending changes page | `tryAutoCheckForeignActivation` |
|
||||||
| **Collapse All / Expand All** | Adds two buttons to the menu bar to collapse or expand all accordion sections at once | `addAccordionToggleButtons` |
|
| **Collapse All / Expand All** | Adds two buttons to the menu bar to collapse or expand all accordion sections at once | `addAccordionToggleButtons` |
|
||||||
| **Site Status Filters** | Adds Disabled / Online / Timeout / Not running toggle buttons below "Add connection" to quickly filter the site connections list | `addSitesFilterBar` |
|
| **Site Status Filters** | Adds Disabled / Online / Timeout / Not running toggle buttons below "Add connection" to quickly filter the site connections list | `addSitesFilterBar` |
|
||||||
|
| **ListChoice Filter Bar** | Adds a search box, a "N / total selected" counter and a "Selected only" toggle above large checkbox lists (e.g. "Deploy custom files with agent") | `addListChoiceFilter` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -148,6 +149,13 @@ Only one filter can be active at a time — clicking the active button turns it
|
||||||
|
|
||||||
Applies to: `wato.py?mode=sites` (with or without sidebar).
|
Applies to: `wato.py?mode=sites` (with or without sidebar).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ListChoice Filter Bar
|
||||||
|
Checkmk's native checkbox-list widget (used by rules like "Deploy custom files with agent") renders one row per item with no grouping or pagination — some lists have 100+ entries even though a rule typically only needs 3-4 checked. Adds a search box, a live "N / total selected" counter, and a "Selected only" toggle above any such list with 15+ items. Purely visual — checkbox state and the Save button are unaffected.
|
||||||
|
|
||||||
|
Applies to: any WATO rule editor page containing a large checkbox-list value (with or without sidebar).
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
The script injects enhancements at page load and re-applies them on SPA navigation (Checkmk reloads content inside an `<iframe>` without a full page reload). Each feature is independent — a feature that does not apply to the current page does nothing and stops polling.
|
The script injects enhancements at page load and re-applies them on SPA navigation (Checkmk reloads content inside an `<iframe>` without a full page reload). Each feature is independent — a feature that does not apply to the current page does nothing and stops polling.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Checkmk SwissKnife
|
// @name Checkmk SwissKnife
|
||||||
// @namespace https://luigidacunto.com/
|
// @namespace https://luigidacunto.com/
|
||||||
// @version 2.16.2
|
// @version 2.17.0
|
||||||
// @checkmk 2.3.x - 2.4.x
|
// @checkmk 2.3.x - 2.4.x
|
||||||
// @description Collection of UI improvements for Checkmk WATO. Each fix or enhancement is added here as an independent feature.
|
// @description Collection of UI improvements for Checkmk WATO. Each fix or enhancement is added here as an independent feature.
|
||||||
// @author Luigi D'Acunto
|
// @author Luigi D'Acunto
|
||||||
|
|
@ -1275,6 +1275,7 @@
|
||||||
let attemptsChangelog = 0;
|
let attemptsChangelog = 0;
|
||||||
let attemptsAccToggle = 0;
|
let attemptsAccToggle = 0;
|
||||||
let attemptsSitesFilter = 0;
|
let attemptsSitesFilter = 0;
|
||||||
|
let attemptsListchoice = 0;
|
||||||
|
|
||||||
function tryEnhanceFolderSelect() {
|
function tryEnhanceFolderSelect() {
|
||||||
const iDoc = getWatoDoc(FOLDER_SELECT_ID);
|
const iDoc = getWatoDoc(FOLDER_SELECT_ID);
|
||||||
|
|
@ -1449,6 +1450,117 @@
|
||||||
addAccordionToggleButtons(doc);
|
addAccordionToggleButtons(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FEATURE: ListChoice Filter Bar
|
||||||
|
//
|
||||||
|
// Checkmk's native ListChoice widget (table.listchoice) renders one row
|
||||||
|
// per item with no grouping or pagination. On rules like "Deploy custom
|
||||||
|
// files with agent" this means 100+ checkboxes to scroll through to find
|
||||||
|
// the 3-4 that actually need to be checked. Adds a search box, a live
|
||||||
|
// selected-count, and a "Selected only" toggle above each such table.
|
||||||
|
// Pure visibility filter (hides <tr> via CSS class): never touches
|
||||||
|
// checked state or name attributes, so native form submission on Save
|
||||||
|
// is completely unaffected.
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
const LISTCHOICE_MIN_ITEMS = 15; // below this, scrolling is not a real problem
|
||||||
|
|
||||||
|
function addListChoiceFilter(doc) {
|
||||||
|
if (doc.body.dataset.cmkListchoiceFilter === '1') return;
|
||||||
|
|
||||||
|
const tables = [...doc.querySelectorAll('table.listchoice')]
|
||||||
|
.filter(t => t.querySelectorAll('input[type="checkbox"]').length >= LISTCHOICE_MIN_ITEMS);
|
||||||
|
if (!tables.length) return;
|
||||||
|
|
||||||
|
doc.body.dataset.cmkListchoiceFilter = '1';
|
||||||
|
|
||||||
|
injectStyles(doc, 'cmk-sk-listchoice-filter-styles', `
|
||||||
|
.cmk-sk-lc-bar {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 5px 8px; margin-bottom: 4px;
|
||||||
|
background: rgba(0,0,0,0.25); border: 1px solid #3a3a3a; border-radius: 4px;
|
||||||
|
font-size: 11px; font-family: monospace; color: #999;
|
||||||
|
}
|
||||||
|
.cmk-sk-lc-search {
|
||||||
|
flex: 0 0 220px; padding: 3px 6px; font-size: 11px; font-family: monospace;
|
||||||
|
border-radius: 3px; border: 1px solid #555; background: #1c1c1c; color: #ddd;
|
||||||
|
}
|
||||||
|
.cmk-sk-lc-toggle {
|
||||||
|
cursor: pointer; padding: 3px 10px; border-radius: 3px; border: 1px solid #555;
|
||||||
|
background: #2a2a2a; color: #bbb; font-size: 11px; font-family: monospace;
|
||||||
|
font-weight: bold; letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
.cmk-sk-lc-toggle:hover { background: #383838; }
|
||||||
|
.cmk-sk-lc-toggle.active { background: #1c3320; border-color: #4caf50; color: #4caf50; }
|
||||||
|
.cmk-sk-lc-hidden { display: none !important; }
|
||||||
|
`);
|
||||||
|
|
||||||
|
tables.forEach(table => {
|
||||||
|
const rows = [...table.querySelectorAll('tr')].map(tr => {
|
||||||
|
const cb = tr.querySelector('input[type="checkbox"]');
|
||||||
|
const label = tr.querySelector('label');
|
||||||
|
return cb && label ? { tr, cb, text: label.textContent.trim().toLowerCase() } : null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
if (!rows.length) return;
|
||||||
|
|
||||||
|
const bar = doc.createElement('div');
|
||||||
|
bar.className = 'cmk-sk-lc-bar';
|
||||||
|
|
||||||
|
const search = doc.createElement('input');
|
||||||
|
search.type = 'text';
|
||||||
|
search.className = 'cmk-sk-lc-search';
|
||||||
|
search.placeholder = 'Filter…';
|
||||||
|
|
||||||
|
const toggle = doc.createElement('button');
|
||||||
|
toggle.type = 'button';
|
||||||
|
toggle.className = 'cmk-sk-lc-toggle';
|
||||||
|
toggle.textContent = 'Selected only';
|
||||||
|
|
||||||
|
const count = doc.createElement('span');
|
||||||
|
|
||||||
|
let selectedOnly = false;
|
||||||
|
|
||||||
|
function updateCount() {
|
||||||
|
const selected = rows.filter(r => r.cb.checked).length;
|
||||||
|
count.textContent = `${selected} / ${rows.length} selected`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilter() {
|
||||||
|
const q = search.value.trim().toLowerCase();
|
||||||
|
rows.forEach(r => {
|
||||||
|
const matchesSearch = !q || r.text.includes(q);
|
||||||
|
const matchesToggle = !selectedOnly || r.cb.checked;
|
||||||
|
r.tr.classList.toggle('cmk-sk-lc-hidden', !(matchesSearch && matchesToggle));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
search.addEventListener('input', applyFilter);
|
||||||
|
toggle.addEventListener('click', () => {
|
||||||
|
selectedOnly = !selectedOnly;
|
||||||
|
toggle.classList.toggle('active', selectedOnly);
|
||||||
|
toggle.textContent = selectedOnly ? 'Show all' : 'Selected only';
|
||||||
|
applyFilter();
|
||||||
|
});
|
||||||
|
rows.forEach(r => r.cb.addEventListener('change', updateCount));
|
||||||
|
|
||||||
|
bar.appendChild(search);
|
||||||
|
bar.appendChild(toggle);
|
||||||
|
bar.appendChild(count);
|
||||||
|
table.before(bar);
|
||||||
|
|
||||||
|
updateCount();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryAddListChoiceFilter() {
|
||||||
|
const doc = getTargetDoc();
|
||||||
|
if (!doc || !doc.body) {
|
||||||
|
if (++attemptsListchoice < MAX_ATTEMPTS) setTimeout(tryAddListChoiceFilter, POLL_INTERVAL_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addListChoiceFilter(doc);
|
||||||
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
const iDoc = getWatoDoc(FOLDER_SELECT_ID);
|
const iDoc = getWatoDoc(FOLDER_SELECT_ID);
|
||||||
const mode = getPageMode(iDoc);
|
const mode = getPageMode(iDoc);
|
||||||
|
|
@ -1463,6 +1575,7 @@
|
||||||
attemptsChangelog = 0;
|
attemptsChangelog = 0;
|
||||||
attemptsAccToggle = 0;
|
attemptsAccToggle = 0;
|
||||||
attemptsSitesFilter = 0;
|
attemptsSitesFilter = 0;
|
||||||
|
attemptsListchoice = 0;
|
||||||
// Folder select: self-stops if element not found, always schedules.
|
// Folder select: self-stops if element not found, always schedules.
|
||||||
setTimeout(tryEnhanceFolderSelect, 800);
|
setTimeout(tryEnhanceFolderSelect, 800);
|
||||||
// Accordion: only on pages in ACCORDION_MODES.
|
// Accordion: only on pages in ACCORDION_MODES.
|
||||||
|
|
@ -1485,6 +1598,8 @@
|
||||||
if (!targetMode || targetMode === 'changelog') setTimeout(tryAutoCheckForeignActivation, 500);
|
if (!targetMode || targetMode === 'changelog') setTimeout(tryAutoCheckForeignActivation, 500);
|
||||||
// Sites status filters: only on mode=sites, self-stops if not applicable.
|
// Sites status filters: only on mode=sites, self-stops if not applicable.
|
||||||
if (!targetMode || targetMode === 'sites') setTimeout(tryAddSitesFilterBar, 500);
|
if (!targetMode || targetMode === 'sites') setTimeout(tryAddSitesFilterBar, 500);
|
||||||
|
// ListChoice filter bar: on any page with a large table.listchoice, self-stops if not applicable.
|
||||||
|
setTimeout(tryAddListChoiceFilter, 600);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === 'complete') {
|
if (document.readyState === 'complete') {
|
||||||
|
|
@ -1541,6 +1656,10 @@
|
||||||
attemptsSitesFilter = 0;
|
attemptsSitesFilter = 0;
|
||||||
setTimeout(tryAddSitesFilterBar, 300);
|
setTimeout(tryAddSitesFilterBar, 300);
|
||||||
}
|
}
|
||||||
|
if (tDoc && tDoc.body && !tDoc.body.dataset.cmkListchoiceFilter) {
|
||||||
|
attemptsListchoice = 0;
|
||||||
|
setTimeout(tryAddListChoiceFilter, 300);
|
||||||
|
}
|
||||||
}).observe(document.body, { childList: true, subtree: true });
|
}).observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
// Restart on iframe load (sidebar layout)
|
// Restart on iframe load (sidebar layout)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue