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.
1405 lines
38 KiB
1405 lines
38 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.
|
|
**/
|
|
|
|
|
|
/*
|
|
* Widget view modes: whether to display the header statically or on mouse hovering (configurable on the widget form).
|
|
*/
|
|
|
|
const ZBX_WIDGET_VIEW_MODE_NORMAL = 0;
|
|
const ZBX_WIDGET_VIEW_MODE_HIDDEN_HEADER = 1;
|
|
|
|
/*
|
|
* Widget states, managed by the dashboard page.
|
|
*/
|
|
|
|
// Initial state of widget: the widget has never yet been displayed on the dashboard page.
|
|
const WIDGET_STATE_INITIAL = 'initial';
|
|
|
|
// Active state of widget: the widget is being displayed on the active dashboard page and is updating periodically.
|
|
const WIDGET_STATE_ACTIVE = 'active';
|
|
|
|
// Inactive state of widget: the widget has been active recently, but is currently hidden on an inactive dashboard page.
|
|
const WIDGET_STATE_INACTIVE = 'inactive';
|
|
|
|
// Destroyed state of widget: the widget has been deleted from the dashboard page.
|
|
const WIDGET_STATE_DESTROYED = 'destroyed';
|
|
|
|
/*
|
|
* Events thrown by widgets to inform the dashboard page about user interaction with the widget, which may impact the
|
|
* dashboard page and other widgets.
|
|
*/
|
|
|
|
// Widget edit event: informs the dashboard page to enter the editing mode.
|
|
const WIDGET_EVENT_EDIT = 'widget-edit';
|
|
|
|
// Widget actions event: informs the dashboard page to display the widget actions popup menu.
|
|
const WIDGET_EVENT_ACTIONS = 'widget-actions';
|
|
|
|
// Widget enter event: informs the dashboard page to focus the widget and un-focus other widgets.
|
|
const WIDGET_EVENT_ENTER = 'widget-enter';
|
|
|
|
// Widget leave event: informs the dashboard page to un-focus the widget.
|
|
const WIDGET_EVENT_LEAVE = 'widget-leave';
|
|
|
|
// Widget before-update event: thrown by a widget immediately before the update cycle has started.
|
|
const WIDGET_EVENT_BEFORE_UPDATE = 'widget-before-update';
|
|
|
|
// Widget after-update event: thrown by a widget immediately after the update cycle has finished.
|
|
const WIDGET_EVENT_AFTER_UPDATE = 'widget-after-update';
|
|
|
|
// Widget copy event: informs the dashboard page to copy the widget to the local storage.
|
|
const WIDGET_EVENT_COPY = 'widget-copy';
|
|
|
|
// Widget paste event: informs the dashboard page to paste the stored widget over the current one.
|
|
const WIDGET_EVENT_PASTE = 'widget-paste';
|
|
|
|
// Widget delete event: informs the dashboard page to delete the widget.
|
|
const WIDGET_EVENT_DELETE = 'widget-delete';
|
|
|
|
/*
|
|
* The base class of all dashboard widgets. Depending on widget needs, it can be instantiated directly or be extended.
|
|
*/
|
|
class CWidgetBase {
|
|
|
|
/**
|
|
* Widget constructor. Invoked by a dashboard page.
|
|
*
|
|
* @param {string} type Widget type ("id" field of the manifest.json).
|
|
* @param {string} name Widget name to display in the header.
|
|
* @param {number} view_mode One of ZBX_WIDGET_VIEW_MODE_NORMAL, ZBX_WIDGET_VIEW_MODE_HIDDEN_HEADER.
|
|
* @param {Object} fields Widget field values (widget configuration data).
|
|
*
|
|
* @param {Object} defaults Widget type defaults.
|
|
* {string} defaults.name Default name to display in the header, if no custom name given.
|
|
* {Object} defaults.size Default size to use when creating new widgets.
|
|
* {number} defaults.size.width Default width.
|
|
* {number} defaults.size.height Default height
|
|
* {string} defaults.js_class JavaScript class name.
|
|
*
|
|
* @param {string|null} widgetid Widget ID stored in the database, or null for new widgets.
|
|
*
|
|
* @param {Object|null} pos Position and size of the widget (in dashboard coordinates).
|
|
* {number} pos.x Horizontal position.
|
|
* {number} pos.y Vertical position.
|
|
* {number} pos.width Widget width.
|
|
* {number} pos.height Widget height.
|
|
*
|
|
* @param {boolean} is_new Create a visual zoom effect when adding new widgets.
|
|
* @param {number} rf_rate Update cycle rate (refresh rate) in seconds. Supported values: 0 (no
|
|
* refresh), 10, 30, 60, 120, 600 or 900 seconds.
|
|
* @param {Object} dashboard Essential data of the dashboard object.
|
|
* {string|null} dashboard.dashboardid Dashboard ID.
|
|
* {string|null} dashboard.templateid Template ID (used for template and host dashboards).
|
|
*
|
|
* @param {Object} dashboard_page Essential data of the dashboard page object.
|
|
* {string} dashboard_page.unique_id Run-time, unique ID of the dashboard page.
|
|
*
|
|
* @param {number} cell_width Dashboard page cell width in percentage.
|
|
* @param {number} cell_height Dashboard page cell height in pixels.
|
|
* @param {number} min_rows Minimum number of dashboard cell rows per single widget.
|
|
* @param {boolean} is_editable Whether to display the "Edit" button.
|
|
* @param {boolean} is_edit_mode Whether the widget is being created in the editing mode.
|
|
* @param {boolean} can_edit_dashboards Whether the user has access to creating and editing dashboards.
|
|
*
|
|
* @param {Object|null} time_period Selected time period (if widget.use_time_selector in manifest.json is
|
|
* set to true in any of the loaded widgets), or null.
|
|
* {string} time_period.from Relative time of period start (like "now-1h").
|
|
* {number} time_period.from_ts Timestamp of period start.
|
|
* {string} time_period.to Relative time of period end (like "now").
|
|
* {number} time_period.to_ts Timestamp of period end.
|
|
*
|
|
* @param {string|null} dynamic_hostid ID of the dynamically selected host on a dashboard (if any of the
|
|
* widgets has the "dynamic" checkbox field configured and checked in the
|
|
* widget configuration), or null.
|
|
* @param {string|null} csrf_token CSRF token for AJAX requests.
|
|
* @param {string} unique_id Run-time, unique ID of the widget.
|
|
*/
|
|
constructor({
|
|
type,
|
|
name = '',
|
|
view_mode,
|
|
fields,
|
|
defaults,
|
|
widgetid = null,
|
|
pos = null,
|
|
is_new,
|
|
rf_rate,
|
|
dashboard,
|
|
dashboard_page,
|
|
cell_width,
|
|
cell_height,
|
|
min_rows,
|
|
is_editable,
|
|
is_edit_mode,
|
|
can_edit_dashboards,
|
|
time_period,
|
|
dynamic_hostid,
|
|
csrf_token = null,
|
|
unique_id
|
|
}) {
|
|
this._target = document.createElement('div');
|
|
|
|
this._type = type;
|
|
this._name = name;
|
|
this._view_mode = view_mode;
|
|
this._fields = fields;
|
|
this._defaults = defaults;
|
|
this._widgetid = widgetid;
|
|
this._pos = pos;
|
|
this._is_new = is_new;
|
|
this._rf_rate = rf_rate;
|
|
|
|
this._dashboard = {
|
|
templateid: dashboard.templateid,
|
|
dashboardid: dashboard.dashboardid
|
|
};
|
|
|
|
this._dashboard_page = {
|
|
unique_id: dashboard_page.unique_id
|
|
};
|
|
|
|
this._cell_width = cell_width;
|
|
this._cell_height = cell_height;
|
|
this._min_rows = min_rows;
|
|
this._is_editable = is_editable;
|
|
this._is_edit_mode = is_edit_mode;
|
|
this._can_edit_dashboards = can_edit_dashboards;
|
|
this._time_period = time_period;
|
|
this._dynamic_hostid = dynamic_hostid;
|
|
this._csrf_token = csrf_token;
|
|
this._unique_id = unique_id;
|
|
|
|
this._init();
|
|
}
|
|
|
|
/**
|
|
* Define initial data. Invoked once, upon instantiation.
|
|
*/
|
|
_init() {
|
|
this._css_classes = {
|
|
actions: 'dashboard-grid-widget-actions',
|
|
container: 'dashboard-grid-widget-container',
|
|
contents: 'dashboard-grid-widget-contents',
|
|
messages: 'dashboard-grid-widget-messages',
|
|
body: 'dashboard-grid-widget-body',
|
|
debug: 'dashboard-grid-widget-debug',
|
|
focus: 'dashboard-grid-widget-focus',
|
|
header: 'dashboard-grid-widget-header',
|
|
hidden_header: 'dashboard-grid-widget-hidden-header',
|
|
mask: 'dashboard-grid-widget-mask',
|
|
root: 'dashboard-grid-widget',
|
|
resize_handle: 'ui-resizable-handle'
|
|
};
|
|
|
|
this._state = WIDGET_STATE_INITIAL;
|
|
|
|
this._contents_size = {};
|
|
this._update_timeout_id = null;
|
|
this._update_interval_id = null;
|
|
this._update_abort_controller = null;
|
|
this._is_updating_paused = false;
|
|
this._update_retry_sec = 3;
|
|
this._show_preloader_asap = true;
|
|
this._resizable_handles = [];
|
|
|
|
this._hide_preloader_animation_frame = null;
|
|
|
|
this._events = {};
|
|
|
|
this.onInitialize();
|
|
}
|
|
|
|
/**
|
|
* Stub method redefined in class.widget.js.
|
|
*/
|
|
onInitialize() {
|
|
}
|
|
|
|
/**
|
|
* Get current state.
|
|
*
|
|
* @returns {string} WIDGET_STATE_INITIAL | WIDGET_STATE_INACTIVE | WIDGET_STATE_ACTIVE | WIDGET_STATE_DESTROYED.
|
|
*/
|
|
getState() {
|
|
return this._state;
|
|
}
|
|
|
|
// Logical state control methods.
|
|
|
|
/**
|
|
* Create widget view (HTML objects). Invoked once, before the first activation of the dashboard page.
|
|
*/
|
|
start() {
|
|
if (this._state !== WIDGET_STATE_INITIAL) {
|
|
throw new Error('Unsupported state change.');
|
|
}
|
|
|
|
this._state = WIDGET_STATE_INACTIVE;
|
|
|
|
this._makeView();
|
|
|
|
if (this._pos !== null) {
|
|
this.setPos(this._pos);
|
|
}
|
|
|
|
this._registerEvents();
|
|
|
|
this.onStart();
|
|
}
|
|
|
|
/**
|
|
* Stub method redefined in class.widget.js.
|
|
*/
|
|
onStart() {
|
|
}
|
|
|
|
/**
|
|
* Start processing DOM events and start updating immediately. Invoked on each activation of the dashboard page.
|
|
*/
|
|
activate() {
|
|
if (this._state !== WIDGET_STATE_INACTIVE) {
|
|
throw new Error('Unsupported state change.');
|
|
}
|
|
|
|
this._state = WIDGET_STATE_ACTIVE;
|
|
|
|
this.onActivate();
|
|
|
|
this._activateEvents();
|
|
this._startUpdating();
|
|
}
|
|
|
|
/**
|
|
* Stub method redefined in class.widget.js.
|
|
*/
|
|
onActivate() {
|
|
}
|
|
|
|
/**
|
|
* Stop processing DOM events and stop updating immediately. Invoked on each deactivation of the dashboard page.
|
|
*/
|
|
deactivate() {
|
|
if (this._state !== WIDGET_STATE_ACTIVE) {
|
|
throw new Error('Unsupported state change.');
|
|
}
|
|
|
|
this._state = WIDGET_STATE_INACTIVE;
|
|
|
|
this.onDeactivate();
|
|
|
|
if (this._is_new) {
|
|
this._is_new = false;
|
|
this._target.classList.remove('new-widget');
|
|
}
|
|
|
|
this._deactivateEvents();
|
|
this._stopUpdating();
|
|
}
|
|
|
|
/**
|
|
* Stub method redefined in class.widget.js.
|
|
*/
|
|
onDeactivate() {
|
|
}
|
|
|
|
/**
|
|
* Destroy the widget which has already been started.
|
|
*
|
|
* Invoked once, when the widget or the dashboard page gets deleted.
|
|
*/
|
|
destroy() {
|
|
if (this._state === WIDGET_STATE_ACTIVE) {
|
|
this.deactivate();
|
|
}
|
|
|
|
if (this._state !== WIDGET_STATE_INACTIVE) {
|
|
throw new Error('Unsupported state change.');
|
|
}
|
|
|
|
this._state = WIDGET_STATE_DESTROYED;
|
|
|
|
this.onDestroy();
|
|
}
|
|
|
|
/**
|
|
* Stub method redefined in class.widget.js.
|
|
*/
|
|
onDestroy() {
|
|
}
|
|
|
|
// External events management methods.
|
|
|
|
/**
|
|
* Check whether the widget is in editing mode.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
isEditMode() {
|
|
return this._is_edit_mode;
|
|
}
|
|
|
|
/**
|
|
* Set widget to editing mode. This is one-way action.
|
|
*/
|
|
setEditMode() {
|
|
this._is_edit_mode = true;
|
|
|
|
if (this._state === WIDGET_STATE_ACTIVE) {
|
|
this._stopUpdating({do_abort: false});
|
|
}
|
|
|
|
this._target.classList.add('ui-draggable', 'ui-resizable');
|
|
|
|
this.onEdit();
|
|
}
|
|
|
|
/**
|
|
* Stub method redefined in class.widget.js.
|
|
*/
|
|
onEdit() {
|
|
}
|
|
|
|
/**
|
|
* Check whether the widget supports dynamic hosts (overriding the host selected in the configuration).
|
|
*
|
|
* The host selection control will be displayed on the dashboard, if any of the loaded widgets has such support.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
supportsDynamicHosts() {
|
|
return this._fields.dynamic === '1';
|
|
}
|
|
|
|
/**
|
|
* Get the dynamic host currently in use. Invoked if the widget supports dynamic hosts.
|
|
*
|
|
* @returns {string|null}
|
|
*/
|
|
getDynamicHost() {
|
|
return this._dynamic_hostid;
|
|
}
|
|
|
|
/**
|
|
* Set the dynamic host. Invoked if the widget supports dynamic hosts.
|
|
*
|
|
* @param {string|null} dynamic_hostid
|
|
*/
|
|
setDynamicHost(dynamic_hostid) {
|
|
this._dynamic_hostid = dynamic_hostid;
|
|
|
|
if (this._state === WIDGET_STATE_ACTIVE) {
|
|
this._startUpdating();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the time period selected in the time selector of the dashboard.
|
|
*
|
|
* @param {Object|null} time_period Selected time period (if widget.use_time_selector in manifest.json is set to
|
|
* true in any of the loaded widgets), or null.
|
|
* {string} time_period.from Relative time of period start (like "now-1h").
|
|
* {number} time_period.from_ts Timestamp of period start.
|
|
* {string} time_period.to Relative time of period end (like "now").
|
|
* {number} time_period.to_ts Timestamp of period end.
|
|
*/
|
|
setTimePeriod(time_period) {
|
|
this._time_period = time_period;
|
|
}
|
|
|
|
/**
|
|
* Find whether the widget is currently entered (focused) my mouse or keyboard. Only one widget can be entered at a
|
|
* time.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
isEntered() {
|
|
return this._target.classList.contains(this._css_classes.focus);
|
|
}
|
|
|
|
/**
|
|
* Enter (focus) the widget. Caused by mouse hovering or keyboard navigation. Only one widget can be entered at a
|
|
* time.
|
|
*/
|
|
enter() {
|
|
if (this._is_edit_mode) {
|
|
this._addResizeHandles();
|
|
}
|
|
|
|
this._target.classList.add(this._css_classes.focus);
|
|
}
|
|
|
|
/**
|
|
* Remove focus from the widget. Caused by mouse hovering or keyboard navigation.
|
|
*/
|
|
leave() {
|
|
if (this._is_edit_mode) {
|
|
this._removeResizeHandles();
|
|
}
|
|
|
|
if (this._header.contains(document.activeElement)) {
|
|
document.activeElement.blur();
|
|
}
|
|
|
|
this._target.classList.remove(this._css_classes.focus);
|
|
}
|
|
|
|
/**
|
|
* Get number of header lines the widget displays when focused.
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
getNumHeaderLines() {
|
|
return this._view_mode === ZBX_WIDGET_VIEW_MODE_HIDDEN_HEADER ? 1 : 0;
|
|
}
|
|
|
|
/**
|
|
* Is widget currently being resized?
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
_isResizing() {
|
|
return this._target.classList.contains('ui-resizable-resizing');
|
|
}
|
|
|
|
/**
|
|
* Set widget resizing state.
|
|
*
|
|
* @param {boolean} is_resizing
|
|
*/
|
|
setResizing(is_resizing) {
|
|
this._target.classList.toggle('ui-resizable-resizing', is_resizing);
|
|
}
|
|
|
|
/**
|
|
* Is widget currently being dragged?
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
_isDragging() {
|
|
return this._target.classList.contains('ui-draggable-dragging');
|
|
}
|
|
|
|
/**
|
|
* Set widget dragging state.
|
|
*
|
|
* @param {boolean} is_dragging
|
|
*/
|
|
setDragging(is_dragging) {
|
|
this._target.classList.toggle('ui-draggable-dragging', is_dragging);
|
|
}
|
|
|
|
/**
|
|
* Are there context menus open or hints displayed for the widget?
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
isUserInteracting() {
|
|
return this._target
|
|
.querySelectorAll('[data-expanded="true"], [aria-expanded="true"][aria-haspopup="true"]').length > 0;
|
|
}
|
|
|
|
/**
|
|
* Take whatever action is required on each resize event of the widget contents' container.
|
|
*/
|
|
resize() {
|
|
this.onResize();
|
|
}
|
|
|
|
/**
|
|
* Stub method redefined in class.widget.js.
|
|
*/
|
|
onResize() {
|
|
}
|
|
|
|
// Data interface methods.
|
|
|
|
/**
|
|
* Get the unique ID of the widget (runtime, dynamically generated).
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
getUniqueId() {
|
|
return this._unique_id;
|
|
}
|
|
|
|
/**
|
|
* Get the widget type ("id" field of the manifest.json).
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
getType() {
|
|
return this._type;
|
|
}
|
|
|
|
/**
|
|
* Get custom widget name (can be empty).
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
getName() {
|
|
return this._name;
|
|
}
|
|
|
|
/**
|
|
* Set custom widget name and, if not empty, display it in the header. Otherwise, display the default name.
|
|
*
|
|
* @param {string} name
|
|
*/
|
|
_setName(name) {
|
|
this._name = name;
|
|
this._setHeaderName(this._name !== '' ? this._name : this._defaults.name);
|
|
}
|
|
|
|
/**
|
|
* Get widget name to be displayed in the header (either custom, if not empty, or the default one).
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
getHeaderName() {
|
|
return this._name !== '' ? this._name : this._defaults.name;
|
|
}
|
|
|
|
/**
|
|
* Display the specified widget name in the header.
|
|
*
|
|
* @param {string} name
|
|
*/
|
|
_setHeaderName(name) {
|
|
if (this._state !== WIDGET_STATE_INITIAL) {
|
|
this._header.querySelector('h4').textContent = name;
|
|
}
|
|
}
|
|
|
|
// Data interface methods.
|
|
|
|
/**
|
|
* Check if widget header is set to be always displayed or displayed only when the widget is entered (focused).
|
|
*
|
|
* @returns {number} One of ZBX_WIDGET_VIEW_MODE_HIDDEN_HEADER, ZBX_WIDGET_VIEW_MODE_NORMAL.
|
|
*/
|
|
getViewMode() {
|
|
return this._view_mode;
|
|
}
|
|
|
|
/**
|
|
* Set widget header to be either always displayed or displayed only when the widget is entered (focused).
|
|
*
|
|
* @param {number} view_mode One of ZBX_WIDGET_VIEW_MODE_HIDDEN_HEADER, ZBX_WIDGET_VIEW_MODE_NORMAL.
|
|
*/
|
|
_setViewMode(view_mode) {
|
|
if (this._view_mode !== view_mode) {
|
|
this._view_mode = view_mode;
|
|
this._target.classList.toggle(this._css_classes.hidden_header,
|
|
this._view_mode === ZBX_WIDGET_VIEW_MODE_HIDDEN_HEADER
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get widget field values (widget configuration data).
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
getFields() {
|
|
return this._fields;
|
|
}
|
|
|
|
/**
|
|
* Set widget field values (widget configuration data).
|
|
*
|
|
* @param {Object} fields
|
|
*/
|
|
_setFields(fields) {
|
|
this._fields = fields;
|
|
}
|
|
|
|
/**
|
|
* Get widget ID.
|
|
*
|
|
* @returns {string|null} Widget ID stored in the database, or null for new widgets.
|
|
*/
|
|
getWidgetId() {
|
|
return this._widgetid;
|
|
}
|
|
|
|
/**
|
|
* Stub method redefined in class.widget.js.
|
|
*/
|
|
hasPadding() {
|
|
}
|
|
|
|
/**
|
|
* Update padding of the widget contents' container. Invoked when widget properties have changed.
|
|
*/
|
|
_updatePadding() {
|
|
if (this._state !== WIDGET_STATE_INITIAL) {
|
|
this._contents.classList.toggle('no-padding', !this.hasPadding());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update widget properties and start updating immediately.
|
|
*
|
|
* @param {string|undefined} name Widget name to display in the header.
|
|
* @param {number|undefined} view_mode One of ZBX_WIDGET_VIEW_MODE_NORMAL, ZBX_WIDGET_VIEW_MODE_HIDDEN_HEADER.
|
|
* @param {Object|undefined} fields Widget field values (widget configuration data).
|
|
*/
|
|
updateProperties({name, view_mode, fields}) {
|
|
if (name !== undefined) {
|
|
this._setName(name);
|
|
}
|
|
|
|
if (view_mode !== undefined) {
|
|
this._setViewMode(view_mode);
|
|
}
|
|
|
|
if (fields !== undefined) {
|
|
this._setFields(fields);
|
|
}
|
|
|
|
this._updatePadding();
|
|
|
|
this._show_preloader_asap = true;
|
|
|
|
if (this._state === WIDGET_STATE_ACTIVE) {
|
|
this._startUpdating();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get update cycle rate (refresh rate) in seconds.
|
|
*
|
|
* @returns {number} Supported values: 0 (no refresh), 10, 30, 60, 120, 600 or 900 seconds.
|
|
*/
|
|
getRfRate() {
|
|
return this._rf_rate;
|
|
}
|
|
|
|
/**
|
|
* Set update cycle rate (refresh rate) in seconds.
|
|
*
|
|
* @param {number} rf_rate Supported values: 0 (no refresh), 10, 30, 60, 120, 600 or 900 seconds.
|
|
*/
|
|
_setRfRate(rf_rate) {
|
|
this._rf_rate = rf_rate;
|
|
|
|
if (this._widgetid !== null) {
|
|
const curl = new Curl('zabbix.php');
|
|
|
|
curl.setArgument('action', 'dashboard.widget.rfrate');
|
|
curl.setArgument('_csrf_token', this._csrf_token);
|
|
|
|
fetch(curl.getUrl(), {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({widgetid: this._widgetid, rf_rate})
|
|
})
|
|
.then((response) => response.json())
|
|
.then((response) => {
|
|
if ('error' in response) {
|
|
throw {error: response.error};
|
|
}
|
|
})
|
|
.catch((exception) => {
|
|
console.log('Could not update widget refresh rate:', exception);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get widget data for purpose of copying the widget.
|
|
*
|
|
* @param {boolean} is_single_copy Whether copying a single widget or copying a whole dashboard page.
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
getDataCopy({is_single_copy}) {
|
|
const data = {
|
|
type: this._type,
|
|
name: this._name,
|
|
view_mode: this._view_mode,
|
|
fields: this._fields,
|
|
pos: is_single_copy
|
|
? {
|
|
width: this._pos.width,
|
|
height: this._pos.height
|
|
}
|
|
: this._pos,
|
|
rf_rate: this._rf_rate
|
|
};
|
|
|
|
if (is_single_copy) {
|
|
data.dashboard = {
|
|
templateid: this._dashboard.templateid
|
|
};
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Get widget data for storing it in the database.
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
save() {
|
|
return {
|
|
widgetid: this._widgetid ?? undefined,
|
|
pos: this._pos,
|
|
type: this._type,
|
|
name: this._name,
|
|
view_mode: this._view_mode,
|
|
fields: Object.keys(this._fields).length > 0 ? this._fields : undefined
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get context menu to display when actions button is clicked.
|
|
*
|
|
* @param {boolean} can_paste_widget Whether a copied widget is ready to be pasted over the current one.
|
|
*
|
|
* @returns {Object[]}
|
|
*/
|
|
getActionsContextMenu({can_paste_widget}) {
|
|
let menu = [];
|
|
let menu_actions = [];
|
|
|
|
if (this._can_edit_dashboards && (this._dashboard.templateid === null || this._dynamic_hostid === null)) {
|
|
menu_actions.push({
|
|
label: t('Copy'),
|
|
clickCallback: () => this.fire(WIDGET_EVENT_COPY)
|
|
});
|
|
}
|
|
|
|
if (this._is_edit_mode) {
|
|
menu_actions.push({
|
|
label: t('Paste'),
|
|
disabled: can_paste_widget === false,
|
|
clickCallback: () => this.fire(WIDGET_EVENT_PASTE)
|
|
});
|
|
|
|
menu_actions.push({
|
|
label: t('Delete'),
|
|
clickCallback: () => this.fire(WIDGET_EVENT_DELETE)
|
|
});
|
|
}
|
|
|
|
if (menu_actions.length) {
|
|
menu.push({
|
|
label: t('Actions'),
|
|
items: menu_actions
|
|
});
|
|
}
|
|
|
|
if (!this._is_edit_mode) {
|
|
const refresh_interval_section = {
|
|
label: t('Refresh interval'),
|
|
items: []
|
|
};
|
|
|
|
const rf_rates = new Map([
|
|
[0, t('No refresh')],
|
|
[10, t('10 seconds')],
|
|
[30, t('30 seconds')],
|
|
[60, t('1 minute')],
|
|
[120, t('2 minutes')],
|
|
[600, t('10 minutes')],
|
|
[900, t('15 minutes')]
|
|
]);
|
|
|
|
for (const [rf_rate, label] of rf_rates.entries()) {
|
|
refresh_interval_section.items.push({
|
|
label: label,
|
|
selected: rf_rate === this._rf_rate,
|
|
clickCallback: () => {
|
|
this._setRfRate(rf_rate);
|
|
|
|
if (this._state === WIDGET_STATE_ACTIVE) {
|
|
if (this._rf_rate > 0) {
|
|
this._startUpdating();
|
|
}
|
|
else {
|
|
this._stopUpdating();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
menu.push(refresh_interval_section);
|
|
}
|
|
|
|
return menu;
|
|
}
|
|
|
|
// Content updating methods.
|
|
|
|
/**
|
|
* Start updating the widget. Invoked on activation of the widget or when the update is required immediately.
|
|
*
|
|
* @param {number} delay_sec Delay seconds before the update.
|
|
* @param {boolean|null} do_update_once Whether the widget is required to update once.
|
|
*/
|
|
_startUpdating(delay_sec = 0, {do_update_once = null} = {}) {
|
|
if (do_update_once === null) {
|
|
do_update_once = this._is_edit_mode;
|
|
}
|
|
|
|
this._stopUpdating({do_abort: false});
|
|
|
|
if (delay_sec > 0) {
|
|
this._update_timeout_id = setTimeout(() => {
|
|
this._update_timeout_id = null;
|
|
this._startUpdating(0, {do_update_once});
|
|
}, delay_sec * 1000);
|
|
}
|
|
else {
|
|
if (!do_update_once && this._rf_rate > 0) {
|
|
this._update_interval_id = setInterval(() => {
|
|
this._update(do_update_once);
|
|
}, this._rf_rate * 1000);
|
|
}
|
|
|
|
this._update(do_update_once);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop updating the widget. Invoked on deactivation of the widget or when the update is required to restart.
|
|
*
|
|
* @param {boolean} do_abort Whether to abort the active update request.
|
|
*/
|
|
_stopUpdating({do_abort = true} = {}) {
|
|
if (this._update_timeout_id !== null) {
|
|
clearTimeout(this._update_timeout_id);
|
|
this._update_timeout_id = null;
|
|
}
|
|
|
|
if (this._update_interval_id !== null) {
|
|
clearInterval(this._update_interval_id);
|
|
this._update_interval_id = null;
|
|
}
|
|
|
|
if (do_abort && this._update_abort_controller !== null) {
|
|
this._update_abort_controller.abort();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pause updating the widget whether the widget is active.
|
|
*/
|
|
_pauseUpdating() {
|
|
this._is_updating_paused = true;
|
|
}
|
|
|
|
/**
|
|
* Resume updating the widget whether the widget is active.
|
|
*/
|
|
_resumeUpdating() {
|
|
this._is_updating_paused = false;
|
|
}
|
|
|
|
/**
|
|
* Organize the update cycle of the widget.
|
|
*
|
|
* @param {boolean} do_update_once Whether the widget is required to update once.
|
|
*/
|
|
_update(do_update_once) {
|
|
if (this._update_abort_controller !== null || this._is_updating_paused || this.isUserInteracting()) {
|
|
this._startUpdating(1, {do_update_once});
|
|
|
|
return;
|
|
}
|
|
|
|
this.fire(WIDGET_EVENT_BEFORE_UPDATE);
|
|
|
|
this._contents_size = this._getContentsSize();
|
|
|
|
this._update_abort_controller = new AbortController();
|
|
|
|
if (this._show_preloader_asap) {
|
|
this._show_preloader_asap = false;
|
|
this._showPreloader();
|
|
}
|
|
else {
|
|
this._schedulePreloader();
|
|
}
|
|
|
|
new Promise((resolve) => resolve(this.promiseUpdate()))
|
|
.then(() => this._hidePreloader())
|
|
.catch((exception) => {
|
|
console.log('Could not update widget:', exception);
|
|
|
|
if (this._update_abort_controller.signal.aborted) {
|
|
this._hidePreloader();
|
|
}
|
|
else {
|
|
this._startUpdating(this._update_retry_sec, {do_update_once});
|
|
}
|
|
})
|
|
.finally(() => {
|
|
this._update_abort_controller = null;
|
|
|
|
this.fire(WIDGET_EVENT_AFTER_UPDATE);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stub method redefined in class.widget.js.
|
|
*/
|
|
promiseUpdate() {
|
|
}
|
|
|
|
// Widget view methods.
|
|
|
|
/**
|
|
* Get main HTML container of the widget.
|
|
*
|
|
* @returns {HTMLDivElement}
|
|
*/
|
|
getView() {
|
|
return this._target;
|
|
}
|
|
|
|
/**
|
|
* Get CSS class name for the specified container or state.
|
|
*
|
|
* @param {string} name Container or state name.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
getCssClass(name) {
|
|
return this._css_classes[name];
|
|
}
|
|
|
|
/**
|
|
* Get position and size of the widget (in dashboard coordinates).
|
|
*
|
|
* @returns {{x: number, y: number, width: number, height: number}|null}
|
|
*/
|
|
getPos() {
|
|
return this._pos;
|
|
}
|
|
|
|
/**
|
|
* Set size and position the widget on the dashboard page.
|
|
*
|
|
* @param {Object} pos Position and size of the widget (in dashboard coordinates).
|
|
* {number} pos.x Horizontal position.
|
|
* {number} pos.y Vertical position.
|
|
* {number} pos.width Widget width.
|
|
* {number} pos.height Widget height.
|
|
*
|
|
* @param {boolean} is_managed Whether physically setting the position and size is managed from the outside.
|
|
*/
|
|
setPos(pos, {is_managed = false} = {}) {
|
|
this._pos = pos;
|
|
|
|
if (!is_managed) {
|
|
this._target.style.left = `${this._cell_width * this._pos.x}%`;
|
|
this._target.style.top = `${this._cell_height * this._pos.y}px`;
|
|
this._target.style.width = `${this._cell_width * this._pos.width}%`;
|
|
this._target.style.height = `${this._cell_height * this._pos.height}px`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate which of the four sides are affected by the resize handle.
|
|
*
|
|
* @param {HTMLElement} resize_handle One of eight dots by which the widget can be resized in editing mode.
|
|
*
|
|
* @returns {{top: boolean, left: boolean, bottom: boolean, right: boolean}}
|
|
*/
|
|
getResizeHandleSides(resize_handle) {
|
|
return {
|
|
top: resize_handle.classList.contains('ui-resizable-nw')
|
|
|| resize_handle.classList.contains('ui-resizable-n')
|
|
|| resize_handle.classList.contains('ui-resizable-ne'),
|
|
right: resize_handle.classList.contains('ui-resizable-ne')
|
|
|| resize_handle.classList.contains('ui-resizable-e')
|
|
|| resize_handle.classList.contains('ui-resizable-se'),
|
|
bottom: resize_handle.classList.contains('ui-resizable-se')
|
|
|| resize_handle.classList.contains('ui-resizable-s')
|
|
|| resize_handle.classList.contains('ui-resizable-sw'),
|
|
left: resize_handle.classList.contains('ui-resizable-sw')
|
|
|| resize_handle.classList.contains('ui-resizable-w')
|
|
|| resize_handle.classList.contains('ui-resizable-nw')
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Add eight resize handles to the widget by which the widget can be resized in editing mode. Invoked when the
|
|
* widget is entered (focused).
|
|
*/
|
|
_addResizeHandles() {
|
|
this._resizable_handles = {};
|
|
|
|
for (const direction of ['n', 'e', 's', 'w', 'ne', 'se', 'sw', 'nw']) {
|
|
const resizable_handle = document.createElement('div');
|
|
|
|
resizable_handle.classList.add('ui-resizable-handle', `ui-resizable-${direction}`);
|
|
|
|
if (['n', 'e', 's', 'w'].includes(direction)) {
|
|
const ui_resize_dot = document.createElement('div');
|
|
|
|
ui_resize_dot.classList.add('ui-resize-dot');
|
|
resizable_handle.appendChild(ui_resize_dot);
|
|
|
|
const ui_resizable_border = document.createElement('div');
|
|
|
|
ui_resizable_border.classList.add(`ui-resizable-border-${direction}`);
|
|
resizable_handle.appendChild(ui_resizable_border);
|
|
}
|
|
|
|
this._target.append(resizable_handle);
|
|
this._resizable_handles[direction] = resizable_handle;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove eight resize handles from the widget by which the widget can be resized in editing mode. Invoked when the
|
|
* widget is left (unfocused).
|
|
*/
|
|
_removeResizeHandles() {
|
|
for (const resizable_handle of Object.values(this._resizable_handles)) {
|
|
resizable_handle.remove();
|
|
}
|
|
|
|
this._resizable_handles = {};
|
|
}
|
|
|
|
/**
|
|
* Calculate viewport dimensions of the contents' container.
|
|
*
|
|
* @returns {{height: number, width: number}}
|
|
*/
|
|
_getContentsSize() {
|
|
const computed_style = getComputedStyle(this._contents);
|
|
|
|
const width = Math.floor(
|
|
parseFloat(computed_style.width)
|
|
- parseFloat(computed_style.paddingLeft) - parseFloat(computed_style.paddingRight)
|
|
- parseFloat(computed_style.borderLeftWidth) - parseFloat(computed_style.borderRightWidth)
|
|
);
|
|
|
|
const height = Math.floor(
|
|
parseFloat(computed_style.height)
|
|
- parseFloat(computed_style.paddingTop) - parseFloat(computed_style.paddingBottom)
|
|
- parseFloat(computed_style.borderTopWidth) - parseFloat(computed_style.borderBottomWidth)
|
|
);
|
|
|
|
return {width, height};
|
|
}
|
|
|
|
/**
|
|
* Update error messages.
|
|
*
|
|
* @param {string[]} messages
|
|
* @param {string|null} title
|
|
*/
|
|
_updateMessages(messages = [], title = null) {
|
|
this._messages.innerHTML = '';
|
|
|
|
if (messages.length > 0 || title !== null) {
|
|
const message_box = makeMessageBox('bad', messages, title)[0];
|
|
|
|
this._messages.appendChild(message_box);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update info buttons in the widget header.
|
|
*
|
|
* @param {Object[]} info
|
|
* {string} info[].icon
|
|
* {string} info[].hint
|
|
*/
|
|
_updateInfo(info = []) {
|
|
for (const li of this._actions.querySelectorAll('.widget-info-button')) {
|
|
li.remove();
|
|
}
|
|
|
|
for (let i = info.length - 1; i >= 0; i--) {
|
|
const li = document.createElement('li');
|
|
|
|
li.classList.add('widget-info-button');
|
|
|
|
const li_button = document.createElement('button');
|
|
|
|
li_button.type = 'button';
|
|
li_button.setAttribute('data-hintbox', '1');
|
|
li_button.setAttribute('data-hintbox-static', '1');
|
|
li_button.classList.add(ZBX_STYLE_BTN_ICON, info[i].icon);
|
|
li.appendChild(li_button);
|
|
|
|
const li_div = document.createElement('div');
|
|
|
|
li_div.innerHTML = info[i].hint;
|
|
li_div.classList.add('hint-box');
|
|
li_div.style.display = 'none';
|
|
li.appendChild(li_div);
|
|
|
|
this._actions.prepend(li);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update debug information.
|
|
*
|
|
* @param {string} debug
|
|
*/
|
|
_updateDebug(debug = '') {
|
|
this._debug.innerHTML = debug;
|
|
}
|
|
|
|
/**
|
|
* Show data preloader immediately. Invoked before the first update cycle of the widget.
|
|
*/
|
|
_showPreloader() {
|
|
// Fixed Safari 16 bug: removing preloader classes on animation frame to ensure removal of icons.
|
|
|
|
if (this._hide_preloader_animation_frame !== null) {
|
|
cancelAnimationFrame(this._hide_preloader_animation_frame);
|
|
this._hide_preloader_animation_frame = null;
|
|
}
|
|
|
|
this._body.classList.add('is-loading');
|
|
this._body.classList.remove('is-loading-fadein', 'delayed-15s');
|
|
}
|
|
|
|
/**
|
|
* Hide data preloader.
|
|
*/
|
|
_hidePreloader() {
|
|
// Fixed Safari 16 bug: removing preloader classes on animation frame to ensure removal of icons.
|
|
|
|
if (this._hide_preloader_animation_frame !== null) {
|
|
return;
|
|
}
|
|
|
|
this._hide_preloader_animation_frame = requestAnimationFrame(() => {
|
|
this._body.classList.remove('is-loading', 'is-loading-fadein', 'delayed-15s');
|
|
this._hide_preloader_animation_frame = null;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Schedule showing data preloader after 15 seconds. Invoked before regular update cycle of the widget.
|
|
*/
|
|
_schedulePreloader() {
|
|
// Fixed Safari 16 bug: removing preloader classes on animation frame to ensure removal of icons.
|
|
|
|
if (this._hide_preloader_animation_frame !== null) {
|
|
cancelAnimationFrame(this._hide_preloader_animation_frame);
|
|
this._hide_preloader_animation_frame = null;
|
|
}
|
|
|
|
this._body.classList.add('is-loading', 'is-loading-fadein', 'delayed-15s');
|
|
}
|
|
|
|
/**
|
|
* Create DOM structure for the widget. Invoked once, on widget start.
|
|
*/
|
|
_makeView() {
|
|
this._container = document.createElement('div');
|
|
this._container.classList.add(this._css_classes.container);
|
|
|
|
this._header = document.createElement('div');
|
|
this._header.classList.add(this._css_classes.header);
|
|
|
|
const header_h4 = document.createElement('h4');
|
|
|
|
header_h4.textContent = this._name !== '' ? this._name : this._defaults.name;
|
|
this._header.appendChild(header_h4);
|
|
|
|
this._actions = document.createElement('ul');
|
|
this._actions.classList.add(this._css_classes.actions);
|
|
|
|
if (this._is_editable) {
|
|
this._button_edit = document.createElement('button');
|
|
this._button_edit.type = 'button';
|
|
this._button_edit.title = t('Edit')
|
|
this._button_edit.classList.add(ZBX_STYLE_BTN_ICON, ZBX_ICON_COG_FILLED, 'js-widget-edit');
|
|
|
|
const li = document.createElement('li');
|
|
|
|
li.appendChild(this._button_edit);
|
|
this._actions.appendChild(li);
|
|
}
|
|
|
|
this._button_actions = document.createElement('button');
|
|
this._button_actions.type = 'button';
|
|
this._button_actions.title = t('Actions');
|
|
this._button_actions.setAttribute('aria-expanded', 'false');
|
|
this._button_actions.setAttribute('aria-haspopup', 'true');
|
|
this._button_actions.classList.add(ZBX_STYLE_BTN_ICON, ZBX_ICON_MORE, 'js-widget-action');
|
|
|
|
const li = document.createElement('li');
|
|
|
|
li.appendChild(this._button_actions);
|
|
this._actions.appendChild(li);
|
|
|
|
this._header.append(this._actions);
|
|
|
|
this._container.appendChild(this._header);
|
|
|
|
this._contents = document.createElement('div');
|
|
this._contents.classList.add(this._css_classes.contents);
|
|
this._contents.classList.add(`dashboard-widget-${this._type}`);
|
|
this._contents.classList.toggle('no-padding', !this.hasPadding());
|
|
|
|
this._messages = document.createElement('div');
|
|
this._messages.classList.add(this._css_classes.messages);
|
|
this._contents.appendChild(this._messages);
|
|
|
|
this._body = document.createElement('div');
|
|
this._body.classList.add(this._css_classes.body);
|
|
this._contents.appendChild(this._body);
|
|
|
|
this._debug = document.createElement('div');
|
|
this._debug.classList.add(this._css_classes.debug);
|
|
this._contents.appendChild(this._debug);
|
|
|
|
this._container.appendChild(this._contents);
|
|
|
|
this._target.appendChild(this._container);
|
|
this._target.classList.add(this._css_classes.root);
|
|
this._target.classList.toggle('ui-draggable', this._is_edit_mode);
|
|
this._target.classList.toggle('ui-resizable', this._is_edit_mode);
|
|
this._target.classList.toggle(this._css_classes.hidden_header,
|
|
this._view_mode === ZBX_WIDGET_VIEW_MODE_HIDDEN_HEADER
|
|
);
|
|
this._target.classList.toggle('new-widget', this._is_new);
|
|
|
|
this._target.style.minWidth = `${this._cell_width}%`;
|
|
this._target.style.minHeight = `${this._cell_height}px`;
|
|
}
|
|
|
|
// Internal events management methods.
|
|
|
|
/**
|
|
* Create event listeners. Invoked once, upon widget initialization.
|
|
*/
|
|
_registerEvents() {
|
|
this._events = {
|
|
actions: (e) => {
|
|
this.fire(WIDGET_EVENT_ACTIONS, {mouse_event: e});
|
|
},
|
|
|
|
edit: () => {
|
|
this.fire(WIDGET_EVENT_EDIT);
|
|
},
|
|
|
|
focusin: () => {
|
|
this.fire(WIDGET_EVENT_ENTER);
|
|
},
|
|
|
|
focusout: () => {
|
|
this.fire(WIDGET_EVENT_LEAVE);
|
|
},
|
|
|
|
enter: () => {
|
|
this.fire(WIDGET_EVENT_ENTER);
|
|
},
|
|
|
|
leave: () => {
|
|
this.fire(WIDGET_EVENT_LEAVE);
|
|
},
|
|
|
|
...this._events
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Activate event listeners. Invoked on each activation of the dashboard page.
|
|
*/
|
|
_activateEvents() {
|
|
this._button_actions.addEventListener('click', this._events.actions);
|
|
|
|
if (this._is_editable) {
|
|
this._button_edit.addEventListener('click', this._events.edit);
|
|
}
|
|
|
|
this._target.addEventListener('mousemove', this._events.enter);
|
|
this._target.addEventListener('mouseleave', this._events.leave);
|
|
this._header.addEventListener('focusin', this._events.focusin);
|
|
this._header.addEventListener('focusout', this._events.focusout);
|
|
}
|
|
|
|
/**
|
|
* Deactivate event listeners. Invoked on each deactivation of the dashboard page.
|
|
*/
|
|
_deactivateEvents() {
|
|
this._button_actions.removeEventListener('click', this._events.actions);
|
|
|
|
if (this._is_editable) {
|
|
this._button_edit.removeEventListener('click', this._events.edit);
|
|
}
|
|
|
|
this._target.removeEventListener('mousemove', this._events.enter);
|
|
this._target.removeEventListener('mouseleave', this._events.leave);
|
|
this._header.removeEventListener('focusin', this._events.focusin);
|
|
this._header.removeEventListener('focusout', this._events.focusout);
|
|
}
|
|
|
|
/**
|
|
* Attach event listener to widget events.
|
|
*
|
|
* @param {string} type
|
|
* @param {function} listener
|
|
* @param {Object|false} options
|
|
*
|
|
* @returns {CWidgetBase}
|
|
*/
|
|
on(type, listener, options = false) {
|
|
this._target.addEventListener(type, listener, options);
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Detach event listener from widget events.
|
|
*
|
|
* @param {string} type
|
|
* @param {function} listener
|
|
* @param {Object|false} options
|
|
*
|
|
* @returns {CWidgetBase}
|
|
*/
|
|
off(type, listener, options = false) {
|
|
this._target.removeEventListener(type, listener, options);
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Dispatch widget event.
|
|
*
|
|
* @param {string} type
|
|
* @param {Object} detail
|
|
* @param {Object} options
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
fire(type, detail = {}, options = {}) {
|
|
return this._target.dispatchEvent(new CustomEvent(type, {...options, detail: {target: this, ...detail}}));
|
|
}
|
|
}
|