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.
534 lines
17 KiB
534 lines
17 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.
|
|
**/
|
|
|
|
|
|
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;
|
|
|
|
onInitialize() {
|
|
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
|
|
};
|
|
}
|
|
|
|
setContents(response) {
|
|
if (this._initial_load) {
|
|
super.setContents(response);
|
|
}
|
|
|
|
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]
|
|
});
|
|
}
|
|
}
|
|
}
|