Files
librenms-librenms/resources/views/map/custom-view.blade.php
eskyuu cb09ae0d54 Additional custom map features (#15806)
* Added options on edges for a label and to show graphs as bps as well as percentages

I think that vis.js needs to be updated to allow both bps and percentages at the same time.

* Add database migration

* Try to avoid putting multiple mid points in the same position

* Added a URL parameter for screenshot mode, where node labels are blanked out
Also fixed up the node labels in the editor

* Added legend to the editor as well as database options for reversing arrows and adjusting the edge separation

All features have been implemented in the editor, but need to be implemented in the viewer

* Fix missing defaults on the edit map list page
Added arrow reverse code to the viewer
Added legend code to the viewer
Added code to the editor to correclty handle moving the legend

* Formatting fixes and DB schema update

* Remove view from database schema
2024-02-07 11:42:35 -06:00

320 lines
14 KiB
PHP

@extends('layouts.librenmsv1')
@section('title', __('map.custom.title.view', ['name' => $name]))
@section('content')
<div class="container-fluid">
<div class="row" id="alert-row">
<div class="col-md-12">
<div class="alert alert-warning" role="alert" id="alert">{{ __('map.custom.view.loading') }}</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<center>
<div id="custom-map"></div>
</center>
</div>
</div>
</div>
@endsection
@section('javascript')
<script type="text/javascript" src="{{ asset('js/vis.min.js') }}"></script>
@endsection
@section('scripts')
<script type="text/javascript">
var bgimage = {{ $background ? "true" : "false" }};
var screenshot = {{ $screenshot ? "true" : "false" }};
var reverse_arrows = {{$reverse_arrows}};
var legend = @json($legend);
var network;
var network_height;
var network_width;
var network_nodes = new vis.DataSet({queue: {delay: 100}});
var network_edges = new vis.DataSet({queue: {delay: 100}});
var edge_port_map = {};
var node_device_map = {};
var node_link_map = {};
var custom_image_base = "{{ $base_url }}images/custommap/icons/";
function legendPctColour(pct) {
if (pct < 0) {
return "black";
} else if (pct < 50) {
// 100% green and slowly increase the red until we get to yellow
return '#' + parseInt(5.1 * pct).toString(16).padStart(2, 0) + 'ff00';
} else if (pct < 100) {
// 100% red and slowly remove green to go from yellow to red
return '#ff' + parseInt(5.1 * (100.0 - pct)).toString(16).padStart(2, 0) + '00';
} else if (pct < 150) {
// 100% red and slowly increase blue to go purple
return '#ff00' + parseInt(5.1 * (pct - 100.0)).toString(16).padStart(2, 0);
}
// Default to purple for links over 150%
return '#ff00ff';
}
function redrawLegend() {
// Clear out the old legend
old_nodes = network_nodes.get({filter: function(node) { return node.id.startsWith("legend_") }});
old_nodes.forEach((node) => {
network_nodes.remove(node.id);
});
if (legend.x >= 0) {
let y_pos = legend.y;
let y_inc = legend.font_size + 10;
let legend_header = {id: "legend_header", label: "<b>Legend</b>", shape: "box", borderWidth: 0, x: legend.x, y: y_pos, font: {multi: 'html', size: legend.font_size}, color: {background: "white"}};
network_nodes.add(legend_header);
y_pos += y_inc;
if (!(Boolean(legend.hide_invalid))) {
let legend_invalid = {id: "legend_invalid", label: "???", title: "Link is down or link speed is not defined", shape: "box", borderWidth: 0, x: legend.x, y: y_pos, font: {face: 'courier new', size: legend.font_size, color: "white"}, color: {background: "black"}};
y_pos += y_inc;
network_nodes.add(legend_invalid);
}
let pct_step;
if (Boolean(legend.hide_overspeed)) {
pct_step = 100.0 / (legend.steps - 1);
} else {
pct_step = 150.0 / (legend.steps - 1);
}
for (let i=0; i < legend.steps; i++) {
let this_pct = Math.round(pct_step * i);
let legend_step = {id: "legend_" + i.toString(), label: this_pct.toString().padStart(3, " ") + "%", shape: "box", borderWidth: 0, x: legend.x, y: y_pos, font: {face: 'courier new', size: legend.font_size, color: "black"}, color: {background: legendPctColour(this_pct)}};
network_nodes.add(legend_step);
y_pos += y_inc;
}
network_nodes.flush();
}
}
function CreateNetwork() {
// Flush the nodes and edges so they are rendered immediately
network_nodes.flush();
network_edges.flush();
var container = document.getElementById('custom-map');
var options = {!! json_encode($map_conf) !!};
network = new vis.Network(container, {nodes: network_nodes, edges: network_edges, stabilize: true}, options);
network_height = $($(container).children(".vis-network")[0]).height();
network_width = $($(container).children(".vis-network")[0]).width();
var centreY = parseInt(network_height / 2);
var centreX = parseInt(network_width / 2);
network.moveTo({position: {x: centreX, y: centreY}, scale: 1});
if(bgimage) {
canvas = $("#custom-map").children()[0].canvas;
$(canvas).css('background-image','url({{ route('maps.custom.background', ['map' => $map_id]) }}?ver={{$bgversion}})').css('background-size', 'cover');
}
network.on('doubleClick', function (properties) {
edge_id = null;
if (properties.nodes.length > 0) {
if(properties.nodes[0] in node_device_map) {
window.location.href = "device/"+node_device_map[properties.nodes[0]].device_id;
} else if (properties.nodes[0] in node_link_map) {
window.location.href = '{{ route('maps.custom.show', ['map' => '?']) }}'.replace('?', node_link_map[properties.nodes[0]]);
} else if (properties.nodes[0].endsWith('_mid')) {
edge_id = properties.nodes[0].split("_")[0];
}
} else if (properties.edges.length > 0) {
edge_id = properties.edges[0].split("_")[0];
}
if (edge_id && (edge_id in edge_port_map)) {
window.location.href = 'device/device=' + edge_port_map[edge_id].device_id + '/tab=port/port=' + edge_port_map[edge_id].port_id + '/';
}
});
}
var Countdown;
function refreshMap() {
$.get( '{{ route('maps.custom.data', ['map' => $map_id]) }}')
.done(function( data ) {
// Add/update nodes
$.each( data.nodes, function( nodeid, node) {
var node_cfg = {};
node_cfg.id = nodeid;
if(node.device_id) {
node_device_map[nodeid] = {device_id: node.device_id, device_name: node.device_name};
delete node_link_map[nodeid];
node_cfg.title = node.device_info;
} else if(node.linked_map_name) {
delete node_device_map[nodeid];
node_link_map[nodeid] = node.linked_map_id;
node_cfg.title = "Go to " + node.linked_map_name;
} else {
node_cfg.title = null;
}
node_cfg.label = screenshot ? node.label.replace(/./g, ' ') : node.label;
node_cfg.shape = node.style;
node_cfg.borderWidth = node.border_width;
node_cfg.x = node.x_pos;
node_cfg.y = node.y_pos;
node_cfg.font = {face: node.text_face, size: node.text_size, color: node.text_colour};
node_cfg.size = node.size;
node_cfg.color = {background: node.colour_bg_view, border: node.colour_bdr_view};
if(node.style == "icon") {
node_cfg.icon = {face: 'FontAwesome', code: String.fromCharCode(parseInt(node.icon, 16)), size: node.size, color: node.colour_bdr};
} else {
node_cfg.icon = {};
}
if(node.style == "image" || node.style == "circularImage") {
if(node.image) {
node_cfg.image = {unselected: custom_image_base + node.image};
} else if (node.device_image) {
node_cfg.image = {unselected: node.device_image};
} else {
// If we do not get a valid image from the database, use defaults
node_cfg.shape = newnodeconf.shape;
node_cfg.icon = newnodeconf.icon;
node_cfg.image = newnodeconf.image;
}
} else {
node_cfg.image = {};
}
if (network_nodes.get(nodeid)) {
network_nodes.update(node_cfg);
} else {
network_nodes.add([node_cfg]);
}
});
$.each( data.edges, function( edgeid, edge) {
var mid_x = edge.mid_x;
var mid_y = edge.mid_y;
var mid = {id: edgeid + "_mid", shape: "dot", size: 0, x: mid_x, y: mid_y, label: screenshot ? '' : edge.label};
if (Boolean(reverse_arrows)) {
arrows = {from: {enabled: true, scaleFactor: 0.6}, to: {enabled: false}};
} else {
arrows = {to: {enabled: true, scaleFactor: 0.6}, from: {enabled: false}};
}
var edge1 = {id: edgeid + "_from", from: edge.custom_map_node1_id, to: edgeid + "_mid", arrows: arrows, font: {face: edge.text_face, size: edge.text_size, color: edge.text_colour}, smooth: {type: edge.style}};
var edge2 = {id: edgeid + "_to", from: edge.custom_map_node2_id, to: edgeid + "_mid", arrows: arrows, font: {face: edge.text_face, size: edge.text_size, color: edge.text_colour}, smooth: {type: edge.style}};
// Special case for curved lines
if(edge2.smooth.type == "curvedCW") {
edge2.smooth.type = "curvedCCW";
} else if (edge2.smooth.type == "curvedCCW") {
edge2.smooth.type = "curvedCW";
}
if(edge.port_id) {
var edge_port_from;
var edge_port_to;
if (Boolean(reverse_arrows)) {
port_from = edge2;
port_to = edge1;
} else {
port_from = edge1;
port_to = edge2;
}
port_from.title = port_to.title = edge.port_info;
if(edge.showpct) {
port_from.label = edge.port_frompct + "%";
port_to.label = edge.port_topct + "%";
}
if(edge.showbps) {
if(port_from.label == null) {
port_from.label = '';
port_to.label = '';
} else {
port_from.label += "\n";
port_to.label += "\n";
}
port_from.label += edge.port_frombps;
port_to.label += edge.port_tobps;
}
port_from.color = {color: edge.colour_from};
port_from.width = edge.width_from;
port_to.color = {color: edge.colour_to};
port_to.width = edge.width_to;
edge_port_map[edgeid] = {device_id: edge.device_id, port_id: edge.port_id};
} else {
delete edge_port_map[edgeid];
}
if (network_nodes.get(mid.id)) {
network_nodes.update(mid);
network_edges.update(edge1);
network_edges.update(edge2);
} else {
network_nodes.add([mid]);
network_edges.add([edge1, edge2]);
}
});
// Remove any nodes that are not in the database, includes edges
$.each( network_nodes.getIds(), function( node_idx, nodeid ) {
if(nodeid.endsWith('_mid')) {
edgeid = nodeid.split("_")[0];
if(! (edgeid in data.edges)) {
network_nodes.remove(edgeid + "_mid");
network_edges.remove(edgeid + "_to");
network_edges.remove(edgeid + "_from");
}
} else {
if(! (nodeid in data.nodes)) {
network_nodes.remove(nodeid);
}
}
});
// Re-draw the legend
redrawLegend();
// Flush in order to make sure nodes exist for edges to connect to
network_nodes.flush();
network_edges.flush();
if (Object.keys(data).length == 0) {
$("#alert").text('{{ __('map.custom.view.no_devices') }}');
$("#alert-row").show();
} else {
$("#alert").text("");
$("#alert-row").hide();
}
});
// Initialise map if it does not exist
if (! network) {
CreateNetwork();
}
}
$(document).ready(function () {
Countdown = {
sec: {{$page_refresh}},
Start: function () {
var cur = this;
this.interval = setInterval(function () {
cur.sec -= 1;
if (cur.sec <= 0) {
refreshMap();
cur.sec = {{$page_refresh}};
}
}, 1000);
},
Pause: function () {
clearInterval(this.interval);
delete this.interval;
},
};
Countdown.Start();
refreshMap();
});
</script>
@endsection