From 4dabc359323b35c8a755a926a1f7491b85a4416c Mon Sep 17 00:00:00 2001 From: Luigi D'Acunto Date: Mon, 28 Sep 2026 11:54:57 +0200 Subject: [PATCH] Add free-text filter on ruleset pages, exclusive with Relevant only --- CHANGELOG.md | 5 ++ README.md | 8 +++ user_script/checkmk_swissknife.user.js | 81 +++++++++++++++++++++++++- 3 files changed, 93 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fdff83..368d7f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.23.0] - 2026-09-28 + +### Added +- Free-text filter on every rule list page (`mode=edit_ruleset`), with or without host/service context: a search box above the list hides the rules (and folders left empty) whose row text doesn't contain the typed string, with a "N / total rules shown" counter. It is mutually exclusive with the "Relevant only" toggle: typing turns "Relevant only" off, and turning "Relevant only" on clears the search box. + ## [2.22.4] - 2026-09-24 ### Added diff --git a/README.md b/README.md index aa47d73..dcfd1fc 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ A [Tampermonkey](https://www.tampermonkey.net/) userscript that enhances the Che | **Ineffective Rule Highlight** | Replaces the hard-to-spot hyphen icon on ineffective rules with a visible ⚠ badge and amber row border | `highlightIneffectiveRules` | | **Rule Match Status** | Colors each rule row green (match) or dimmed (no match) when a ruleset is opened with host/service context | `highlightRuleMatchStatus` | | **Relevant Only Filter** | Adds a "Relevant only" toggle above the rule list to hide all non-matching and non-highlighted rows | `addRulesetFilterToggle` | +| **Ruleset Free-Text Filter** | Adds a search box above the rule list on any ruleset page to hide rules and folders whose row text doesn't contain the typed string; mutually exclusive with "Relevant only" | `addRulesetTextFilter` | | **Extra Column — Host Actions** | Inserts an Extra column in monitoring tables with per-host buttons: Service Discovery, copy FQDN, copy short hostname, copy IP | `addInventoryButtons` | | **Monitor Button in WATO Folder** | Adds a green eye icon next to each active host in WATO folder listings to open its monitoring view in a new tab | `addWatoFolderMonitorButtons` | | **Configure in WATO Menu** | Adds a host-selector dropdown and Open button to the monitoring view menu bar for bulk-opening hosts in WATO | `addViewWatoMenu` | @@ -82,6 +83,13 @@ Applies to: `mode=edit_ruleset` (when at least one `✓ match` or ineffective ro --- +### Ruleset Free-Text Filter +A search box above the rule list hides every rule whose row text (conditions, value, description…) doesn't contain the typed string (case-insensitive) and every folder left with no visible rule; a counter shows "N / total rules shown". It is mutually exclusive with "Relevant only": typing in the box turns "Relevant only" off, and turning "Relevant only" on clears the box. + +Applies to: every `mode=edit_ruleset` page, with or without host/service context. + +--- + ### Extra Column with Host Actions Adds a dedicated **Extra** column before the Host column in all monitoring view tables. Each row gets up to four icon buttons: diff --git a/user_script/checkmk_swissknife.user.js b/user_script/checkmk_swissknife.user.js index 42c2007..49a1afe 100644 --- a/user_script/checkmk_swissknife.user.js +++ b/user_script/checkmk_swissknife.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Checkmk SwissKnife // @namespace https://luigidacunto.com/ -// @version 2.22.4 +// @version 2.23.0 // @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. // @author Luigi D'Acunto @@ -805,6 +805,12 @@ const isActive = doc.body.classList.toggle('cmk-sk-filter-active'); btn.classList.toggle('active', isActive); btn.textContent = isActive ? 'Show all' : 'Relevant only'; + // Mutually exclusive with the free-text filter + const text = doc.getElementById('cmk-sk-text-filter-input'); + if (isActive && text && text.value) { + text.value = ''; + text.dispatchEvent(new Event('input')); + } }); bar.appendChild(btn); @@ -815,6 +821,78 @@ } + // ========================================================================= + // FEATURE: Ruleset Free-Text Filter + // + // Search box on every edit_ruleset page: hides the rules (and folders left + // empty) whose row text does not contain the typed string. Mutually + // exclusive with the "Relevant only" toggle (typing turns it off, turning + // it on clears the box). + // ========================================================================= + + function addRulesetTextFilter(doc) { + if (doc.body.dataset.cmkTextFilter === '1') return; + const rows = doc.querySelectorAll('tr.data'); + if (!rows.length) return; + doc.body.dataset.cmkTextFilter = '1'; + + injectStyles(doc, 'cmk-sk-text-filter-styles', ` + #cmk-sk-text-filter-bar { + display: flex; + align-items: center; + gap: 10px; + padding: 5px 10px; + margin: 6px 0 4px 0; + background: rgba(0,0,0,0.25); + border: 1px solid #3a3a3a; + border-radius: 4px; + font-size: 11px; + color: #999; + font-family: monospace; + } + #cmk-sk-text-filter-input { width: 280px; font-size: 11px; font-family: monospace; } + .cmk-sk-text-hidden { display: none !important; } + `); + + const bar = doc.createElement('div'); + bar.id = 'cmk-sk-text-filter-bar'; + + const input = doc.createElement('input'); + input.id = 'cmk-sk-text-filter-input'; + input.type = 'search'; + input.placeholder = 'Filter rules by text…'; + + const info = doc.createElement('span'); + + input.addEventListener('input', () => { + const q = input.value.trim().toLowerCase(); + if (q) { + // Mutually exclusive with "Relevant only" + doc.body.classList.remove('cmk-sk-filter-active'); + const relBtn = doc.getElementById('cmk-sk-filter-toggle-btn'); + if (relBtn) { relBtn.classList.remove('active'); relBtn.textContent = 'Relevant only'; } + } + let shown = 0; + rows.forEach(row => { + const hide = !!q && !row.textContent.toLowerCase().includes(q); + row.classList.toggle('cmk-sk-text-hidden', hide); + if (!hide) shown++; + }); + doc.querySelectorAll('div.foldable_wrapper').forEach(w => { + const empty = !!q && !w.querySelector('tr.data:not(.cmk-sk-text-hidden)'); + w.classList.toggle('cmk-sk-text-hidden', empty); + }); + info.textContent = q ? `${shown} / ${rows.length} rules shown` : ''; + }); + + bar.appendChild(input); + bar.appendChild(info); + + const anchor = doc.querySelector('div.foldable_wrapper') || doc.querySelector('div.wato') || rows[0].closest('table'); + if (anchor) anchor.before(bar); + } + + // ========================================================================= // FEATURE: Inventory Button on view.py // @@ -1970,6 +2048,7 @@ highlightIneffectiveRules(doc); highlightRuleMatchStatus(doc); addRulesetFilterToggle(doc); + addRulesetTextFilter(doc); } function tryAddInventoryButtons() {