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
This commit is contained in:
eskyuu 2024-02-08 01:42:35 +08:00 committed by GitHub
parent d6ce29c052
commit cb09ae0d54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 680 additions and 57 deletions

View File

@ -30,6 +30,7 @@ use App\Http\Requests\CustomMapSettingsRequest;
use App\Models\CustomMap;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use LibreNMS\Config;
@ -47,6 +48,16 @@ class CustomMapController extends Controller
'maps' => CustomMap::orderBy('name')->get(['custom_map_id', 'name']),
'name' => 'New Map',
'node_align' => 10,
'edge_separation' => 10,
'reverse_arrows' => 0,
'legend' => [
'x' => -1,
'y' => -1,
'steps' => 7,
'hide_invalid' => 0,
'hide_overspeed' => 0,
'font_size' => 14,
],
'background' => null,
'map_conf' => [
'height' => '800px',
@ -75,8 +86,14 @@ class CustomMapController extends Controller
->header('Content-Type', 'text/plain');
}
public function show(CustomMap $map): View
public function show(Request $request, CustomMap $map): View
{
$request->validate([
'screenshot' => 'nullable|in:yes',
]);
$screenshot = $request->input('screenshot') === 'yes' ? 1 : 0;
$map_conf = $map->options;
$map_conf['width'] = $map->width;
$map_conf['height'] = $map->height;
@ -84,6 +101,8 @@ class CustomMapController extends Controller
'edit' => false,
'map_id' => $map->custom_map_id,
'name' => $map->name,
'reverse_arrows' => $map->reverse_arrows,
'legend' => $this->legendConfig($map),
'background' => (bool) $map->background_suffix,
'bgversion' => $map->background_version,
'page_refresh' => Config::get('page_refresh', 300),
@ -93,6 +112,7 @@ class CustomMapController extends Controller
'newnode_conf' => $map->newnodeconfig,
'vmargin' => 20,
'hmargin' => 20,
'screenshot' => $screenshot,
];
return view('map.custom-view', $data);
@ -104,6 +124,9 @@ class CustomMapController extends Controller
'map_id' => $map->custom_map_id,
'name' => $map->name,
'node_align' => $map->node_align,
'edge_separation' => $map->edge_separation,
'reverse_arrows' => $map->reverse_arrows,
'legend' => $this->legendConfig($map),
'newedge_conf' => $map->newedgeconfig,
'newnode_conf' => $map->newnodeconfig,
'map_conf' => $map->options,
@ -142,6 +165,8 @@ class CustomMapController extends Controller
'name' => $map->name,
'width' => $map->width,
'height' => $map->height,
'reverse_arrows' => $map->reverse_arrows,
'edge_separation' => $map->edge_separation,
]);
}
@ -164,4 +189,21 @@ class CustomMapController extends Controller
return $images;
}
/**
* Return the legend config
*/
private function legendConfig(CustomMap $map): array
{
$legend = [
'x' => $map->legend_x,
'y' => $map->legend_y,
'steps' => $map->legend_steps,
'hide_invalid' => $map->legend_hide_invalid,
'hide_overspeed' => $map->legend_hide_overspeed,
'font_size' => $map->legend_font_size,
];
return $legend;
}
}

View File

