Files
cacti/include/js/navigationBox.tableLayout.js
Thomas Vincent 08d0f94f24 Fix develop client behavior, popup isolation and PHP method visibility (#8292)
* fix(core): correct client state and declare public method contracts

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(security): isolate Midwinter external homepage popup

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(client): isolate menu state and simplify validated JavaScript guards

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(auth): use certificate expiry truth in rendering assertions

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* refactor(js): remove unused local storage while retaining initializer calls

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(js): encode inline translated values through the shared JSON boundary

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* style(js): wrap encoded translation literals without changing messages

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(client): remove unused checkbox reads from graph resizing

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* refactor(client): declare repeated function locals once

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(ui): run column width regression in browser CI

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-11 21:15:39 -04:00

294 lines
12 KiB
JavaScript

/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDTool-based Graphing Solution |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
// ensure namespace exists
var midwinter = midwinter || {};
midwinter.navigationBox = midwinter.navigationBox || {};
midwinter.navigationBox.table = {
// Internal constants for decoupling
_selectors: {
table: '.cactiTable',
header: 'tr.tableHeader',
sortInfo: 'div.sortinfo'
},
/**
* cyrb53 (c) 2018 bryc (github.com/bryc)
* License: Public domain. Attribution appreciated.
* A fast and simple 53-bit string hash function with decent collision resistance.
* Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
*/
_hash: function(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for(let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
},
// internal helper to notify the Cacti theme (e.g. that content is empty)
_notifyState: function($box, hasContent) {
const event = new CustomEvent('mdw:pluginStateUpdate', {
detail: {
helper: $box.data('helper'),
plugin: 'table',
hasContent: hasContent
},
bubbles: true // ensure we are reaching the document level
});
$box[0].dispatchEvent(event);
},
_executeReset: function(tableHash) {
if (!tableHash) return;
const storage = Storages.localStorage;
let storageData = storage.get('cactiTable_' + tableHash);
let settings = (typeof storageData === 'string') ? JSON.parse(storageData) : storageData;
if (settings && settings[1]) {
// remove all hide-classes from the actual table
const $table = $(`table[data-table="${tableHash}"]`);
$table.removeClass(settings[0]);
// reset the hidden-classes array
settings[0] = [];
// set all columns to visible (1) in the metadata
settings[1].forEach(col => {
col[4] = 1;
});
// sync back to storage
storage.set('cactiTable_' + tableHash, JSON.stringify(settings));
// update the UI Checkboxes in the Box
const $box = $(`[data-helper="displayOptions"]`);
$box.find('input[type="checkbox"]').prop('checked', true);
$box.find('#mdw-columns-reset').addClass('inactive');
console.log(`[TablePlugin] Reset executed for table ${tableHash}`);
}
},
/**
* Internal toggle logic
*/
_executeToggle: function(tableHash, columnIndex, isChecked) {
const columnClass = `no-col${columnIndex}`;
const storage = Storages.localStorage;
let storageData = storage.get('cactiTable_' + tableHash);
let settings = (typeof storageData === 'string') ? JSON.parse(storageData) : storageData;
if (!settings) return;
const $table = $(`table[data-table="${tableHash}"]`);
// update visibility flag in metadata [index, name, title, hideable, visible]
settings[1][columnIndex - 1][4] = Number(isChecked);
if (!isChecked) {
// hide column: add class to table and storage list
if (!settings[0].includes(columnClass)) settings[0].push(columnClass);
$table.addClass(columnClass);
} else {
// show column: remove class
const index = settings[0].indexOf(columnClass);
if (index !== -1) settings[0].splice(index, 1);
$table.removeClass(columnClass);
}
storage.set('cactiTable_' + tableHash, JSON.stringify(settings));
// update reset button state within the box
$(`[data-helper="displayOptions"] #mdw-columns-reset`).toggleClass('inactive', settings[0].length === 0);
},
/**
* returns the configuration for the table layout box
* @param {object} overrides - optional configuration overwrites
*/
getDefaultConfig: function(overrides = {}) {
const base = 'midwinter.navigationBox.table';
const defaults = {
title: 'Table Layout',
helper: 'displayOptions',
contentLoader: `${base}.content`,
controlLoader: `${base}.control`,
initCallback: `${base}.init`,
contextMenuItems: {
'table' : {
name: "Table Options",
items: {
"reset": {
name: "Reset Columns",
icon: "ti ti-rotate",
callback: `${base}.reset`
}
}
}
},
isRefreshable: true,
};
return $.extend(true, {}, defaults, overrides);
},
/**
* returns the html for the control area (Reset Button)
*/
control: function(args) {
const $box = Array.isArray(args) ? args[0] : args;
const helper = ($box && typeof $box.data === 'function') ? $box.data('helper') : 'displayOptions';
return `
<div class="navBox-control-group">
<button class="navBox-control-btn compact_nav_icon hint--bottom hint--rounded"
data-action="reset_table" data-helper="${helper}"
aria-label="reset columns">
<i class="ti ti-rotate"></i>
</button>
</div>`;
},
/**
* generates the column checkbox list
*/
content: function($box) {
const ns = midwinter.navigationBox.table;
const $headerRow = $(`${ns._selectors.header}:has(th:nth-of-type(2))`).first();
if (!$headerRow.length) return '<div class="navBox-empty">No compatible table found</div>';
const $currentTable = $headerRow.closest(ns._selectors.table);
const tableID = $currentTable.attr('id') || 'no-id';
// build header string for hashing
let cHeaderStr = '';
$headerRow.find('th').each(function() {
const $th = $(this);
const cName = $th.hasClass('sortable') ? $th.find(ns._selectors.sortInfo).attr('sort-column') : 'n/a';
cHeaderStr += cName;
});
const tableHash = this._hash(window.location.pathname + tableID + cHeaderStr);
$currentTable.attr('data-table', tableHash);
// storage handling
const storageKey = 'cactiTable_' + tableHash; // Updated prefix
let storageData = Storages.localStorage.get(storageKey);
let settings = (typeof storageData === 'string') ? JSON.parse(storageData) : storageData;
// init storage if missing
if (!settings || !settings[1]) {
let cArray = [];
$headerRow.find('th').each(function(index) {
const $th = $(this);
let cIndex = index + 1;
let cName = 'n/a', cTitle = 'n/a', cHideable = 0;
if ($th.hasClass('sortable')) {
cName = $th.find(ns._selectors.sortInfo).attr('sort-column');
cTitle = $th.find('i:first').parent().text().trim();
cHideable = 1;
} else if (!$th.hasClass('tableSubHeaderCheckbox')) {
cTitle = $th.text().trim();
cHideable = 1;
}
cArray.push([cIndex, cName, cTitle, cHideable, 1]);
});
settings = [[], cArray, (typeof sessionLocale !== 'undefined' ? sessionLocale : 'en-US')];
Storages.localStorage.set(storageKey, JSON.stringify(settings));
}
// 3. Render HTML
let html = '';
const columns = settings[1]; // Correct index for column definitions
columns.forEach((col) => {
const [index, name, title, hideable, visible] = col;
if (hideable) {
const inputID = `cacti_col_${index}`;
html += `
<div class="navBox-item-row">
<span>${title}</span>
<div class="navBox-item-action">
<input type="checkbox" id="${inputID}" class="plugin-table-toggle"
data-table="${tableHash}" data-column="${index}"
${visible ? 'checked' : ''}
${index === 1 ? 'disabled' : ''}>
<label for="${inputID}"></label>
</div>
</div>`;
}
});
return html || '<div class="navBox-empty">No hideable columns</div>';
},
/**
* initializes events and visibility
*/
init: function($box) {
if (!$box || !$box.length) return;
$box.data('helper');
const ns = midwinter.navigationBox.table;
// bind toggle checkboxes
$box.rebind('change', '.plugin-table-toggle', (e) => {
const $el = $(e.currentTarget);
ns._executeToggle($el.data('table'), $el.data('column'), e.target.checked);
});
// bind reset control button
$box.rebind('click', '[data-action="reset_table"]', (e) => {
const tableHash = $box.find('input[data-table]').first().data('table');
ns._executeReset(tableHash);
});
// handle navigation button visibility via mdw.actions
const hasContent= $box.find('input[type="checkbox"]').length > 0;
ns._notifyState($box, hasContent);
},
reset: function(key, opt) {
const ns = midwinter.navigationBox.table;
const $box = opt.$trigger.closest('div[class*="ConsoleNavigationBox"]')
const tableHash = $box.find('input[data-table]').first().attr('data-table');
ns._executeReset(tableHash);
},
};
// register plugin
midwinter.navigationBox.registerPlugin('midwinter.navigationBox.table');