You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
625 lines
16 KiB
625 lines
16 KiB
/*
|
|
** 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);
|
|
}
|
|
}
|