@ -55,6 +55,8 @@ class CustomMapDataController extends Controller
'reverse' => $edge->reverse,
'style' => $edge->style,
'showpct' => $edge->showpct,
'showbps' => $edge->showbps,
'label' => $edge->label,
'text_face' => $edge->text_face,
'text_size' => $edge->text_size,
'text_colour' => $edge->text_colour,
@ -122,6 +124,8 @@ class CustomMapDataController extends Controller
$edges[$edgeid]['colour_to'] = $this->speedColour($edges[$edgeid]['port_topct']);
$edges[$edgeid]['colour_from'] = $this->speedColour($edges[$edgeid]['port_frompct']);
}
$edges[$edgeid]['port_tobps'] = $this->rateString($rateto);
$edges[$edgeid]['port_frombps'] = $this->rateString($ratefrom);
$edges[$edgeid]['width_to'] = $this->speedWidth($speedto);
$edges[$edgeid]['width_from'] = $this->speedWidth($speedfrom);
}
@ -185,11 +189,20 @@ class CustomMapDataController extends Controller
'newedgeconf' => 'array',
'nodes' => 'array',
'edges' => 'array',
'legend_x' => 'integer',
'legend_y' => 'integer',
]);
$map->load(['nodes', 'edges']);
DB::transaction(function () use ($map, $data) {
if ($map->legend_x != $data['legend_x'] || $map->legend_y != $data['legend_y']) {
$map->legend_x = $data['legend_x'];
$map->legend_y = $data['legend_y'];
$map->save();
}
$dbnodes = $map->nodes->keyBy('custom_map_node_id')->all();
$dbedges = $map->edges->keyBy('custom_map_edge_id')->all();
@ -249,6 +262,8 @@ class CustomMapDataController extends Controller
$dbedge->port_id = $edge['port_id'] ? $edge['port_id'] : null;
$dbedge->reverse = filter_var($edge['reverse'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$dbedge->showpct = filter_var($edge['showpct'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$dbedge->showbps = filter_var($edge['showbps'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$dbedge->label = $edge['label'] ? $edge['label'] : '';
$dbedge->style = $edge['style'];
$dbedge->text_face = $edge['text_face'];
$dbedge->text_size = $edge['text_size'];
@ -274,6 +289,23 @@ class CustomMapDataController extends Controller
return response()->json(['id' => $map->custom_map_id]);
}
private function rateString(int $rate): string
{
if ($rate < 1000) {
return $rate . ' bps';
} elseif ($rate < 1000000) {
return intval($rate / 1000) . ' kbps';
} elseif ($rate < 1000000000) {
return intval($rate / 1000000) . ' Mbps';
} elseif ($rate < 1000000000000) {
return intval($rate / 1000000000) . ' Gbps';
} elseif ($rate < 1000000000000000) {
return intval($rate / 1000000000000) . ' Tbps';
} else {
return intval($rate / 1000000000000000) . ' Pbps';
}
}
private function snmpSpeed(string $speeds): int
{
// Only succeed if the string startes with a number optionally followed by a unit

View File

@ -25,6 +25,8 @@ class CustomMapSettingsRequest extends FormRequest
return [
'name' => 'required|string',
'node_align' => 'integer',
'reverse_arrows' => 'boolean',
'edge_separation' => 'integer',
'width_type' => 'in:px,%',
'width' => [
function (string $attribute, mixed $value, Closure $fail) {
@ -49,6 +51,12 @@ class CustomMapSettingsRequest extends FormRequest
}
},
],
'legend_x' => 'integer',
'legend_y' => 'integer',
'legend_steps' => 'integer',
'legend_font_size' => 'integer',
'legend_hide_invalid' => 'boolean',
'legend_hide_overspeed' => 'boolean',
];
}
}

View File

@ -44,6 +44,14 @@ class CustomMap extends BaseModel
'width',
'height',
'node_align',
'reverse_arrows',
'edge_separation',
'legend_x',
'legend_y',
'legend_steps',
'legend_font_size',
'legend_hide_invalid',
'legend_hide_overspeed',
'background_suffix',
'background_version',
];

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('custom_map_edges', function (Blueprint $table) {
$table->boolean('showbps')->default(0)->after('showpct');
$table->string('label', 255)->default('')->after('showbps');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('custom_map_edges', function (Blueprint $table) {
$table->dropColumn(['showbps', 'label']);
});
}
};

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('custom_maps', function (Blueprint $table) {
$table->boolean('reverse_arrows')->default(0)->after('node_align');
$table->smallInteger('edge_separation')->default(10)->after('reverse_arrows');
$table->integer('legend_x')->default(-1)->after('edge_separation');
$table->integer('legend_y')->default(-1)->after('legend_x');
$table->smallInteger('legend_steps')->default(7)->after('legend_y');
$table->smallInteger('legend_font_size')->default(14)->after('legend_steps');
$table->boolean('legend_hide_invalid')->default(0)->after('legend_font_size');
$table->boolean('legend_hide_overspeed')->default(0)->after('legend_hide_invalid');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('custom_maps', function (Blueprint $table) {
$table->dropColumn(['reverse_arrows', 'edge_separation', 'legend_x', 'legend_y', 'legend_steps', 'legend_steps', 'legend_hide_invalid', 'legend_hide_overspeed']);
});
}
};

View File

@ -25,6 +25,19 @@ Some key points about the viewer are:
- Red at 100% utilisation, with a gradual change to
- Purple at 150% utilisation and above
### Viewer URL options
You can manually add the following parameters to a URL to alter the display of a
custom map.
The following URL options are available:
- bare=yes : Removes the control bar from the top of the page.
- screenshot=yes : Removes all labels from the nodes and links
e.g. If you want bare and screenshot enabled, https://_nmsserver_/maps/custom/2
becomes https://_nmsserver_/maps/custom/2?bare=yes&screenshot=yes
## Editor
To access the custom map editor, a user must be an admin. The editor

View File

@ -36,6 +36,9 @@ return [
'width' => 'Width',
'height' => 'Height',
'alignment' => 'Node Alignment',
'edgeseparation' => 'Link Separation',
'reverse' => 'Reverse Arrows',
'enable_legend' => 'Enable Legend',
'saving' => 'Saving...',
'save_errors' => 'Save failed due to the following errors:',
'save_error' => 'Save failed. Server returned error response code: :code',
@ -45,6 +48,12 @@ return [
'edit' => 'Edit Map Settings',
'rerender' => 'Re-Render Map',
'save' => 'Save Map',
'legend' => [
'font_size' => 'Legend Text Size',
'steps' => 'Legend Steps',
'hideinvalid' => 'Hide Invalid',
'hideoverspeed' => 'Hide 100%+',
],
],
'node' => [
'new' => 'New Node',
@ -126,6 +135,8 @@ return [
'cubicBezier' => 'Cubic Bezier',
],
'show_usage_percent' => 'Show percent usage',
'show_usage_bps' => 'Show bps usage',
'label' => 'Label',
'recenter' => 'Recenter Line',
],
'validate' => [

View File

@ -522,6 +522,14 @@ custom_maps:
- { Field: width, Type: varchar(10), 'Null': false, Extra: '' }
- { Field: height, Type: varchar(10), 'Null': false, Extra: '' }
- { Field: node_align, Type: smallint, 'Null': false, Extra: '', Default: '0' }
- { Field: reverse_arrows, Type: tinyint, 'Null': false, Extra: '', Default: '0' }
- { Field: edge_separation, Type: smallint, 'Null': false, Extra: '', Default: '10' }
- { Field: legend_x, Type: int, 'Null': false, Extra: '', Default: '-1' }
- { Field: legend_y, Type: int, 'Null': false, Extra: '', Default: '-1' }
- { Field: legend_steps, Type: smallint, 'Null': false, Extra: '', Default: '7' }
- { Field: legend_font_size, Type: smallint, 'Null': false, Extra: '', Default: '14' }
- { Field: legend_hide_invalid, Type: tinyint, 'Null': false, Extra: '', Default: '0' }
- { Field: legend_hide_overspeed, Type: tinyint, 'Null': false, Extra: '', Default: '0' }
- { Field: background_suffix, Type: varchar(10), 'Null': true, Extra: '' }
- { Field: background_version, Type: 'int unsigned', 'Null': false, Extra: '' }
- { Field: options, Type: longtext, 'Null': true, Extra: '' }
@ -553,6 +561,8 @@ custom_map_edges:
- { Field: reverse, Type: tinyint, 'Null': false, Extra: '' }
- { Field: style, Type: varchar(50), 'Null': false, Extra: '' }
- { Field: showpct, Type: tinyint, 'Null': false, Extra: '' }
- { Field: showbps, Type: tinyint, 'Null': false, Extra: '', Default: '0' }
- { Field: label, Type: varchar(255), 'Null': false, Extra: '', Default: '' }
- { Field: text_face, Type: varchar(50), 'Null': false, Extra: '' }
- { Field: text_size, Type: int, 'Null': false, Extra: '' }
- { Field: text_colour, Type: varchar(10), 'Null': false, Extra: '' }

View File

@ -62,12 +62,24 @@
</select>
</div>
</div>
<div class="form-group row">
<div class="form-group row single-node">
<label for="edgetextshow" class="col-sm-3 control-label">{{ __('map.custom.edit.edge.show_usage_percent') }}</label>
<div class="col-sm-9">
<input class="form-check-input" type="checkbox" role="switch" id="edgetextshow">
</div>
</div>
<div class="form-group row">
<label for="edgebpsshow" class="col-sm-3 control-label">{{ __('map.custom.edit.edge.show_usage_bps') }}</label>
<div class="col-sm-9">
<input class="form-check-input" type="checkbox" role="switch" id="edgebpsshow">
</div>
</div>
<div class="form-group row">
<label for="edgelabel" class="col-sm-3 control-label">{{ __('map.custom.edit.edge.label') }}</label>
<div class="col-sm-9">
<input type=text id="edgelabel" class="form-control input-sm" value="" />
</div>
</div>
<div class="form-group row">
<label for="edgetextface" class="col-sm-3 control-label">{{ __('map.custom.edit.text_font') }}</label>
<div class="col-sm-9">

View File

@ -59,12 +59,136 @@
var network;
var network_height;
var network_width;
var node_align = {{$node_align}};
var network_nodes = new vis.DataSet({queue: {delay: 100}});
var network_edges = new vis.DataSet({queue: {delay: 100}});
var edge_nodes_map = [];
var node_device_map = {};
var custom_image_base = "{{ $base_url }}images/custommap/icons/";
function edgeNodesRemove(nm_id, edgeid) {
// Remove old item from map if it exists
if (nm_id in edge_nodes_map) {
const edge_idx = edge_nodes_map[nm_id].indexOf(edgeid);
if (edge_idx >= 0) {
edge_nodes_map[nm_id].splice(edge_idx, 1);
}
}
}
function edgeNodesUpdate(edgeid, node1_id, node2_id, old_node1_id, old_node2_id) {
var nm_id = node1_id < node2_id ? node1_id + '.' + node2_id : node2_id + '.' + node1_id;
var old_nm_id = old_node1_id < old_node2_id ? old_node1_id + '.' + old_node2_id : old_node2_id + '.' + old_node1_id;
// No update is needed if the new and old are the same
if (nm_id == old_nm_id) {
return;
}
if (old_node1_id > 0 && old_node2_id > 0) {
edgeNodesRemove(old_nm_id, edgeid);
}
if (!(nm_id in edge_nodes_map)) {
edge_nodes_map[nm_id] = [];
}
edge_nodes_map[nm_id].push(edgeid);
}
function getMidOffests(pos1, pos2) {
// First work out which pos is on the left-hand side
var left_pos;
var right_pos;
if(pos1.x < pos2.x) {
left_pos = pos1;
right_pos = pos2;
} else {
left_pos = pos2;
right_pos = pos1;
}
// The X axis needs to move left/right based on whether the line rises or falls
var x_diff = right_pos.y - left_pos.y;
// The Y axis needs to move up always based on how far apart the left and right nodes are
var y_diff = left_pos.x - right_pos.x;
// Calculate how far each mid point needs to move
var tot_diff = Math.abs(x_diff) + Math.abs(y_diff);
return {x: Math.round(edge_sep * (x_diff / tot_diff)), y: Math.round(edge_sep * (y_diff / tot_diff))};
}
function getMidPos(edgeid, from_id, to_id) {
var nm_id = from_id < to_id ? from_id + '.' + to_id : to_id + '.' + from_id;
const node_links = nm_id in edge_nodes_map ? edge_nodes_map[nm_id] : [];
var node_offsets = [];
node_links.forEach((link_edgeid) => {
// Ignore the edge we are creating
if (link_edgeid == edgeid) {
return;
}
// Save the offset in the hash
let link_mid = network_nodes.get(link_edgeid + "_mid");
let link_mid_offset = link_mid.x + '.' + link_mid.y;
node_offsets[link_mid_offset] = true;
});
var pos = network.getPositions([from_id, to_id]);
const offsets = getMidOffests(pos[from_id], pos[to_id]);
// Calculate the center point
var mid_center = {x: (pos[from_id].x + pos[to_id].x) >> 1, y: (pos[from_id].y + pos[to_id].y) >> 1};
var mids = [mid_center];
for (let i = 1; i < node_links.length; i++) {
let multiplier = ((i + 1) >> 1);
let this_x = mid_center.x;
let this_y = mid_center.y;
if(i & 1) {
// Odd numbers go the normal direction
mids.push({x: mid_center.x + (multiplier * offsets.x), y: mid_center.y + (multiplier * offsets.y)});
} else {
// Even numbers go the opposite direction
mids.push({x: mid_center.x - (multiplier * offsets.x), y: mid_center.y - (multiplier * offsets.y)});
}
}
// Find the first unused mid point from the center
for (let i = 0; i < mids.length; i++) {
let this_offset = mids[i].x + '.' + mids[i].y;
if (!(this_offset in node_offsets)) {
return {x: mids[i].x, y: mids[i].y};
}
}
// Default to mid point
return {x: mid_center.x, y: mid_center.y};
}
function fixNodePos(nodeid, node) {
var move=false;
if ( node_align && !nodeid.endsWith("_mid")) {
node.x = Math.round(node.x / node_align) * node_align;
node.y = Math.round(node.y / node_align) * node_align;
move = true;
}
if ( node.x < {{ $hmargin }} ) {
node.x = {{ $hmargin }};
move = true;
} else if ( node.x > network_width - {{ $hmargin }} ) {
node.x = network_width - {{ $hmargin }};
move = true;
}
if ( node.y < {{ $vmargin }} ) {
node.y = {{ $vmargin }};
move = true;
} else if ( node.y > network_height - {{ $vmargin }} ) {
node.y = network_height - {{ $vmargin }};
move = true;
}
return move;
}
function CreateNetwork() {
// Flush the nodes and edges so they are rendered immediately
network_nodes.flush();
@ -120,12 +244,15 @@
return;
}
var pos = network.getPositions([data.from, data.to]);
var mid_x = (pos[data.from].x + pos[data.to].x) >> 1;
var mid_y = (pos[data.from].y + pos[data.to].y) >> 1;
var edgeid = "new" + newcount++;
edgeNodesUpdate(edgeid, data.from, data.to, -1, -1);
const mid_pos = getMidPos(edgeid, data.from, data.to);
// Default to using the center point
var mid_x = mid_pos.x;
var mid_y = mid_pos.y;
var mid = {id: edgeid + "_mid", shape: "dot", size: 3, x: mid_x, y: mid_y};
var edge1 = structuredClone(newedgeconf);
@ -185,26 +312,21 @@
// Make sure a node is not dragged outside the canvas
nodepos = network.getPositions(data.nodes);
$.each( nodepos, function( nodeid, node ) {
move = false;
if ( node_align && !nodeid.endsWith("_mid")) {
node.x = Math.round(node.x / node_align) * node_align;
node.y = Math.round(node.y / node_align) * node_align;
move = true;
}
if ( node.x < {{ $hmargin }} ) {
node.x = {{ $hmargin }};
move = true;
} else if ( node.x > network_width - {{ $hmargin }} ) {
node.x = network_width - {{ $hmargin }};
move = true;
}
if ( node.y < {{ $vmargin }} ) {
node.y = {{ $vmargin }};
move = true;
} else if ( node.y > network_height - {{ $vmargin }} ) {
node.y = network_height - {{ $vmargin }};
move = true;
if ( nodeid == "legend_header" ) {
// If the legend header was moved, just redraw it
fixNodePos(nodeid, node);
legend.x = node.x;
legend.y = node.y;
redrawLegend();
return;
} else if ( nodeid.startsWith("legend_") ) {
// Get the original node and move it back
node = network_nodes.get(nodeid);
network_nodes.update(node);
return;
}
let move = fixNodePos(nodeid, node);
if ( move ) {
network.moveNode(nodeid, node.x, node.y);
}
@ -242,12 +364,87 @@
window.location.href = "{{ route('maps.custom.index') }}";
}
function swapArrows(reverse) {
var arrows;
if (reverse) {
arrows = {from: {enabled: true, scaleFactor: 0.6}, to: {enabled: false}};
} else {
arrows = {to: {enabled: true, scaleFactor: 0.6}, from: {enabled: false}};
}
network_edges.forEach((edge) => {
edge.arrows = arrows;
network_edges.update(edge);
});
network_edges.flush();
}
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 editMapSuccess(data) {
$("#title").text(data.name);
$("#savemap-alert").attr("class", "col-sm-12");
$("#savemap-alert").text("");
network.setSize(data.width, data.height);
edge_sep = data.edge_separation;
if(reverse_arrows != parseInt(data.reverse_arrows)) {
swapArrows(Boolean(parseInt(data.reverse_arrows)));
}
reverse_arrows = parseInt(data.reverse_arrows);
redrawLegend();
editMapCancel();
}
@ -261,11 +458,13 @@
var edges = {};
$.each(network_nodes.get(), function (node_idx, node) {
if(node.id.endsWith("_mid")) {
if(node.id.startsWith("legend_")) {
return;
} else if(node.id.endsWith("_mid")) {
edgeid = node.id.split("_")[0];
edge1 = network_edges.get(edgeid + "_from");
edge2 = network_edges.get(edgeid + "_to");
edges[edgeid] = {id: edgeid, text_colour: edge1.font.color, text_size: edge1.font.size, text_face: edge1.font.face, from: edge1.from, to: edge2.from, showpct: (edge1.label ? true : false), port_id: edge1.title, style: edge1.smooth.type, mid_x: node.x, mid_y: node.y, reverse: (edgeid in edge_port_map ? edge_port_map[edgeid].reverse : false)};
edges[edgeid] = {id: edgeid, text_colour: edge1.font.color, text_size: edge1.font.size, text_face: edge1.font.face, from: edge1.from, to: edge2.from, showpct: (edge1.label != null && edge1.label.includes("xx%")), showbps: (edge1.label != null && edge1.label.includes("bps")), label: (node.label || ''), port_id: edge1.title, style: edge1.smooth.type, mid_x: node.x, mid_y: node.y, reverse: (edgeid in edge_port_map ? edge_port_map[edgeid].reverse : false)};
} else {
if(node.icon.code) {
node.icon = node.icon.code.charCodeAt(0).toString(16);
@ -290,6 +489,8 @@
newedgeconf: newedgeconf,
nodes: nodes,
edges: edges,
legend_x: legend.x,
legend_y: legend.y,
},
dataType: 'json',
type: 'POST'
@ -646,7 +847,8 @@
$("#edgetextface").val(newedgeconf.font.face);
$("#edgetextsize").val(newedgeconf.font.size);
$("#edgetextcolour").val(newedgeconf.font.color);
$("#edgetextshow").bootstrapSwitch('state', Boolean(newedgeconf.label));
$("#edgetextshow").bootstrapSwitch('state', (newedgeconf.label.includes('xx%') || newedgeconf.label.includes('true')));
$("#edgebpsshow").bootstrapSwitch('state', (newedgeconf.label.includes('bps')));
$('#edgecolourtextreset').attr('disabled', 'disabled');
$("#edge-saveButton").hide();
@ -654,13 +856,30 @@
$('#edgeModal').modal({backdrop: 'static', keyboard: false}, 'show');
}
function edgeLabel(show_pct, show_bps, default_val) {
var label = '';
if(show_pct) {
label = 'xx%';
}
if(show_bps) {
if(Boolean(label.length)) {
label += "\n";
}
label += 'xx bps';
}
if(Boolean(label.length)) {
return label;
}
return default_val;
}
function editEdgeDefaultsSave() {
editEdgeHide();
newedgeconf.smooth.type = $("#edgestyle").val();
newedgeconf.font.face = $("#edgetextface").val();
newedgeconf.font.size = $("#edgetextsize").val();
newedgeconf.font.color = $("#edgetextcolour").val();
newedgeconf.label = $("#edgetextshow").prop('checked');
newedgeconf.label = edgeLabel($("#edgetextshow").prop('checked'), $("#edgebpsshow").prop('checked'), '');
$("#map-saveDataButton").show();
}
@ -690,7 +909,8 @@
$("#edgetextface").val(edgedata.edge1.font.face);
$("#edgetextsize").val(edgedata.edge1.font.size);
$("#edgetextcolour").val(edgedata.edge1.font.color);
$("#edgetextshow").bootstrapSwitch('state', Boolean(edgedata.edge1.label));
$("#edgetextshow").bootstrapSwitch('state', (edgedata.edge1.label != null && edgedata.edge1.label.includes('xx%')));
$("#edgebpsshow").bootstrapSwitch('state', (edgedata.edge1.label != null && edgedata.edge1.label.includes('bps')));
$("#edgeRecenterRow").show();
$("#divEdgeFrom").show();
@ -705,6 +925,8 @@
function editEdgeSave(event) {
edgedata = event.data.data;
edgeNodesUpdate(edgedata.id, $("#edgefrom").val(), $("#edgeto").val(), edgedata.edge1.from, edgedata.edge2.from);
editEdgeHide();
edgedata.edge1.smooth.type = $("#edgestyle").val();
edgedata.edge2.smooth.type = $("#edgestyle").val();
@ -713,8 +935,9 @@
edgedata.edge1.font.face = edgedata.edge2.font.face = $("#edgetextface").val();
edgedata.edge1.font.size = edgedata.edge2.font.size = $("#edgetextsize").val();
edgedata.edge1.font.color = edgedata.edge2.font.color = $("#edgetextcolour").val();
edgedata.edge1.label = edgedata.edge2.label = $("#edgetextshow").prop('checked') ? "xx%" : null;
edgedata.edge1.label = edgedata.edge2.label = edgeLabel($("#edgetextshow").prop('checked'), $("#edgebpsshow").prop('checked'), null);
edgedata.edge1.title = edgedata.edge2.title = $("#port_id").val();
edgedata.mid.label = ($("#edgelabel").val() || '');
if(edgedata.id) {
if($("#port_id").val()) {
@ -738,14 +961,14 @@
network_edges.flush();
} else {
network_edges.update([edgedata.edge1, edgedata.edge2]);
network_nodes.update([edgedata.mid]);
if($("#edgerecenter").is(":checked")) {
var pos = network.getPositions([edgedata.edge1.from, edgedata.edge2.from]);
var mid_x = (pos[edgedata.edge1.from].x + pos[edgedata.edge2.from].x) >> 1;
var mid_y = (pos[edgedata.edge1.from].y + pos[edgedata.edge2.from].y) >> 1;
const mid_pos = getMidPos(edgedata.id, edgedata.edge1.from, edgedata.edge2.from);
edgedata.mid.x = mid_x;
edgedata.mid.y = mid_y;
edgedata.mid.x = mid_pos.x;
edgedata.mid.y = mid_pos.y;
network_nodes.update([edgedata.mid]);
$("#map-renderButton").show();
}
@ -794,6 +1017,10 @@
}
function deleteEdge(edgeid) {
const edge1 = network_edges.get(edgeid + "_from");
const edge2 = network_edges.get(edgeid + "_to");
var nm_id = edge1.from < edge2.from ? edge1.from + '.' + edge2.from : edge2.from + '.' + edge1.from;
edgeNodesRemove(nm_id, edgeid);
network_edges.remove(edgeid + "_to");
network_edges.remove(edgeid + "_from");
network_edges.flush();
@ -803,6 +1030,7 @@
}
function refreshMap() {
edge_nodes_map = [];
$.get( '{{ route('maps.custom.data', ['map' => $map_id]) }}')
.done(function( data ) {
// Add/update nodes
@ -853,14 +1081,23 @@
});
$.each( data.edges, function( edgeid, edge) {
edgeNodesUpdate(edgeid, edge.custom_map_node1_id, edge.custom_map_node2_id, -1, -1);
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};
var mid = {id: edgeid + "_mid", shape: "dot", size: 0, x: mid_x, y: mid_y, label: edge.label};
mid.size = 3;
var edge1 = {id: edgeid + "_from", from: edge.custom_map_node1_id, to: edgeid + "_mid", arrows: {to: {enabled: true, scaleFactor: 0.6}}, 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: {to: {enabled: true, scaleFactor: 0.6}}, font: {face: edge.text_face, size: edge.text_size, color: edge.text_colour}, smooth: {type: edge.style}};
var arrows;
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") {
@ -874,11 +1111,7 @@
} else {
edge1.title = edge2.title = '';
}
if(edge.showpct) {
edge1.label = edge2.label = 'xx%';
} else {
edge1.label = edge2.label = '';
}
edge1.label = edge2.label = edgeLabel(edge.showpct, edge.showbps, '');
if (network_nodes.get(mid.id)) {
network_nodes.update(mid);
network_edges.update(edge1);
@ -905,6 +1138,9 @@
}
});
// Add the legend back to the map
redrawLegend();
// Flush in order to make sure nodes exist for edges to connect to
network_nodes.flush();
network_edges.flush();

View File

@ -33,6 +33,48 @@
<input type="number" id="mapnodealign" name="mapnodealign" class="form-control input-sm" value="{{ $node_align ?? 10 }}">
</div>
</div>
<div class="form-group row">
<label for="mapedgesep" class="col-sm-3 control-label">{{ __('map.custom.edit.map.edgeseparation') }}</label>
<div class="col-sm-9">
<input type="number" id="mapedgesep" name="mapedgesep" class="form-control input-sm" value="{{ $edge_separation ?? 10 }}">
</div>
</div>
<div class="form-group row">
<label for="mapreversearrows" class="col-sm-3 control-label">{{ __('map.custom.edit.map.reverse') }}</label>
<div class="col-sm-9">
<input class="form-check-input" type="checkbox" role="switch" id="mapreversearrows">
</div>
</div>
<div class="form-group row">
<label for="maplegend" class="col-sm-3 control-label">{{ __('map.custom.edit.map.enable_legend') }}</label>
<div class="col-sm-9">
<input class="form-check-input" type="checkbox" role="switch" id="maplegend" onChange="toggleMapLegend()">
</div>
</div>
<div class="form-group row maplegend">
<label for="maplegendfontsize" class="col-sm-4 control-label">{{ __('map.custom.edit.map.legend.font_size') }}</label>
<div class="col-sm-8">
<input type=number id="maplegendfontsize" class="form-control input-sm" value={{ $legend['font_size'] }} />
</div>
</div>
<div class="form-group row maplegend">
<label for="maplegendsteps" class="col-sm-4 control-label">{{ __('map.custom.edit.map.legend.steps') }}</label>
<div class="col-sm-8">
<input type=number id="maplegendsteps" class="form-control input-sm" value={{ $legend['steps'] }} />
</div>
</div>
<div class="form-group row maplegend">
<label for="maplegendhideinvalid" class="col-sm-4 control-label">{{ __('map.custom.edit.map.legend.hideinvalid') }}</label>
<div class="col-sm-8">
<input class="form-check-input" type="checkbox" role="switch" id="maplegendhideinvalid">
</div>
</div>
<div class="form-group row maplegend">
<label for="maplegendhideoverspeed" class="col-sm-4 control-label">{{ __('map.custom.edit.map.legend.hideoverspeed') }}</label>
<div class="col-sm-8">
<input class="form-check-input" type="checkbox" role="switch" id="maplegendhideoverspeed">
</div>
</div>
<hr>
<div class="row">
<div class="col-sm-12" id="savemap-alert">
@ -53,6 +95,11 @@
</div>
<script>
var node_align = {{$node_align}};
var edge_sep = {{$edge_separation}};
var reverse_arrows = {{$reverse_arrows}};
var legend = @json($legend);
function saveMapSettings() {
$("#map-saveButton").attr('disabled','disabled');
$("#savemap-alert").text('{{ __('map.custom.edit.map.saving') }}');
@ -63,6 +110,34 @@
var height = $("#mapheight").val();
var node_align = $("#mapnodealign").val();
var mapwdith = 100;
if (!isNaN(width)) {
mapwidth = width;
} else if (width.includes("px")) {
mapwidth = width.replace("px", "");
} else if (width.includes("%")) {
mapwidth = window.innerWidth * width.replace("%", "") / 100;
}
// Update the x and y coordinates
if ($("#maplegend").prop('checked')) {
if (legend.x < 0) {
legend.x = mapwidth - 50;
legend.y = 100;
}
} else {
legend.x = -1;
legend.y = -1;
}
legend.font_size = parseInt($("#maplegendfontsize").val());
legend.steps = parseInt($("#maplegendsteps").val());
legend.hide_invalid = $("#maplegendhideinvalid").prop('checked') ? 1 : 0;
legend.hide_overspeed = $("#maplegendhideoverspeed").prop('checked') ? 1 : 0;
var map_reverse_arrows = $("#mapreversearrows").prop('checked') ? 1 : 0;
var map_edge_sep = $("#mapedgesep").val();
if(!isNaN(width)) {
width = width + "px";
}
@ -84,7 +159,15 @@
name: name,
width: width,
height: height,
node_align: node_align
node_align: node_align,
reverse_arrows: map_reverse_arrows,
edge_separation: map_edge_sep,
legend_x: legend.x,
legend_y: legend.y,
legend_steps: legend.steps,
legend_font_size: legend.font_size,
legend_hide_invalid: legend.hide_invalid,
legend_hide_overspeed: legend.hide_overspeed,
},
dataType: 'json',
type: method
@ -105,4 +188,22 @@
$("#map-saveButton").removeAttr('disabled');
});
}
function toggleMapLegend() {
if($("#maplegend").prop('checked')) {
$(".maplegend").show();
} else {
$(".maplegend").hide();
}
}
$(document).ready(function () {
if(legend.x < 0 || legend.y < 0) {
$(".maplegend").hide();
}
$("#mapreversearrows").bootstrapSwitch('state', Boolean(reverse_arrows));
$("#maplegend").bootstrapSwitch('state', (legend.x >= 0 && legend.y >= 0));
$("#maplegendhideinvalid").bootstrapSwitch('state', Boolean(legend.hide_invalid));
$("#maplegendhideoverspeed").bootstrapSwitch('state', Boolean(legend.hide_overspeed));
});
</script>

View File

@ -26,6 +26,9 @@
@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;
@ -36,6 +39,60 @@
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();
@ -95,7 +152,7 @@
} else {
node_cfg.title = null;
}
node_cfg.label = node.label;
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;
@ -134,10 +191,16 @@
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};
var mid = {id: edgeid + "_mid", shape: "dot", size: 0, x: mid_x, y: mid_y, label: screenshot ? '' : edge.label};
var edge1 = {id: edgeid + "_from", from: edge.custom_map_node1_id, to: edgeid + "_mid", arrows: {to: {enabled: true, scaleFactor: 0.6}}, 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: {to: {enabled: true, scaleFactor: 0.6}}, font: {face: edge.text_face, size: edge.text_size, color: edge.text_colour}, smooth: {type: edge.style}};
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") {
@ -146,15 +209,35 @@
edge2.smooth.type = "curvedCW";
}
if(edge.port_id) {
edge1.title = edge2.title = edge.port_info;
if(edge.showpct) {
edge1.label = edge.port_frompct + "%";
edge2.label = edge.port_topct + "%";
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;
}
edge1.color = {color: edge.colour_from};
edge1.width = edge.width_from;
edge2.color = {color: edge.colour_to};
edge2.width = edge.width_to;
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 {
@ -186,6 +269,9 @@
}
});
// Re-draw the legend
redrawLegend();
// Flush in order to make sure nodes exist for edges to connect to
network_nodes.flush();
network_edges.flush();