/* ** Zabbix ** Copyright (C) 2001-2023 Zabbix SIA ** ** 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. ** ** You should have received a copy of the GNU General Public License ** along with this program; if not, write to the Free Software ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. **/ const TABFILTERITEM_EVENT_COLLAPSE = 'collapse.item.tabfilter'; const TABFILTERITEM_EVENT_EXPAND = 'expand.item.tabfilter'; const TABFILTERITEM_EVENT_SELECT = 'select.item.tabfilter'; const TABFILTERITEM_EVENT_RENDER = 'render.item.tabfilter'; const TABFILTERITEM_EVENT_URLSET = 'urlset.item.tabfilter'; const TABFILTERITEM_EVENT_UPDATE = 'update.item.tabfilter'; const TABFILTERITEM_EVENT_DELETE = 'delete.item.tabfilter'; const TABFILTERITEM_EVENT_ACTION = 'action.item.tabfilter'; const TABFILTERITEM_STYLE_UNSAVED = 'unsaved'; const TABFILTERITEM_STYLE_BTN_EDIT = 'tabfilter-edit'; const TABFILTERITEM_STYLE_SELECTED = 'selected'; const TABFILTERITEM_STYLE_EXPANDED = 'expanded'; const TABFILTERITEM_STYLE_DISABLED = 'disabled'; const TABFILTERITEM_STYLE_FOCUSED = 'focused'; class CTabFilterItem extends CBaseComponent { constructor(target, options) { super(target); this._parent = options.parent || null; this._idx_namespace = options.idx_namespace; this._index = options.index; this._unsaved = false; this._unsaved_indicator = null; this._content_container = options.container; this._data = options.data || {}; this._template = options.template; this._expanded = options.expanded; this._support_custom_time = options.support_custom_time; this._template_rendered = false; this._src_url = null; this._apply_url = null; this._subfilters_expanded = []; this.init(); this.registerEvents(); } init() { if (this._expanded) { this.renderContentTemplate(); this.updateApplyUrl(); } if (this._data.filter_show_counter) { this.setCounter(''); } } /** * Initialize indicator DOM node for tab unsaved state. */ initUnsavedIndicator() { let green_dot = document.createElement('span'); green_dot.setAttribute('data-indicator-value', '1'); green_dot.setAttribute('data-indicator', 'mark'); green_dot.classList.toggle('display-none', !this._unsaved); this._target.appendChild(green_dot); this._unsaved_indicator = green_dot; } /** * Set results counter value. * * @param {int} value Results counter value. */ setCounter(value) { this._target.setAttribute('data-counter', value); } /** * Get results counter value. */ getCounter() { return this._target.getAttribute('data-counter'); } /** * Remove results counter value. */ removeCounter() { this._target.removeAttribute('data-counter'); } /** * Update item label counter when "show counter" is enabled otherwise will remove counter attribute. * * @param {string} value Value to be shown in item label when "show counter" is enabled. */ updateCounter(value) { if (this._data.filter_show_counter) { this.setCounter(value); } else { this.removeCounter(); } } /** * Return item state of "show counter". */ hasCounter() { return parseInt(this._data.filter_show_counter) == 1; } /** * Return filter form HTML element. */ getForm() { return this._content_container.querySelector('form'); } /** * Render tab template with data. Fire TABFILTERITEM_EVENT_RENDER on template container binding this as event this. */ renderContentTemplate() { if (this._template && !this._template_rendered) { this._template_rendered = true; this._content_container.innerHTML = (new Template(this._template.innerHTML)).evaluate(this._data); this._template.dispatchEvent(new CustomEvent(TABFILTERITEM_EVENT_RENDER, {detail: this})); } } /** * Open tab filter configuration popup. * * @param {object} params Object of params to be passed to ajax call when opening popup. * @param {Node} trigger_element DOM element to broadcast popup update or delete event. */ openPropertiesDialog(params, trigger_element) { let defaults = { idx: this._idx_namespace, idx2: this._index, filter_show_counter: this._data.filter_show_counter, filter_custom_time: this._data.filter_custom_time, tabfilter_from: this._data.from || '', tabfilter_to: this._data.to || '', support_custom_time: +this._support_custom_time }; if (this._data.filter_name !== '') { defaults.filter_name = this._data.filter_name; } this.updateUnsavedState(); return PopUp('popup.tabfilter.edit', { ...defaults, ...params }, {dialogueid: 'tabfilter_dialogue', trigger_element} ); } /** * Add gear icon and bind click event. */ addActionIcons() { if (this._target.parentNode.querySelector('.' + TABFILTERITEM_STYLE_BTN_EDIT)) { return; } let edit = document.createElement('a'); edit.classList.add(ZBX_STYLE_BTN_ICON, ZBX_ICON_COG_FILLED, TABFILTERITEM_STYLE_BTN_EDIT); edit.addEventListener('click', () => this.openPropertiesDialog({}, this._target)); this._target.parentNode.appendChild(edit); } /** * Remove gear icon HTMLElement. */ removeActionIcons() { let icon = this._target.parentNode.querySelector('.' + TABFILTERITEM_STYLE_BTN_EDIT); if (icon) { icon.remove(); } } /** * Get selected state of item. * * @return {boolean} */ isSelected() { return this._target.parentNode.classList.contains(TABFILTERITEM_STYLE_SELECTED); } /** * Set browser focus to filter label element. */ setFocused() { this._target.focus(); } /** * Set selected state of item. */ setSelected() { this._target.parentNode.classList.add(TABFILTERITEM_STYLE_SELECTED); this.renderContentTemplate(); if (this._data.filter_configurable) { this.addActionIcons(); } if (this._template) { this._template.dispatchEvent(new CustomEvent(TABFILTERITEM_EVENT_SELECT, {detail: this})); } } /** * Remove selected state of item. */ removeSelected() { this._target.parentNode.classList.remove(TABFILTERITEM_STYLE_SELECTED); if (this._data.filter_configurable) { this.removeActionIcons(); } } /** * Set expanded state of item and it content container, render content from template if it was not rendered yet. * Fire TABFILTERITEM_EVENT_EXPAND event on template. */ setExpanded() { let item_template = this._template || this._content_container.querySelector('[data-template]'); this._target.parentNode.classList.add(TABFILTERITEM_STYLE_EXPANDED); if (item_template instanceof HTMLElement && !this._expanded) { item_template.dispatchEvent(new CustomEvent(TABFILTERITEM_EVENT_EXPAND, {detail: this})); } this._expanded = true; this._content_container.classList.remove('display-none'); } /** * Remove expanded state of item and it content. Fire TABFILTERITEM_EVENT_COLLAPSE on item template. */ removeExpanded() { let item_template = this._template || this._content_container.querySelector('[data-template]'); this._expanded = false; this._target.parentNode.classList.remove(TABFILTERITEM_STYLE_EXPANDED); this._content_container.classList.add('display-none'); if (item_template instanceof HTMLElement) { item_template.dispatchEvent(new CustomEvent(TABFILTERITEM_EVENT_COLLAPSE, {detail: this})); } } /** * Delete item, clean up all related HTMLElement nodes. */ delete() { this._target.parentNode.remove(); this._content_container.remove(); } /** * Toggle item is item selectable or not. * * @param {boolean} state Selectable when true. */ setDisabled(state) { this.toggleClass(TABFILTERITEM_STYLE_DISABLED, state); this._target.parentNode.classList.toggle(TABFILTERITEM_STYLE_DISABLED, state); } /** * Check if item have custom time interval. * * @return {boolean} */ hasCustomTime() { return !!this._data.filter_custom_time; } /** * Update tab filter configuration: name, show_counter, custom_time. Set browser URL according new values. * * @param {object} data Updated tab properties object. */ update(data) { var form = this.getForm(), fields = { filter_name: form.querySelector('[name="filter_name"]'), filter_show_counter: form.querySelector('[name="filter_show_counter"]'), filter_custom_time: form.querySelector('[name="filter_custom_time"]') }; if (data.filter_custom_time) { this._data.from = data.from; this._data.to = data.to; } Object.keys(fields).forEach((key) => { this._data[key] = data[key]; if (fields[key] instanceof HTMLElement) { fields[key].value = data[key]; } }); if (data.filter_show_counter) { this.setCounter(''); } else { this.removeCounter(); } if (!this._unsaved) { this.updateApplyUrl(); } this._target.text = data.filter_name; this.initUnsavedIndicator(); this.setBrowserLocationToApplyUrl(); } /** * Get filter parameters as URLSearchParams object, defining value of unchecked checkboxes equal to * 'unchecked-value' attribute value. * * @param {boolean} preserve_page Parameter for resetting page. * * @return {URLSearchParams} */ getFilterParams(preserve_page = true) { let form = this.getForm(), params = null; const TAG_OPERATOR_EXISTS = '4'; const TAG_OPERATOR_NOT_EXISTS = '5'; if (form instanceof HTMLFormElement) { var form_data = new FormData(form); // Unset tag filter values for exists/not exists tag filters. for (const [key, value] of form_data.entries()) { let check_tag = key.match(/tags\[(\d+)\]\[operator\]/); if (check_tag && (value === TAG_OPERATOR_EXISTS || value === TAG_OPERATOR_NOT_EXISTS)) { form_data.set('tags['+check_tag[1]+'][value]', ''); } } params = new URLSearchParams(form_data); for (const checkbox of form.querySelectorAll('input[type="checkbox"][unchecked-value]')) { if (!checkbox.checked) { params.set(checkbox.getAttribute('name'), checkbox.getAttribute('unchecked-value')); } } if (this._data.filter_custom_time) { params.set('from', this._data.from); params.set('to', this._data.to); } if (preserve_page && 'page' in this._data && this._data.page > 1) { params.set('page', this._data.page); } } return params; } /** * Set browser location URL according to passed values. Argument 'action' from already set URL is preserved. * Create TABFILTER_EVENT_URLSET event with detail.target equal instance of CTabFilter. * * @param {URLSearchParams} search_params Filter field values to be set in URL. */ setBrowserLocation(search_params) { let url = new Curl(''); search_params.set('action', url.getArgument('action')); url.query = search_params.toString(); url.formatArguments(); history.replaceState(history.state, '', url.getUrl()); this.fire(TABFILTERITEM_EVENT_URLSET); } /** * Set argument to URL used for data pooling (_apply_url) and URL used to track unsaved changes (_src_url). * Allow to change argument without affecting "unsaved" state of filter. * * @param {string} name Argument name. * @param {string} value Argument value. */ setUrlArgument(name, value) { let apply_url = new URLSearchParams(this._apply_url || (this.getFilterParams()).toString()), src_url = new URLSearchParams(this._src_url); apply_url.set(name, value); src_url.set(name, value); this._apply_url = apply_url.toString(); this._src_url = src_url.toString(); } /** * Keep filter tab results request parameters. */ updateApplyUrl(preserve_page = true) { this._apply_url = (this.getFilterParams(preserve_page)).toString(); } /** * Request filter results for fields defined before last 'Apply' being used. */ setBrowserLocationToApplyUrl() { if (this._apply_url === null) { this.updateApplyUrl(); } this.setBrowserLocation(new URLSearchParams(this._apply_url)); } /** * Checks difference between original form values and to be posted values. * Updates this._unsaved according to check results */ updateUnsavedState(preserve_page = true) { let search_params = this.getFilterParams(preserve_page), src_query = new URLSearchParams(this._src_url), ignore_fields = ['filter_name', 'filter_custom_time', 'filter_show_counter', 'from', 'to', 'action', 'page']; if (search_params === null || !this._data.filter_configurable) { // Not templated tabs does not contain form fields, no need to update unsaved state. return; } for (const field of ignore_fields) { src_query.delete(field); search_params.delete(field); } src_query.sort(); search_params.sort(); this._unsaved = (src_query.toString() !== search_params.toString()); if (this._unsaved_indicator) { this._unsaved_indicator.classList.toggle('display-none', !this._unsaved); } } /** * Reset item unsaved state. Set this._src_url to filter parameters. */ resetUnsavedState() { let src_query = this.getFilterParams(); if (src_query === null) { return; } if (src_query.get('filter_custom_time') !== '1') { src_query.delete('from'); src_query.delete('to'); } src_query.delete('action'); src_query.sort(); this._src_url = src_query.toString(); if (this._unsaved_indicator) { this._unsaved_indicator.classList.add('display-none'); } } /** * Initialize _src_url property from item rendered form fields values. */ initUnsavedState() { if (this._src_url === null) { this.resetUnsavedState(); } } /** * Unset selected subfilters. */ emptySubfilter() { [...this.getForm().elements] .filter(el => el.name.substr(0, 10) === 'subfilter_') .forEach(el => el.remove()); } /** * Shorthand function to check if subfilter has given value. * * @param {string} key Subfilter parameter name. * @param {string} value Subfilter parameter value. * * @return {bool} */ hasSubfilter(key, value) { return Boolean([...this.getForm().elements].filter(el => (el.name === key && el.value === value)).length); } /** * Set new subfilter field. * * @param {string} key Subfilter parameter name. * @param {string} value Subfilter parameter value. */ setSubfilter(key, value) { value = String(value); if (!this.hasSubfilter(key, value)) { const el = document.createElement('input'); el.type = 'hidden'; el.name = key; el.value = value; this.getForm().appendChild(el); } } /** * Remove some of existing subfilter field. * * @param {string} key Subfilter parameter name. * @param {string} value Subfilter parameter value. */ unsetSubfilter(key, value) { value = String(value); if (this.hasSubfilter(key, value)) { [...this.getForm().elements] .filter(el => (el.name === key && el.value === value)) .forEach(el => el.remove()); } } /** * Set expanded subfilter name. */ setExpandedSubfilters(name) { return this._subfilters_expanded.push(name); } /** * Retrieve expanded subfilter names. * * @returns {array} */ getExpandedSubfilters() { return this._subfilters_expanded; } /** * Unset expanded subfilters. */ unsetExpandedSubfilters() { this._subfilters_expanded = []; } registerEvents() { this._events = { click: () => { if (this.hasClass(TABFILTERITEM_STYLE_DISABLED)) { return; } this.setFocused(); this.fire(TABFILTERITEM_EVENT_SELECT); }, expand: () => { this.setExpanded(); if (this._src_url === null) { this.resetUnsavedState(); } }, collapse: () => { this.removeExpanded(); }, focusin: () => { this._target.parentNode.classList.add(TABFILTERITEM_STYLE_FOCUSED); }, focusout: () => { this._target.parentNode.classList.remove(TABFILTERITEM_STYLE_FOCUSED); } } this .on(TABFILTERITEM_EVENT_EXPAND, this._events.expand) .on(TABFILTERITEM_EVENT_COLLAPSE, this._events.collapse) .on('focusin', this._events.focusin) .on('focusout', this._events.focusout) .on('click', this._events.click); } }