Hi Zabbix Community,
In dense Zabbix 7.0 dashboard environments, two common usability challenges often arise:
To solve this, I developed self-contained bash/Python deployment scripts for Zabbix 7.0 (tested on Zabbix 7.0.31 on Ubuntu 24.04 / Debian). Key Features
Run this single, self-contained deployment script on your Zabbix web server terminal as sudo:
Bash
#!/bin/bash
set -e
FILE_HONEYCOMB="/usr/share/zabbix/widgets/honeycomb/assets/js/class.widget.js"
FILE_SVG="/usr/share/zabbix/widgets/honeycomb/assets/js/class.svghoneycomb.js"
FILE_SIDEBAR="/usr/share/zabbix/js/class.sidebar.js"
FILE_NAV="/usr/share/zabbix/widgets/itemnavigator/assets/js/class.itemnavigator.js"
echo "=== [1/3] Restoring Pristine Backups if Present ==="
for FILE in "$FILE_HONEYCOMB" "$FILE_SVG" "$FILE_SIDEBAR" "$FILE_NAV"; do
if [ -f "$FILE" ] && [ ! -f "${FILE}.og" ]; then
sudo cp "$FILE" "${FILE}.og"
echo "[+] Backup created: ${FILE}.og"
fi
done
echo "=== [2/3] Applying Scoped Click Interceptor & Single Search Bar Patch ==="
sudo python3 -c '
# 1. Patch Item Navigator to create ONLY ONE search input and give it an explicit unique class
file_nav = "/usr/share/zabbix/widgets/itemnavigator/assets/js/class.itemnavigator.js"
with open(file_nav + ".og", "r") as f:
nav_content = f.read()
target_nav = "this.#container.appendChild(this.#navigation_ tree .getContainer());"
replacement_nav = """\t\ttry {
\t\t\tif (!this.#container.querySelector(".zbx-custom-nav-search")) {
\t\t\t\tconst filterDiv = document.createElement("div");
\t\t\t\tfilterDiv.className = "zbx-custom-nav-search-wrapper";
\t\t\t\tfilterDiv.style.cssText = "padding: 4px 6px; background: #fff; border-bottom: 1px solid #dfe4e8; margin-bottom: 4px;";
\t\t\t\tconst filterInput = document.createElement("input");
\t\t\t\tfilterInput.type = "text";
\t\t\t\tfilterInput.placeholder = "Search items...";
\t\t\t\tfilterInput.className = "form-input zbx-custom-nav-search";
\t\t\t\tfilterInput.style.cssText = "width: 100%; height: 24px; padding: 0 6px; font-size: 11px; box-sizing: border-box;";
\t\t\t\tfilterInput.addEventListener("input", (e) => {
\t\t\t\t\tconst val = e.target.value.toLowerCase().trim();
\t\t\t\t\tconst nodes = this.#container.querySelectorAll("li, .tree-item, a, [class*=\x27navigation-tree\x27]");
\t\t\t\t\tnodes.forEach(node => {
\t\t\t\t\t\tconst txt = (node.innerText || node.textContent || "").toLowerCase();
\t\t\t\t\t\tif (val === "" || txt.includes(val)) {
\t\t\t\t\t\t\tnode.style.setProperty("display", "", "important");
\t\t\t\t\t\t} else {
\t\t\t\t\t\t\tnode.style.setProperty("display", "none", "important");
\t\t\t\t\t\t}
\t\t\t\t\t});
\t\t\t\t});
\t\t\t\tfilterDiv.appendChild(filterInput);
\t\t\t\tthis.#container.appendChild(filterDiv);
\t\t\t}
\t\t} catch(err) { console.error("Item Navigator patch error:", err); }
\t\tthis.#container.appendChild(this.#navigation_t ree.getContainer());"""
if target_nav in nav_content:
with open(file_nav, "w") as f:
f.write(nav_content.replace(target_nav, replacement_nav))
print("[+] Scoped Item Navigator search input patched cleanly!")
# 2. Patch Honeycomb SVG to target strictly .zbx-custom-nav-search inputs (skipping left sidebar menu inputs)
file_svg = "/usr/share/zabbix/widgets/honeycomb/assets/js/class.svghoneycomb.js"
with open(file_svg + ".og", "r") as f:
svg_content = f.read()
target_svg = """\t#drawCell(cell) {
\t\tcell
\t\t\t.call(cell => this.#drawLabel(cell))
\t\t\t.on(\x27click\x27, (e, d) => {
\t\t\t\tif (this.selectCell(d.itemid)) {
\t\t\t\t\tthis.#svg.dispatch(CSVGHoneycomb.EVENT_C ELL_CLICK, {
\t\t\t\t\t\tdetail: {
\t\t\t\t\t\t\thostid: d.hostid,
\t\t\t\t\t\t\titemid: d.itemid
\t\t\t\t\t\t}
\t\t\t\t\t});
\t\t\t\t}
\t\t\t})"""
replacement_svg = """\t#drawCell(cell) {
\t\tcell
\t\t\t.style(\x27cursor\x27, \x27pointer\x27)
\t\t\t.call(cell => this.#drawLabel(cell))
\t\t\t.on(\x27click\x27, (e, d) => {
\t\t\t\tif (this.selectCell(d.itemid)) {
\t\t\t\t\tthis.#svg.dispatch(CSVGHoneycomb.EVENT_C ELL_CLICK, {
\t\t\t\t\t\tdetail: {
\t\t\t\t\t\t\thostid: d.hostid,
\t\t\t\t\t\t\titemid: d.itemid
\t\t\t\t\t\t}
\t\t\t\t\t});
\t\t\t\t}
\t\t\t\tsetTimeout(() => {
\t\t\t\t\ttry {
\t\t\t\t\t\tlet vmName = "";
\t\t\t\t\t\tif (d && d.labels && d.labels.primary) {
\t\t\t\t\t\t\tif (d.labels.primary.lines) {
\t\t\t\t\t\t\t\tvmName = Array.from(d.labels.primary.lines).join(" ");
\t\t\t\t\t\t\t} else if (d.labels.primary.text) {
\t\t\t\t\t\t\t\tvmName = d.labels.primary.text;
\t\t\t\t\t\t\t}
\t\t\t\t\t\t}
\t\t\t\t\t\tif (!vmName && e) {
\t\t\t\t\t\t\tconst t = e.currentTarget || e.target;
\t\t\t\t\t\t\tif (t) vmName = t.textContent || t.innerText || "";
\t\t\t\t\t\t}
\t\t\t\t\t\tif (vmName) {
\t\t\t\t\t\t\tvmName = vmName.includes(" - ") ? vmName.split(" - ").pop().trim() : vmName.trim();
\t\t\t\t\t\t\t// Target strictly custom item navigator search inputs
\t\t\t\t\t\t\tconst navInputs = document.querySelectorAll("input.zbx-custom-nav-search");
\t\t\t\t\t\t\tnavInputs.forEach(inp => {
\t\t\t\t\t\t\t\tinp.value = vmName;
\t\t\t\t\t\t\t\tinp.dispatchEvent(new Event("input", { bubbles: true }));
\t\t\t\t\t\t\t\tinp.dispatchEvent(new Event("change", { bubbles: true }));
\t\t\t\t\t\t\t});
\t\t\t\t\t\t}
\t\t\t\t\t} catch(err) { console.error("SVG click handler error:", err); }
\t\t\t\t}, 10);
\t\t\t})"""
if target_svg in svg_content:
with open(file_svg, "w") as f:
f.write(svg_content.replace(target_svg, replacement_svg))
print("[+] Honeycomb SVG click handler updated with explicit class scoping!")
# 3. Patch Sidebar Click-Only Mode
file_sidebar = "/usr/share/zabbix/js/class.sidebar.js"
with open(file_sidebar + ".og", "r") as f:
sidebar_content = f.read()
sidebar_content = sidebar_content.replace("this.on(\x27mouseenter\x2 7, this._events.mouseenter);", "// disabled hover open")
sidebar_content = sidebar_content.replace("item.hasSubmenu() && item.on(\x27mouseenter\x27", "// item.hasSubmenu() && item.on(\x27mouseenter\x27")
sidebar_content = sidebar_content.replace("this.on(\x27mouseenter\x2 7, this._events.cancelExpandSelected);", "// disabled cancel expand")
with open(file_sidebar, "w") as f:
f.write(sidebar_content)
print("[+] Sidebar global click-only patch applied!")
'
echo "=== [3/3] Setting Permissions & Restarting Web Stack ==="
sudo chown www-data:www-data "$FILE_HONEYCOMB" "$FILE_SVG" "$FILE_SIDEBAR" "$FILE_NAV"
sudo chmod 644 "$FILE_HONEYCOMB" "$FILE_SVG" "$FILE_SIDEBAR" "$FILE_NAV"
sudo systemctl restart php*-fpm 2>/dev/null || true
sudo systemctl restart nginx apache2 2>/dev/null || true
echo "=== Deployment Complete ==="
In dense Zabbix 7.0 dashboard environments, two common usability challenges often arise:
- Sidebar Hover Auto-Expansion: The collapsed navigation sidebar automatically expands when hovering your cursor over it, causing unwanted UI layout shifts during routine monitoring.
- Widget Inter-Communication: When clicking a host cell in the Honeycomb widget, there is no native dynamic filtering passed to the Item Navigator widget tree to isolate that host's metrics.
To solve this, I developed self-contained bash/Python deployment scripts for Zabbix 7.0 (tested on Zabbix 7.0.31 on Ubuntu 24.04 / Debian). Key Features
- Pristine Backups: Creates .og original backups of core files before applying any changes.
- Click-Only Sidebar: Completely disables all mouseenter triggers on class.sidebar.js.
- Dynamic Host Filtering: Intercepts Honeycomb cell clicks in class.svghoneycomb.js and targets custom-scoped search inputs in class.itemnavigator.js to isolate host metrics without populating global menu search bars.
Run this single, self-contained deployment script on your Zabbix web server terminal as sudo:
Bash
#!/bin/bash
set -e
FILE_HONEYCOMB="/usr/share/zabbix/widgets/honeycomb/assets/js/class.widget.js"
FILE_SVG="/usr/share/zabbix/widgets/honeycomb/assets/js/class.svghoneycomb.js"
FILE_SIDEBAR="/usr/share/zabbix/js/class.sidebar.js"
FILE_NAV="/usr/share/zabbix/widgets/itemnavigator/assets/js/class.itemnavigator.js"
echo "=== [1/3] Restoring Pristine Backups if Present ==="
for FILE in "$FILE_HONEYCOMB" "$FILE_SVG" "$FILE_SIDEBAR" "$FILE_NAV"; do
if [ -f "$FILE" ] && [ ! -f "${FILE}.og" ]; then
sudo cp "$FILE" "${FILE}.og"
echo "[+] Backup created: ${FILE}.og"
fi
done
echo "=== [2/3] Applying Scoped Click Interceptor & Single Search Bar Patch ==="
sudo python3 -c '
# 1. Patch Item Navigator to create ONLY ONE search input and give it an explicit unique class
file_nav = "/usr/share/zabbix/widgets/itemnavigator/assets/js/class.itemnavigator.js"
with open(file_nav + ".og", "r") as f:
nav_content = f.read()
target_nav = "this.#container.appendChild(this.#navigation_ tree .getContainer());"
replacement_nav = """\t\ttry {
\t\t\tif (!this.#container.querySelector(".zbx-custom-nav-search")) {
\t\t\t\tconst filterDiv = document.createElement("div");
\t\t\t\tfilterDiv.className = "zbx-custom-nav-search-wrapper";
\t\t\t\tfilterDiv.style.cssText = "padding: 4px 6px; background: #fff; border-bottom: 1px solid #dfe4e8; margin-bottom: 4px;";
\t\t\t\tconst filterInput = document.createElement("input");
\t\t\t\tfilterInput.type = "text";
\t\t\t\tfilterInput.placeholder = "Search items...";
\t\t\t\tfilterInput.className = "form-input zbx-custom-nav-search";
\t\t\t\tfilterInput.style.cssText = "width: 100%; height: 24px; padding: 0 6px; font-size: 11px; box-sizing: border-box;";
\t\t\t\tfilterInput.addEventListener("input", (e) => {
\t\t\t\t\tconst val = e.target.value.toLowerCase().trim();
\t\t\t\t\tconst nodes = this.#container.querySelectorAll("li, .tree-item, a, [class*=\x27navigation-tree\x27]");
\t\t\t\t\tnodes.forEach(node => {
\t\t\t\t\t\tconst txt = (node.innerText || node.textContent || "").toLowerCase();
\t\t\t\t\t\tif (val === "" || txt.includes(val)) {
\t\t\t\t\t\t\tnode.style.setProperty("display", "", "important");
\t\t\t\t\t\t} else {
\t\t\t\t\t\t\tnode.style.setProperty("display", "none", "important");
\t\t\t\t\t\t}
\t\t\t\t\t});
\t\t\t\t});
\t\t\t\tfilterDiv.appendChild(filterInput);
\t\t\t\tthis.#container.appendChild(filterDiv);
\t\t\t}
\t\t} catch(err) { console.error("Item Navigator patch error:", err); }
\t\tthis.#container.appendChild(this.#navigation_t ree.getContainer());"""
if target_nav in nav_content:
with open(file_nav, "w") as f:
f.write(nav_content.replace(target_nav, replacement_nav))
print("[+] Scoped Item Navigator search input patched cleanly!")
# 2. Patch Honeycomb SVG to target strictly .zbx-custom-nav-search inputs (skipping left sidebar menu inputs)
file_svg = "/usr/share/zabbix/widgets/honeycomb/assets/js/class.svghoneycomb.js"
with open(file_svg + ".og", "r") as f:
svg_content = f.read()
target_svg = """\t#drawCell(cell) {
\t\tcell
\t\t\t.call(cell => this.#drawLabel(cell))
\t\t\t.on(\x27click\x27, (e, d) => {
\t\t\t\tif (this.selectCell(d.itemid)) {
\t\t\t\t\tthis.#svg.dispatch(CSVGHoneycomb.EVENT_C ELL_CLICK, {
\t\t\t\t\t\tdetail: {
\t\t\t\t\t\t\thostid: d.hostid,
\t\t\t\t\t\t\titemid: d.itemid
\t\t\t\t\t\t}
\t\t\t\t\t});
\t\t\t\t}
\t\t\t})"""
replacement_svg = """\t#drawCell(cell) {
\t\tcell
\t\t\t.style(\x27cursor\x27, \x27pointer\x27)
\t\t\t.call(cell => this.#drawLabel(cell))
\t\t\t.on(\x27click\x27, (e, d) => {
\t\t\t\tif (this.selectCell(d.itemid)) {
\t\t\t\t\tthis.#svg.dispatch(CSVGHoneycomb.EVENT_C ELL_CLICK, {
\t\t\t\t\t\tdetail: {
\t\t\t\t\t\t\thostid: d.hostid,
\t\t\t\t\t\t\titemid: d.itemid
\t\t\t\t\t\t}
\t\t\t\t\t});
\t\t\t\t}
\t\t\t\tsetTimeout(() => {
\t\t\t\t\ttry {
\t\t\t\t\t\tlet vmName = "";
\t\t\t\t\t\tif (d && d.labels && d.labels.primary) {
\t\t\t\t\t\t\tif (d.labels.primary.lines) {
\t\t\t\t\t\t\t\tvmName = Array.from(d.labels.primary.lines).join(" ");
\t\t\t\t\t\t\t} else if (d.labels.primary.text) {
\t\t\t\t\t\t\t\tvmName = d.labels.primary.text;
\t\t\t\t\t\t\t}
\t\t\t\t\t\t}
\t\t\t\t\t\tif (!vmName && e) {
\t\t\t\t\t\t\tconst t = e.currentTarget || e.target;
\t\t\t\t\t\t\tif (t) vmName = t.textContent || t.innerText || "";
\t\t\t\t\t\t}
\t\t\t\t\t\tif (vmName) {
\t\t\t\t\t\t\tvmName = vmName.includes(" - ") ? vmName.split(" - ").pop().trim() : vmName.trim();
\t\t\t\t\t\t\t// Target strictly custom item navigator search inputs
\t\t\t\t\t\t\tconst navInputs = document.querySelectorAll("input.zbx-custom-nav-search");
\t\t\t\t\t\t\tnavInputs.forEach(inp => {
\t\t\t\t\t\t\t\tinp.value = vmName;
\t\t\t\t\t\t\t\tinp.dispatchEvent(new Event("input", { bubbles: true }));
\t\t\t\t\t\t\t\tinp.dispatchEvent(new Event("change", { bubbles: true }));
\t\t\t\t\t\t\t});
\t\t\t\t\t\t}
\t\t\t\t\t} catch(err) { console.error("SVG click handler error:", err); }
\t\t\t\t}, 10);
\t\t\t})"""
if target_svg in svg_content:
with open(file_svg, "w") as f:
f.write(svg_content.replace(target_svg, replacement_svg))
print("[+] Honeycomb SVG click handler updated with explicit class scoping!")
# 3. Patch Sidebar Click-Only Mode
file_sidebar = "/usr/share/zabbix/js/class.sidebar.js"
with open(file_sidebar + ".og", "r") as f:
sidebar_content = f.read()
sidebar_content = sidebar_content.replace("this.on(\x27mouseenter\x2 7, this._events.mouseenter);", "// disabled hover open")
sidebar_content = sidebar_content.replace("item.hasSubmenu() && item.on(\x27mouseenter\x27", "// item.hasSubmenu() && item.on(\x27mouseenter\x27")
sidebar_content = sidebar_content.replace("this.on(\x27mouseenter\x2 7, this._events.cancelExpandSelected);", "// disabled cancel expand")
with open(file_sidebar, "w") as f:
f.write(sidebar_content)
print("[+] Sidebar global click-only patch applied!")
'
echo "=== [3/3] Setting Permissions & Restarting Web Stack ==="
sudo chown www-data:www-data "$FILE_HONEYCOMB" "$FILE_SVG" "$FILE_SIDEBAR" "$FILE_NAV"
sudo chmod 644 "$FILE_HONEYCOMB" "$FILE_SVG" "$FILE_SIDEBAR" "$FILE_NAV"
sudo systemctl restart php*-fpm 2>/dev/null || true
sudo systemctl restart nginx apache2 2>/dev/null || true
echo "=== Deployment Complete ==="
Comment