diff options
Diffstat (limited to 'ui/widgets/geomap/assets/js/class.widget.js')
-rw-r--r-- | ui/widgets/geomap/assets/js/class.widget.js | 546 |
1 files changed, 546 insertions, 0 deletions
diff --git a/ui/widgets/geomap/assets/js/class.widget.js b/ui/widgets/geomap/assets/js/class.widget.js new file mode 100644 index 00000000000..1d115d87f5a --- /dev/null +++ b/ui/widgets/geomap/assets/js/class.widget.js @@ -0,0 +1,546 @@ +/* +** Zabbix +** Copyright (C) 2001-2022 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. +**/ + + +class CWidgetGeoMap extends CWidget { + static SEVERITY_NO_PROBLEMS = -1; + static SEVERITY_NOT_CLASSIFIED = 0; + static SEVERITY_INFORMATION = 1; + static SEVERITY_WARNING = 2; + static SEVERITY_AVERAGE = 3; + static SEVERITY_HIGH = 4; + static SEVERITY_DISASTER = 5; + + _init() { + super._init(); + + this._map = null; + this._icons = {}; + this._initial_load = true; + this._home_coords = {}; + this._severity_levels = new Map(); + } + + _getUpdateRequestData() { + return { + ...super._getUpdateRequestData(), + initial_load: this._initial_load ? 1 : 0, + unique_id: this._unique_id + }; + } + + _processUpdateResponse(response) { + if (this._initial_load) { + super._processUpdateResponse(response); + } + else { + let message_box = this._content_body.querySelector('output'); + + if (message_box !== null) { + message_box.remove(); + } + + if (response.messages !== undefined) { + this._content_body.prepend(makeMessageBox('bad', response.messages)[0]); + } + } + + if (response.geomap !== undefined) { + if (response.geomap.config !== undefined) { + this._initMap(response.geomap.config); + } + + this._addMarkers(response.geomap.hosts); + } + + this._initial_load = false; + } + + updateProperties({name, view_mode, fields}) { + this._initial_load = true; + + super.updateProperties({name, view_mode, fields}); + } + + _addMarkers(hosts) { + this._markers.clearLayers(); + this._clusters.clearLayers(); + + this._markers.addData(hosts); + this._clusters.addLayer(this._markers); + } + + _initMap(config) { + const latLng = new L.latLng([config.center.latitude, config.center.longitude]); + + this._home_coords = config.home_coords; + + // Initialize map and load tile layer. + this._map = L.map(this._unique_id).setView(latLng, config.center.zoom); + L.tileLayer(config.tile_url, { + tap: false, + minZoom: 0, + maxZoom: parseInt(config.max_zoom, 10), + minNativeZoom: 1, + maxNativeZoom: parseInt(config.max_zoom, 10), + attribution: config.attribution + }).addTo(this._map); + + this.initSeverities(config.colors); + + // Create cluster layer. + this._clusters = this._createClusterLayer(); + this._map.addLayer(this._clusters); + + // Create markers layer. + this._markers = L.geoJSON([], { + pointToLayer: function (point, ll) { + return L.marker(ll, { + icon: this._icons[point.properties.severity] + }); + }.bind(this) + }); + + this._map.setDefaultView(latLng, config.center.zoom); + + // Severity filter. + this._map.severityFilterControl = L.control.severityFilter({ + position: 'topright', + checked: config.filter.severity, + severity_levels: this._severity_levels, + disabled: !this._widgetid + }).addTo(this._map); + + // Navigate home btn. + this._map.navigateHomeControl = L.control.navigateHomeBtn({position: 'topleft'}).addTo(this._map); + if (Object.keys(this._home_coords).length > 0) { + const home_btn_title = ('default' in this._home_coords) + ? t('Navigate to default view') + : t('Navigate to initial view'); + + this._map.navigateHomeControl.setTitle(home_btn_title); + this._map.navigateHomeControl.show(); + } + + // Workaround to prevent dashboard jumping to make map completely visible. + this._map.getContainer().focus = () => {}; + + // Add event listeners. + this._map.getContainer().addEventListener('click', (e) => { + if (e.target.classList.contains('leaflet-container')) { + this._map.severityFilterControl.close(); + } + }, false); + + this._map.getContainer().addEventListener('filter', (e) => { + this.removeHintBoxes(); + this.updateFilter(e.detail.join(',')); + }, false); + + this._map.getContainer().addEventListener('cluster.click', (e) => { + const cluster = e.detail; + const node = cluster.originalEvent.srcElement.classList.contains('marker-cluster') + ? cluster.originalEvent.srcElement + : cluster.originalEvent.srcElement.closest('.marker-cluster'); + + if ('hintBoxItem' in node) { + return; + } + + const container = this._map._container; + const style = 'left: 0px; top: 0px;'; + + const content = document.createElement('div'); + content.style.overflow = 'auto'; + content.style.maxHeight = (cluster.originalEvent.clientY-60)+'px'; + content.style.display = 'block'; + content.appendChild(this.makePopupContent(cluster.layer.getAllChildMarkers().map(o => o.feature))); + + node.hintBoxItem = hintBox.createBox(e, node, content, '', true, style, container.parentNode); + + const cluster_bounds = cluster.originalEvent.target.getBoundingClientRect(); + const hintbox_bounds = this._target.getBoundingClientRect(); + + let x = cluster_bounds.left + cluster_bounds.width / 2 - hintbox_bounds.left; + let y = cluster_bounds.top - hintbox_bounds.top - 10; + + node.hintBoxItem.position({ + of: node.hintBoxItem, + my: 'center bottom', + at: `left+${x}px top+${y}px`, + collision: 'fit' + }); + + Overlay.prototype.recoverFocus.call({'$dialogue': node.hintBoxItem}); + Overlay.prototype.containFocus.call({'$dialogue': node.hintBoxItem}); + }); + + this._markers.on('click keypress', (e) => { + const node = e.originalEvent.srcElement; + if ('hintBoxItem' in node) { + return; + } + + if (e.type === 'keypress') { + if (e.originalEvent.key !== ' ' && e.originalEvent.key !== 'Enter') { + return; + } + e.originalEvent.preventDefault(); + } + + const container = this._map._container; + const content = this.makePopupContent([e.layer.feature]); + const style = 'left: 0px; top: 0px;'; + + node.hintBoxItem = hintBox.createBox(e, node, content, '', true, style, container.parentNode); + + const marker_bounds = e.originalEvent.target.getBoundingClientRect(); + const hintbox_bounds = this._target.getBoundingClientRect(); + + let x = marker_bounds.left + marker_bounds.width / 2 - hintbox_bounds.left; + let y = marker_bounds.top - hintbox_bounds.top - 10; + + node.hintBoxItem.position({ + of: node.hintBoxItem, + my: 'center bottom', + at: `left+${x}px top+${y}px`, + collision: 'fit' + }); + + Overlay.prototype.recoverFocus.call({'$dialogue': node.hintBoxItem}); + Overlay.prototype.containFocus.call({'$dialogue': node.hintBoxItem}); + }); + + this._map.getContainer().addEventListener('cluster.dblclick', (e) => { + e.detail.layer.zoomToBounds({padding: [20, 20]}); + }); + + this._map.getContainer().addEventListener('contextmenu', (e) => { + if (e.target.classList.contains('leaflet-container')) { + const $obj = $(e.target); + const menu = [{ + label: t('Actions'), + items: [{ + label: t('Set this view as default'), + clickCallback: this.updateDefaultView.bind(this), + disabled: !this._widgetid + }, { + label: t('Reset to initial view'), + clickCallback: this.unsetDefaultView.bind(this), + disabled: !('default' in this._home_coords) + }] + }]; + + $obj.menuPopup(menu, e, { + position: { + of: $obj, + my: 'left top', + at: 'left+'+e.layerX+' top+'+e.layerY, + collision: 'fit' + } + }); + } + + e.preventDefault(); + }); + + // Close opened hintboxes when moving/zooming/resizing widget. + this._map.on('zoomstart movestart resize', () => { + this.removeHintBoxes(); + }); + } + + /** + * Function to create cluster layer. + * + * @returns {CWidgetGeoMap._createClusterLayer.clusters|L.MarkerClusterGroup} + */ + _createClusterLayer() { + const clusters = L.markerClusterGroup({ + showCoverageOnHover: false, + zoomToBoundsOnClick: false, + removeOutsideVisibleBounds: true, + spiderfyOnMaxZoom: false, + iconCreateFunction: (cluster) => { + const max_severity = Math.max(...cluster.getAllChildMarkers().map(p => p.feature.properties.severity)); + const color = this._severity_levels.get(max_severity).color; + + return new L.DivIcon({ + html: ` + <div style="background-color: ${color};"> + <span>${cluster.getChildCount()}</span> + </div>`, + className: 'marker-cluster', + iconSize: new L.Point(40, 40) + }); + } + }); + + // Transform 'clusterclick' event as 'cluster.click' and 'cluster.dblclick' events. + clusters.on('clusterclick clusterkeypress', (c) => { + if (c.type === 'clusterkeypress') { + if (c.originalEvent.key !== ' ' && c.originalEvent.key !== 'Enter') { + return; + } + c.originalEvent.preventDefault(); + } + + if ('event_click' in clusters) { + clearTimeout(clusters.event_click); + delete clusters.event_click; + this._map.getContainer().dispatchEvent( + new CustomEvent('cluster.dblclick', {detail: c}) + ); + } + else { + clusters.event_click = setTimeout(() => { + delete clusters.event_click; + this._map.getContainer().dispatchEvent( + new CustomEvent('cluster.click', {detail: c}) + ); + }, 300); + } + }); + + return clusters; + } + + /** + * Save severity filter values in user profile and update widget. + * + * @param {string} filter + */ + updateFilter(filter) { + updateUserProfile('web.dashboard.widget.geomap.severity_filter', filter, [this._widgetid], PROFILE_TYPE_STR) + .always(() => { + if (this._state === WIDGET_STATE_ACTIVE) { + this._startUpdating(); + } + }); + } + + /** + * Save default view. + * + * @param {string} filter + */ + updateDefaultView() { + const ll = this._map.getCenter(); + const zoom = this._map.getZoom(); + const view = `${ll.lat},${ll.lng},${zoom}`; + + updateUserProfile('web.dashboard.widget.geomap.default_view', view, [this._widgetid], PROFILE_TYPE_STR); + this._map.setDefaultView(ll, zoom); + this._home_coords['default'] = true; + this._map.navigateHomeControl.show(); + this._map.navigateHomeControl.setTitle(t('Navigate to default view')); + } + + /** + * Unset default view. + * + * @returns {undefined} + */ + unsetDefaultView() { + updateUserProfile('web.dashboard.widget.geomap.default_view', '', [this._widgetid], PROFILE_TYPE_STR) + .always(() => { + delete this._home_coords.default; + }); + + if ('initial' in this._home_coords) { + const latLng = new L.latLng([this._home_coords.initial.latitude, this._home_coords.initial.longitude]); + this._map.setDefaultView(latLng, this._home_coords.initial.zoom); + this._map.navigateHomeControl.setTitle(t('Navigate to initial view')); + this._map.setView(latLng, this._home_coords.initial.zoom); + } + else { + this._map.navigateHomeControl.hide(); + } + } + + /** + * Function to delete all opened hintboxes. + */ + removeHintBoxes() { + const markers = this._map._container.parentNode.querySelectorAll('.marker-cluster, .leaflet-marker-icon'); + [...markers].forEach((m) => { + if ('hintboxid' in m) { + hintBox.deleteHint(m); + } + }); + } + + /** + * Create host popup content. + * + * @param {array} hosts + * + * @return {string} + */ + makePopupContent(hosts) { + const makeHostBtn = (host) => { + const {name, hostid} = host.properties; + const data_menu_popup = JSON.stringify({type: 'host', data: {hostid: hostid}}); + const btn = document.createElement('a'); + btn.ariaExpanded = false; + btn.ariaHaspopup = true; + btn.role = 'button'; + btn.setAttribute('data-menu-popup', data_menu_popup); + btn.classList.add('link-action'); + btn.href = 'javascript:void(0)'; + btn.textContent = name; + + return btn; + }; + + const makeDataCell = (host, severity) => { + if (severity in host.properties.problems) { + const style = this._severity_levels.get(severity).class; + const problems = host.properties.problems[severity]; + return `<td class="${style}">${problems}</td>`; + } + else { + return `<td></td>`; + } + }; + + const makeTableRows = () => { + hosts.sort((a, b) => { + if (a.properties.name < b.properties.name) { + return -1; + } + if (a.properties.name > b.properties.name) { + return 1; + } + return 0; + }); + + let rows = ``; + hosts.forEach(host => { + rows += ` + <tr> + <td class="nowrap">${makeHostBtn(host).outerHTML}</td> + ${makeDataCell(host, CWidgetGeoMap.SEVERITY_DISASTER)} + ${makeDataCell(host, CWidgetGeoMap.SEVERITY_HIGH)} + ${makeDataCell(host, CWidgetGeoMap.SEVERITY_AVERAGE)} + ${makeDataCell(host, CWidgetGeoMap.SEVERITY_WARNING)} + ${makeDataCell(host, CWidgetGeoMap.SEVERITY_INFORMATION)} + ${makeDataCell(host, CWidgetGeoMap.SEVERITY_NOT_CLASSIFIED)} + </tr>`; + }); + + return rows; + }; + + const html = ` + <table class="list-table"> + <thead> + <tr> + <th>${t('Host')}</th> + <th>${this._severity_levels.get(CWidgetGeoMap.SEVERITY_DISASTER).abbr}</th> + <th>${this._severity_levels.get(CWidgetGeoMap.SEVERITY_HIGH).abbr}</th> + <th>${this._severity_levels.get(CWidgetGeoMap.SEVERITY_AVERAGE).abbr}</th> + <th>${this._severity_levels.get(CWidgetGeoMap.SEVERITY_WARNING).abbr}</th> + <th>${this._severity_levels.get(CWidgetGeoMap.SEVERITY_INFORMATION).abbr}</th> + <th>${this._severity_levels.get(CWidgetGeoMap.SEVERITY_NOT_CLASSIFIED).abbr}</th> + </th> + </thead> + <tbody>${makeTableRows()}</tbody> + </table>`; + + // Make DOM. + const dom = document.createElement('template'); + dom.innerHTML = html; + + return dom.content; + } + + /** + * Function creates marker icons and severity-related options. + * + * @param {object} severity_colors + */ + initSeverities(severity_colors) { + this._severity_levels.set(CWidgetGeoMap.SEVERITY_NO_PROBLEMS, { + name: t('No problems'), + color: severity_colors[CWidgetGeoMap.SEVERITY_NO_PROBLEMS] + }); + this._severity_levels.set(CWidgetGeoMap.SEVERITY_NOT_CLASSIFIED, { + name: t('Not classified'), + abbr: t('N'), + class: 'na-bg', + color: severity_colors[CWidgetGeoMap.SEVERITY_NOT_CLASSIFIED] + }); + this._severity_levels.set(CWidgetGeoMap.SEVERITY_INFORMATION, { + name: t('Information'), + abbr: t('I'), + class: 'info-bg', + color: severity_colors[CWidgetGeoMap.SEVERITY_INFORMATION] + }); + this._severity_levels.set(CWidgetGeoMap.SEVERITY_WARNING, { + name: t('Warning'), + abbr: t('W'), + class: 'warning-bg', + color: severity_colors[CWidgetGeoMap.SEVERITY_WARNING] + }); + this._severity_levels.set(CWidgetGeoMap.SEVERITY_AVERAGE, { + name: t('Average'), + abbr: t('A'), + class: 'average-bg', + color: severity_colors[CWidgetGeoMap.SEVERITY_AVERAGE] + }); + this._severity_levels.set(CWidgetGeoMap.SEVERITY_HIGH, { + name: t('High'), + abbr: t('H'), + class: 'high-bg', + color: severity_colors[CWidgetGeoMap.SEVERITY_HIGH] + }); + this._severity_levels.set(CWidgetGeoMap.SEVERITY_DISASTER, { + name: t('Disaster'), + abbr: t('D'), + class: 'disaster-bg', + color: severity_colors[CWidgetGeoMap.SEVERITY_DISASTER] + }); + + for (const severity in severity_colors) { + const color = severity_colors[severity]; + const tmpl = ` + <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="40px" height="40px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" viewBox="0 0 500 500" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <style type="text/css"><![CDATA[ + .outline {fill: #000; fill-rule: nonzero; fill-opacity: .2} + ]]></style> + </defs> + <g> + <path fill="${color}" d="M254 13c81,0 146,65 146,146 0,40 -16,77 -43,103l-97 233 -11 3 -98 -236c-27,-26 -43,-63 -43,-103 0,-81 65,-146 146,-146zm0 82c84,0 84,127 0,127 -84,0 -84,-127 0,-127z"/> + <path class="outline" d="M254 6c109,0 182,111 141,211 -8,18 -19,35 -32,49l-98 235 -19 5 -100 -240c-18,-27 -44,-46 -44,-107 0,-84 68,-153 152,-153zm99 54c-132,-132 -327,70 -197,198l97 233 3 -1 97 -232c54,-54 55,-143 0,-198zm-99 29c92,0 92,140 0,140 -92,0 -92,-140 0,-140zm40 29c-53,-53 -134,28 -80,81 53,53 134,-27 80,-81z"/> + </g> + </svg>`; + + this._icons[severity] = L.icon({ + iconUrl: 'data:image/svg+xml;base64,' + btoa(tmpl), + shadowUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAApCAQAAAACach9AAACMUlEQVR4Ae3ShY7jQBAE0Aoz/f9/HTMzhg1zrdKUrJbdx+Kd2nD8VNudfsL/Th/'+'/'+'/dyQN2TH6f3y/BGpC379rV+S+qqetBOxImNQXL8JCAr2V4iMQXHGNJxeCfZXhSRBcQMfvkOWUdtfzlLgAENmZDcmo2TVmt8OSM2eXxBp3DjHSMFutqS7SbmemzBiR+xpKCNUIRkdkkYxhAkyGoBvyQFEJEefwSmmvBfJuJ6aKqKWnAkvGZOaZXTUgFqYULWNSHUckZuR1HIIimUExutRxwzOLROIG4vKmCKQt364mIlhSyzAf1m9lHZHJZrlAOMMztRRiKimp/rpdJDc9Awry5xTZCte7FHtuS8wJgeYGrex28xNTd086Dik7vUMscQOa8y4DoGtCCSkAKlNwpgNtphjrC6MIHUkR6YWxxs6Sc5xqn222mmCRFzIt8lEdKx+ikCtg91qS2WpwVfBelJCiQJwvzixfI9cxZQWgiSJelKnwBElKYtDOb2MFbhmUigbReQBV0Cg4+qMXSxXSyGUn4UbF8l+7qdSGnTC0XLCmahIgUHLhLOhpVCtw4CzYXvLQWQbJNmxoCsOKAxSgBJno75avolkRw8iIAFcsdc02e9iyCd8tHwmeSSoKTowIgvscSGZUOA7PuCN5b2BX9mQM7S0wYhMNU74zgsPBj3HU7wguAfnxxjFQGBE6pwN+GjME9zHY7zGp8wVxMShYX9NXvEWD3HbwJf4giO4CFIQxXScH1/TM+04kkBiAAAAAElFTkSuQmCC', + iconSize: [40, 40], + iconAnchor: [20, 40], + shadowSize: [40, 40], + shadowAnchor: [13, 40] + }); + } + } +} |