Add functionality for custom maps (weathermaps) (#15633)
* Initial commit with editor. * Added custom map models and database migrations. Modified the controller and view to support saving the custom map settings to the database * Added menu items and sorted out access permissions for maps and nodes * Cleaned up some of the conditions in the javascript section of the blade * Started work on the map data save * Save of map nodes and edges is complete * Got the map to load data on page load and added the delete functionality * Fixed a typo and made link colour black if intertface is down * Various usability fix-ups * Show the save button on node and edge delete * Fixed up access for users without global read * Increase typeahead search size and standardised the way modals are triggered. * Update data fetch to copy values into array so I can add more fields * Convert blank array check to use count() * Formatting changes * More formatting fixes * Formatting again * DB schema update * Revert previous commit * Pass device id to pages * Remove bad characters from javascript * Re-add the - character in search results * Update to avoid background colour being set to the current colour for offline devices * Fixed a bug in speed detection when no suffix is given * Fixed up the speed colour calculation and added comments * Update default edge font size to 12 * Reduce arrow size * Formatting fix * Update the custom map controller to handle null interface speeds * Alter JSON columns to be longtext instead * Only refresh map data on successful save * Update labels on default settings to make it clear that they are not saved * Added timestamps to all custom map tables Use HasFactory instead of static definitions for custom map tables convert JSON DB fields to longtext and updated PHP to do the appropriate JSON decoding as a result * Added missing vis.js images for the editor * Split the custom map blade into different pages * formatting fixes * Initial commit with editor. * Added custom map models and database migrations. Modified the controller and view to support saving the custom map settings to the database * Added menu items and sorted out access permissions for maps and nodes * Cleaned up some of the conditions in the javascript section of the blade * Started work on the map data save * Save of map nodes and edges is complete * Got the map to load data on page load and added the delete functionality * Various usability fix-ups * Show the save button on node and edge delete * Fixed up access for users without global read * Increase typeahead search size and standardised the way modals are triggered. * Convert blank array check to use count() * Formatting changes * More formatting fixes * Formatting again * DB schema update * Revert previous commit * Pass device id to pages * Remove bad characters from javascript * Re-add the - character in search results * Update to avoid background colour being set to the current colour for offline devices * Reduce arrow size * Only refresh map data on successful save * Update labels on default settings to make it clear that they are not saved * Added timestamps to all custom map tables Use HasFactory instead of static definitions for custom map tables convert JSON DB fields to longtext and updated PHP to do the appropriate JSON decoding as a result * Added missing vis.js images for the editor * Split the custom map blade into different pages * Updated the custom maps to use the select2 searches for ports and devices * Fix port search clearing with select2 * Update DB schema to add timestamps * Add the ability to set a node alignment value where nodes will align to a grid * Add a checkbox to re-center edge lines * Schema update for node alignment * Removed unused route * Fixups after rebase * Remove DevicePortSearchController * Rebase fixups * Remove unneeded controller * Formatting fixes * Update all network map documentation * Fixed typo in doc * Change background imgae database migration * Update migration for custom map background to fix schema error * Place a try/catch around the BLOB->MEDIUMBLOB migration * Formatting fix * Moved custom map background image location and added some SVG images to test as image options * Updated the editor to use a static set of device images * Update the image logic in the editor and added to the viewer * DB Schema update * Formatting * remove svg height/width attributes * Added some more icon options for arrows * Added database migration to allow nodes to link to another custom map Fixed an error in the image migration * Added the ability to link a node to another custom map * Formatting fixes * DB Schema update * Remove images-custom directory * Explicitly cast map ID to int * Made the image selection list dynamic based on the contents of the custom map icons directory * Formatting fix * Double-clicking on a link will take you to the link * Remove whitespace * Add translations fix an xss and hopefully not add any new ones refactor node image to use translations with fallback * split modals out into separate files return width/height to avoid js scope issues * Formatting fixes * refactor edit select page into a "manage" page Still left: validation/custom request Controller refactor ui tweaks * MapSettingsRequest * Refactor more routes, policy, controller I think this is the last refactor. Everything is now organized in a standard way. Missing a method to check if a user has access to a map * Fix booleans and style * Add versioning to the background image to prevent browser caching * Fixed the background image update by splitting it into a separate modal Changed the delete button on the map editor screen to return to the map list * Formatting fix * Added double-click actions in editor to edit nodes and edges --------- Co-authored-by: Tony Murray <murraytony@gmail.com>
94
app/Http/Controllers/Maps/CustomMapBackgroundController.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* CustomMapController.php
|
||||
*
|
||||
* Controller for custom maps
|
||||
*
|
||||
* 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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @link https://www.librenms.org
|
||||
*
|
||||
* @copyright 2023 Steven Wilton
|
||||
* @author Steven Wilton <swilton@fluentit.com.au>
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Maps;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CustomMap;
|
||||
use App\Models\CustomMapBackground;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class CustomMapBackgroundController extends Controller
|
||||
{
|
||||
public function get(CustomMap $map)
|
||||
{
|
||||
$this->authorize('view', $map);
|
||||
|
||||
$background = $this->checkImageCache($map);
|
||||
if ($background) {
|
||||
$path = Storage::disk('base')->path('html/images/custommap/background/' . $background);
|
||||
|
||||
return response()->file($path, [
|
||||
'Content-Type' => Storage::mimeType($background),
|
||||
]);
|
||||
}
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function save(FormRequest $request, CustomMap $map)
|
||||
{
|
||||
$this->authorize('update', $map);
|
||||
|
||||
if ($request->bgimage) {
|
||||
$map->background_suffix = $request->bgimage->extension();
|
||||
if (! $map->background) {
|
||||
$background = new CustomMapBackground;
|
||||
$background->background_image = $request->bgimage->getContent();
|
||||
$map->background()->save($background);
|
||||
} else {
|
||||
$map->background->background_image = $request->bgimage->getContent();
|
||||
$map->background->save();
|
||||
}
|
||||
$map->background_version++;
|
||||
$map->save();
|
||||
} elseif ($request->bgclear) {
|
||||
if ($map->background) {
|
||||
$map->background->delete();
|
||||
}
|
||||
$map->background_suffix = null;
|
||||
$map->save();
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'bgimage' => $map->background_suffix ? true : false,
|
||||
'bgversion' => $map->background_version,
|
||||
]);
|
||||
}
|
||||
|
||||
private function checkImageCache(CustomMap $map): ?string
|
||||
{
|
||||
if (! $map->background_suffix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$imageName = $map->custom_map_id . '_' . $map->background_version . '.' . $map->background_suffix;
|
||||
if (Storage::disk('base')->missing('html/images/custommap/background/' . $imageName)) {
|
||||
Storage::disk('base')->put('html/images/custommap/background/' . $imageName, $map->background->background_image);
|
||||
}
|
||||
|
||||
return $imageName;
|
||||
}
|
||||
}
|
165
app/Http/Controllers/Maps/CustomMapController.php
Normal file
@ -0,0 +1,165 @@
|
||||
<?php
|
||||
/**
|
||||
* CustomMapController.php
|
||||
*
|
||||
* Controller for custom maps
|
||||
*
|
||||
* 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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @link https://www.librenms.org
|
||||
*
|
||||
* @copyright 2023 Steven Wilton
|
||||
* @author Steven Wilton <swilton@fluentit.com.au>
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Maps;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\CustomMapSettingsRequest;
|
||||
use App\Models\CustomMap;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use LibreNMS\Config;
|
||||
|
||||
class CustomMapController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->authorizeResource(CustomMap::class, 'map');
|
||||
}
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
return view('map.custom-manage', [
|
||||
'maps' => CustomMap::orderBy('name')->get(['custom_map_id', 'name']),
|
||||
'name' => 'New Map',
|
||||
'node_align' => 10,
|
||||
'background' => null,
|
||||
'map_conf' => [
|
||||
'height' => '800px',
|
||||
'width' => '1800px',
|
||||
'interaction' => [
|
||||
'dragNodes' => true,
|
||||
'dragView' => false,
|
||||
'zoomView' => false,
|
||||
],
|
||||
'manipulation' => [
|
||||
'enabled' => true,
|
||||
'initiallyActive' => true,
|
||||
],
|
||||
'physics' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(CustomMap $map): Response
|
||||
{
|
||||
$map->delete();
|
||||
|
||||
return response('Success', 200)
|
||||
->header('Content-Type', 'text/plain');
|
||||
}
|
||||
|
||||
public function show(CustomMap $map): View
|
||||
{
|
||||
$map_conf = $map->options;
|
||||
$map_conf['width'] = $map->width;
|
||||
$map_conf['height'] = $map->height;
|
||||
$data = [
|
||||
'edit' => false,
|
||||
'map_id' => $map->custom_map_id,
|
||||
'name' => $map->name,
|
||||
'background' => (bool) $map->background_suffix,
|
||||
'bgversion' => $map->background_version,
|
||||
'page_refresh' => Config::get('page_refresh', 300),
|
||||
'map_conf' => $map_conf,
|
||||
'newedge_conf' => $map->newedgeconfig,
|
||||
'newnode_conf' => $map->newnodeconfig,
|
||||
'vmargin' => 20,
|
||||
'hmargin' => 20,
|
||||
];
|
||||
|
||||
return view('map.custom-view', $data);
|
||||
}
|
||||
|
||||
public function edit(CustomMap $map): View
|
||||
{
|
||||
$data = [
|
||||
'map_id' => $map->custom_map_id,
|
||||
'name' => $map->name,
|
||||
'node_align' => $map->node_align,
|
||||
'newedge_conf' => $map->newedgeconfig,
|
||||
'newnode_conf' => $map->newnodeconfig,
|
||||
'map_conf' => $map->options,
|
||||
'background' => (bool) $map->background_suffix,
|
||||
'bgversion' => $map->background_version,
|
||||
'edit' => true,
|
||||
'vmargin' => 20,
|
||||
'hmargin' => 20,
|
||||
'images' => $this->listNodeImages(),
|
||||
'maps' => CustomMap::orderBy('name')->where('custom_map_id', '<>', $map->custom_map_id)->get(['custom_map_id', 'name']),
|
||||
];
|
||||
|
||||
$data['map_conf']['width'] = $map->width;
|
||||
$data['map_conf']['height'] = $map->height;
|
||||
// Override some settings for the editor
|
||||
$data['map_conf']['interaction'] = ['dragNodes' => true, 'dragView' => false, 'zoomView' => false];
|
||||
$data['map_conf']['manipulation'] = ['enabled' => true, 'initiallyActive' => true];
|
||||
$data['map_conf']['physics'] = ['enabled' => false];
|
||||
|
||||
return view('map.custom-edit', $data);
|
||||
}
|
||||
|
||||
public function store(CustomMapSettingsRequest $request): JsonResponse
|
||||
{
|
||||
return $this->update($request, new CustomMap);
|
||||
}
|
||||
|
||||
public function update(CustomMapSettingsRequest $request, CustomMap $map): JsonResponse
|
||||
{
|
||||
$map->fill($request->validated());
|
||||
$map->save(); // save to get ID
|
||||
|
||||
return response()->json([
|
||||
'id' => $map->custom_map_id,
|
||||
'name' => $map->name,
|
||||
'width' => $map->width,
|
||||
'height' => $map->height,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all available node images with a label.
|
||||
*/
|
||||
private function listNodeImages(): array
|
||||
{
|
||||
$images = [];
|
||||
$image_translations = __('map.custom.edit.node.image_options');
|
||||
|
||||
foreach (Storage::disk('base')->files('html/images/custommap/icons') as $image) {
|
||||
if (in_array(strtolower(pathinfo($image, PATHINFO_EXTENSION)), ['svg', 'png', 'jpg', 'gif'])) {
|
||||
$file = pathinfo($image, PATHINFO_BASENAME);
|
||||
$filename = pathinfo($image, PATHINFO_FILENAME);
|
||||
|
||||
$images[$file] = $image_translations[$filename] ?? ucwords(str_replace(['-', '_'], [' - ', ' '], $filename));
|
||||
}
|
||||
}
|
||||
|
||||
return $images;
|
||||
}
|
||||
}
|
356
app/Http/Controllers/Maps/CustomMapDataController.php
Normal file
@ -0,0 +1,356 @@
|
||||
<?php
|
||||
/**
|
||||
* CustomMapController.php
|
||||
*
|
||||
* Controller for custom maps
|
||||
*
|
||||
* 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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @link https://www.librenms.org
|
||||
*
|
||||
* @copyright 2023 Steven Wilton
|
||||
* @author Steven Wilton <swilton@fluentit.com.au>
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Maps;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CustomMap;
|
||||
use App\Models\CustomMapEdge;
|
||||
use App\Models\CustomMapNode;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use LibreNMS\Config;
|
||||
use LibreNMS\Util\Url;
|
||||
|
||||
class CustomMapDataController extends Controller
|
||||
{
|
||||
public function get(Request $request, CustomMap $map): JsonResponse
|
||||
{
|
||||
$this->authorize('view', $map);
|
||||
|
||||
$edges = [];
|
||||
$nodes = [];
|
||||
|
||||
foreach ($map->edges as $edge) {
|
||||
$edgeid = $edge->custom_map_edge_id;
|
||||
$edges[$edgeid] = [
|
||||
'custom_map_edge_id' => $edge->custom_map_edge_id,
|
||||
'custom_map_node1_id' => $edge->custom_map_node1_id,
|
||||
'custom_map_node2_id' => $edge->custom_map_node2_id,
|
||||
'port_id' => $edge->port_id,
|
||||
'reverse' => $edge->reverse,
|
||||
'style' => $edge->style,
|
||||
'showpct' => $edge->showpct,
|
||||
'text_face' => $edge->text_face,
|
||||
'text_size' => $edge->text_size,
|
||||
'text_colour' => $edge->text_colour,
|
||||
'mid_x' => $edge->mid_x,
|
||||
'mid_y' => $edge->mid_y,
|
||||
];
|
||||
if ($edge->port) {
|
||||
$edges[$edgeid]['device_id'] = $edge->port->device_id;
|
||||
$edges[$edgeid]['port_name'] = $edge->port->device->displayName() . ' - ' . $edge->port->getLabel();
|
||||
$edges[$edgeid]['port_info'] = Url::portLink($edge->port, null, null, false, true);
|
||||
|
||||
// Work out speed to and from
|
||||
$speedto = 0;
|
||||
$speedfrom = 0;
|
||||
$rateto = 0;
|
||||
$ratefrom = 0;
|
||||
|
||||
// Try to interpret the SNMP speeds
|
||||
if ($edge->port->port_descr_speed) {
|
||||
$speed_parts = explode('/', $edge->port->port_descr_speed, 2);
|
||||
|
||||
if (count($speed_parts) == 1) {
|
||||
$speedto = $this->snmpSpeed($speed_parts[0]);
|
||||
$speedfrom = $speedto;
|
||||
} elseif ($edge->reverse) {
|
||||
$speedto = $this->snmpSpeed($speed_parts[1]);
|
||||
$speedfrom = $this->snmpSpeed($speed_parts[0]);
|
||||
} else {
|
||||
$speedto = $this->snmpSpeed($speed_parts[0]);
|
||||
$speedfrom = $this->snmpSpeed($speed_parts[1]);
|
||||
}
|
||||
if ($speedto == 0 || $speedfrom == 0) {
|
||||
$speedto = 0;
|
||||
$speedfrom = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If we did not get a speed from the snmp desc, use the deteced speed
|
||||
if ($speedto == 0 && $edge->port->ifSpeed) {
|
||||
$speedto = $edge->port->ifSpeed;
|
||||
$speedfrom = $edge->port->ifSpeed;
|
||||
}
|
||||
|
||||
// Get the to/from rates
|
||||
if ($edge->reverse) {
|
||||
$ratefrom = $edge->port->ifInOctets_rate * 8;
|
||||
$rateto = $edge->port->ifOutOctets_rate * 8;
|
||||
} else {
|
||||
$ratefrom = $edge->port->ifOutOctets_rate * 8;
|
||||
$rateto = $edge->port->ifInOctets_rate * 8;
|
||||
}
|
||||
|
||||
if ($speedto == 0) {
|
||||
$edges[$edgeid]['port_topct'] = -1.0;
|
||||
$edges[$edgeid]['port_frompct'] = -1.0;
|
||||
} else {
|
||||
$edges[$edgeid]['port_topct'] = round($rateto / $speedto * 100.0, 2);
|
||||
$edges[$edgeid]['port_frompct'] = round($ratefrom / $speedfrom * 100.0, 2);
|
||||
}
|
||||
if ($edge->port->ifOperStatus != 'up') {
|
||||
// If the port is not online, show the same as speed unknown
|
||||
$edges[$edgeid]['colour_to'] = $this->speedColour(-1.0);
|
||||
$edges[$edgeid]['colour_from'] = $this->speedColour(-1.0);
|
||||
} else {
|
||||
$edges[$edgeid]['colour_to'] = $this->speedColour($edges[$edgeid]['port_topct']);
|
||||
$edges[$edgeid]['colour_from'] = $this->speedColour($edges[$edgeid]['port_frompct']);
|
||||
}
|
||||
$edges[$edgeid]['width_to'] = $this->speedWidth($speedto);
|
||||
$edges[$edgeid]['width_from'] = $this->speedWidth($speedfrom);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($map->nodes as $node) {
|
||||
$nodeid = $node->custom_map_node_id;
|
||||
$nodes[$nodeid] = [
|
||||
'custom_map_node_id' => $node->custom_map_node_id,
|
||||
'device_id' => $node->device_id,
|
||||
'linked_map_id' => $node->linked_custom_map_id,
|
||||
'linked_map_name' => $node->linked_map ? $node->linked_map->name : null,
|
||||
'label' => $node->label,
|
||||
'style' => $node->style,
|
||||
'icon' => $node->icon,
|
||||
'image' => $node->image,
|
||||
'size' => $node->size,
|
||||
'border_width' => $node->border_width,
|
||||
'text_face' => $node->text_face,
|
||||
'text_size' => $node->text_size,
|
||||
'text_colour' => $node->text_colour,
|
||||
'colour_bg' => $node->colour_bg,
|
||||
'colour_bdr' => $node->colour_bdr,
|
||||
'colour_bg_view' => $node->colour_bg,
|
||||
'colour_bdr_view' => $node->colour_bdr,
|
||||
'x_pos' => $node->x_pos,
|
||||
'y_pos' => $node->y_pos,
|
||||
];
|
||||
if ($node->device) {
|
||||
$nodes[$nodeid]['device_name'] = $node->device->hostname . '(' . $node->device->sysName . ')';
|
||||
$nodes[$nodeid]['device_image'] = $node->device->icon;
|
||||
$nodes[$nodeid]['device_info'] = Url::deviceLink($node->device, null, [], 0, 0, 0, 0);
|
||||
|
||||
if ($node->device->disabled) {
|
||||
$device_style = $this->nodeDisabledStyle();
|
||||
} elseif (! $node->device->status) {
|
||||
$device_style = $this->nodeDownStyle();
|
||||
} else {
|
||||
$device_style = $this->nodeUpStyle();
|
||||
}
|
||||
|
||||
if ($device_style['background']) {
|
||||
$nodes[$nodeid]['colour_bg_view'] = $device_style['background'];
|
||||
}
|
||||
|
||||
if ($device_style['border']) {
|
||||
$nodes[$nodeid]['colour_bdr_view'] = $device_style['border'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['nodes' => $nodes, 'edges' => $edges]);
|
||||
}
|
||||
|
||||
public function save(Request $request, CustomMap $map): JsonResponse
|
||||
{
|
||||
$this->authorize('update', $map);
|
||||
|
||||
$data = $this->validate($request, [
|
||||
'newnodeconf' => 'array',
|
||||
'newedgeconf' => 'array',
|
||||
'nodes' => 'array',
|
||||
'edges' => 'array',
|
||||
]);
|
||||
|
||||
$map->load(['nodes', 'edges']);
|
||||
|
||||
DB::transaction(function () use ($map, $data) {
|
||||
$dbnodes = $map->nodes->keyBy('custom_map_node_id')->all();
|
||||
$dbedges = $map->edges->keyBy('custom_map_edge_id')->all();
|
||||
|
||||
$nodesProcessed = [];
|
||||
$edgesProcessed = [];
|
||||
|
||||
$newNodes = [];
|
||||
|
||||
$map->newnodeconfig = $data['newnodeconf'];
|
||||
$map->newedgeconfig = $data['newedgeconf'];
|
||||
$map->save();
|
||||
|
||||
foreach ($data['nodes'] as $nodeid => $node) {
|
||||
if (strpos($nodeid, 'new') === 0) {
|
||||
$dbnode = new CustomMapNode;
|
||||
$dbnode->map()->associate($map);
|
||||
} else {
|
||||
$dbnode = $dbnodes[$nodeid];
|
||||
if (! $dbnode) {
|
||||
Log::error('Could not find existing node for node id ' . $nodeid);
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
$dbnode->device_id = is_numeric($node['title']) ? $node['title'] : null;
|
||||
$dbnode->linked_custom_map_id = str_starts_with($node['title'], 'map:') ? (int) str_replace('map:', '', $node['title']) : null;
|
||||
$dbnode->label = $node['label'];
|
||||
$dbnode->style = $node['shape'];
|
||||
$dbnode->icon = $node['icon'];
|
||||
$dbnode->image = $node['image']['unselected'] ?? '';
|
||||
$dbnode->size = $node['size'];
|
||||
$dbnode->text_face = $node['font']['face'];
|
||||
$dbnode->text_size = $node['font']['size'];
|
||||
$dbnode->text_colour = $node['font']['color'];
|
||||
$dbnode->colour_bg = $node['color']['background'] ?? null;
|
||||
$dbnode->colour_bdr = $node['color']['border'] ?? null;
|
||||
$dbnode->border_width = $node['borderWidth'];
|
||||
$dbnode->x_pos = intval($node['x']);
|
||||
$dbnode->y_pos = intval($node['y']);
|
||||
|
||||
$dbnode->save();
|
||||
$nodesProcessed[$dbnode->custom_map_node_id] = true;
|
||||
$newNodes[$nodeid] = $dbnode;
|
||||
}
|
||||
foreach ($data['edges'] as $edgeid => $edge) {
|
||||
if (strpos($edgeid, 'new') === 0) {
|
||||
$dbedge = new CustomMapEdge;
|
||||
$dbedge->map()->associate($map);
|
||||
} else {
|
||||
$dbedge = $dbedges[$edgeid];
|
||||
if (! $dbedge) {
|
||||
Log::error('Could not find existing edge for edge id ' . $edgeid);
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
$dbedge->custom_map_node1_id = strpos($edge['from'], 'new') == 0 ? $newNodes[$edge['from']]->custom_map_node_id : $edge['from'];
|
||||
$dbedge->custom_map_node2_id = strpos($edge['to'], 'new') == 0 ? $newNodes[$edge['to']]->custom_map_node_id : $edge['to'];
|
||||
$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->style = $edge['style'];
|
||||
$dbedge->text_face = $edge['text_face'];
|
||||
$dbedge->text_size = $edge['text_size'];
|
||||
$dbedge->text_colour = $edge['text_colour'];
|
||||
$dbedge->mid_x = intval($edge['mid_x']);
|
||||
$dbedge->mid_y = intval($edge['mid_y']);
|
||||
|
||||
$dbedge->save();
|
||||
$edgesProcessed[$dbedge->custom_map_edge_id] = true;
|
||||
}
|
||||
foreach ($map->edges as $edge) {
|
||||
if (! array_key_exists($edge->custom_map_edge_id, $edgesProcessed)) {
|
||||
$edge->delete();
|
||||
}
|
||||
}
|
||||
foreach ($map->nodes as $node) {
|
||||
if (! array_key_exists($node->custom_map_node_id, $nodesProcessed)) {
|
||||
$node->delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return response()->json(['id' => $map->custom_map_id]);
|
||||
}
|
||||
|
||||
private function snmpSpeed(string $speeds): int
|
||||
{
|
||||
// Only succeed if the string startes with a number optionally followed by a unit
|
||||
if (preg_match('/^(\d+)([kMGTP])?/', $speeds, $matches)) {
|
||||
$speed = (int) $matches[1];
|
||||
if (count($matches) < 3) {
|
||||
return $speed;
|
||||
} elseif ($matches[2] == 'k') {
|
||||
$speed *= 1000;
|
||||
} elseif ($matches[2] == 'M') {
|
||||
$speed *= 1000000;
|
||||
} elseif ($matches[2] == 'G') {
|
||||
$speed *= 1000000000;
|
||||
} elseif ($matches[2] == 'T') {
|
||||
$speed *= 1000000000000;
|
||||
} elseif ($matches[2] == 'P') {
|
||||
$speed *= 1000000000000000;
|
||||
}
|
||||
|
||||
return $speed;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function speedColour(float $pct): string
|
||||
{
|
||||
// For the maths below, the 5.1 is worked out as 255 / 50
|
||||
// (255 being the max colour value and 50 is the max of the $pct calcluation)
|
||||
if ($pct < 0) {
|
||||
// Black if we can't determine the percentage (link down or speed 0)
|
||||
return '#000000';
|
||||
} elseif ($pct < 50) {
|
||||
// 100% green and slowly increase the red until we get to yellow
|
||||
return sprintf('#%02XFF00', (int) (5.1 * $pct));
|
||||
} elseif ($pct < 100) {
|
||||
// 100% red and slowly remove green to go from yellow to red
|
||||
return sprintf('#FF%02X00', (int) (5.1 * (100.0 - $pct)));
|
||||
} elseif ($pct < 150) {
|
||||
// 100% red and slowly increase blue to go purple
|
||||
return sprintf('#FF00%02X', (int) (5.1 * ($pct - 100.0)));
|
||||
}
|
||||
|
||||
// Default to purple for links over 150%
|
||||
return '#FF00FF';
|
||||
}
|
||||
|
||||
private function speedWidth(int $speed): float
|
||||
{
|
||||
if ($speed < 1000000) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
return (strlen((string) $speed) - 5) / 2.0;
|
||||
}
|
||||
|
||||
protected function nodeDisabledStyle(): array
|
||||
{
|
||||
return [
|
||||
'border' => Config::get('network_map_legend.di.border'),
|
||||
'background' => Config::get('network_map_legend.di.node'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function nodeDownStyle(): array
|
||||
{
|
||||
return [
|
||||
'border' => Config::get('network_map_legend.dn.border'),
|
||||
'background' => Config::get('network_map_legend.dn.node'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function nodeUpStyle(): array
|
||||
{
|
||||
return [
|
||||
'border' => null,
|
||||
'background' => null,
|
||||
];
|
||||
}
|
||||
}
|
@ -53,7 +53,7 @@ class DeviceController extends SelectController
|
||||
// list devices the user does not have access to
|
||||
if ($request->get('access') == 'inverted' && $user_id && $request->user()->isAdmin()) {
|
||||
return Device::query()
|
||||
->select('device_id', 'hostname', 'sysName', 'display')
|
||||
->select('device_id', 'hostname', 'sysName', 'display', 'icon')
|
||||
->whereNotIn('device_id', function ($query) use ($user_id) {
|
||||
$query->select('device_id')
|
||||
->from('devices_perms')
|
||||
@ -63,7 +63,7 @@ class DeviceController extends SelectController
|
||||
}
|
||||
|
||||
return Device::hasAccess($request->user())
|
||||
->select('device_id', 'hostname', 'sysName', 'display')
|
||||
->select('device_id', 'hostname', 'sysName', 'display', 'icon')
|
||||
->orderBy('hostname');
|
||||
}
|
||||
|
||||
@ -73,6 +73,7 @@ class DeviceController extends SelectController
|
||||
return [
|
||||
'id' => $device->{$this->id},
|
||||
'text' => $device->displayName(),
|
||||
'icon' => $device->icon,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ class PortController extends SelectController
|
||||
{
|
||||
return [
|
||||
'device' => 'nullable|int',
|
||||
'devices' => 'nullable|array',
|
||||
];
|
||||
}
|
||||
|
||||
@ -79,6 +80,10 @@ class PortController extends SelectController
|
||||
$query->where('ports.device_id', $device_id);
|
||||
}
|
||||
|
||||
if ($device_ids = $request->get('devices')) {
|
||||
$query->whereIn('ports.device_id', $device_ids);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
@ -91,6 +96,7 @@ class PortController extends SelectController
|
||||
return [
|
||||
'id' => $port->port_id,
|
||||
'text' => $label . ' - ' . $port->device->shortDisplayName() . $description,
|
||||
'device_id' => $port->device_id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
54
app/Http/Requests/CustomMapSettingsRequest.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CustomMapSettingsRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->isAdmin(); // TODO permissions
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string',
|
||||
'node_align' => 'integer',
|
||||
'width_type' => 'in:px,%',
|
||||
'width' => [
|
||||
function (string $attribute, mixed $value, Closure $fail) {
|
||||
if (! preg_match('/^(\d+)(px|%)$/', $value, $matches)) {
|
||||
$fail(__('map.custom.edit.validate.width_format'));
|
||||
} elseif ($matches[2] == 'px' && $matches[1] < 200) {
|
||||
$fail(__('map.custom.edit.validate.width_pixels'));
|
||||
} elseif ($matches[2] == '%' && ($matches[1] < 10 || $matches[1] > 100)) {
|
||||
$fail(__('map.custom.edit.validate.width_percent'));
|
||||
}
|
||||
},
|
||||
],
|
||||
'height_type' => 'in:px,%',
|
||||
'height' => [
|
||||
function (string $attribute, mixed $value, Closure $fail) {
|
||||
if (! preg_match('/^(\d+)(px|%)$/', $value, $matches)) {
|
||||
$fail(__('map.custom.edit.validate.height_format'));
|
||||
} elseif ($matches[2] == 'px' && $matches[1] < 200) {
|
||||
$fail(__('map.custom.edit.validate.height_pixels'));
|
||||
} elseif ($matches[2] == '%' && ($matches[1] < 10 || $matches[1] > 100)) {
|
||||
$fail(__('map.custom.edit.validate.height_percent'));
|
||||
}
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ namespace App\Http\ViewComposers;
|
||||
|
||||
use App\Models\AlertRule;
|
||||
use App\Models\BgpPeer;
|
||||
use App\Models\CustomMap;
|
||||
use App\Models\Dashboard;
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceGroup;
|
||||
@ -76,6 +77,9 @@ class MenuComposer
|
||||
//Dashboards
|
||||
$vars['dashboards'] = Dashboard::select('dashboard_id', 'dashboard_name')->allAvailable($user)->orderBy('dashboard_name')->get();
|
||||
|
||||
//Custom Maps
|
||||
$vars['custommaps'] = CustomMap::select('custom_map_id', 'name')->hasAccess($user)->orderBy('name')->get();
|
||||
|
||||
// Device menu
|
||||
$vars['device_groups'] = DeviceGroup::hasAccess($user)->orderBy('name')->get(['device_groups.id', 'name', 'desc']);
|
||||
$vars['package_count'] = Package::hasAccess($user)->count();
|
||||
|
97
app/Models/CustomMap.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
/**
|
||||
* CustomMap.php
|
||||
*
|
||||
* -Description-
|
||||
*
|
||||
* 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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @link https://www.librenms.org
|
||||
*
|
||||
* @copyright 2023 Steven Wilton
|
||||
* @author Steven Wilton <swilton@fluentit.com.au>
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class CustomMap extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
protected $primaryKey = 'custom_map_id';
|
||||
protected $casts = [
|
||||
'options' => 'array',
|
||||
'newnodeconfig' => 'array',
|
||||
'newedgeconfig' => 'array',
|
||||
];
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'width',
|
||||
'height',
|
||||
'node_align',
|
||||
'background_suffix',
|
||||
'background_version',
|
||||
];
|
||||
|
||||
// default values for attributes
|
||||
protected $attributes = [
|
||||
'options' => '{"interaction":{"dragNodes":false,"dragView":false,"zoomView":false},"manipulation":{"enabled":false},"physics":{"enabled":false}}',
|
||||
'newnodeconfig' => '{"borderWidth":1,"color":{"border":"#2B7CE9","background":"#D2E5FF"},"font":{"color":"#343434","size":14,"face":"arial"},"icon":[],"label":true,"shape":"box","size":25}',
|
||||
'newedgeconfig' => '{"arrows":{"to":{"enabled":true}},"smooth":{"type":"dynamic"},"font":{"color":"#343434","size":12,"face":"arial"},"label":true}',
|
||||
'background_version' => 0,
|
||||
];
|
||||
|
||||
public function hasAccess(): bool
|
||||
{
|
||||
return false; // TODO calculate based on device access
|
||||
}
|
||||
|
||||
public function scopeHasAccess($query, User $user)
|
||||
{
|
||||
if ($user->hasGlobalRead()) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
// Allow only if the user has access to all devices on the map
|
||||
return $query->withCount([
|
||||
'nodes as device_nodes_count' => function (Builder $q) {
|
||||
$q->whereNotNull('device_id');
|
||||
},
|
||||
'nodes as device_nodes_allowed_count' => function (Builder $q) use ($user) {
|
||||
$this->hasDeviceAccess($q, $user, 'custom_map_nodes');
|
||||
},
|
||||
])
|
||||
->havingRaw('device_nodes_count = device_nodes_allowed_count')
|
||||
->having('device_nodes_count', '>', 0);
|
||||
}
|
||||
|
||||
public function nodes(): HasMany
|
||||
{
|
||||
return $this->hasMany(CustomMapNode::class, 'custom_map_id');
|
||||
}
|
||||
|
||||
public function edges(): HasMany
|
||||
{
|
||||
return $this->hasMany(CustomMapEdge::class, 'custom_map_id');
|
||||
}
|
||||
|
||||
public function background(): HasOne
|
||||
{
|
||||
return $this->hasOne(CustomMapBackground::class, 'custom_map_id');
|
||||
}
|
||||
}
|
40
app/Models/CustomMapBackground.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* CustomMapBackground.php
|
||||
*
|
||||
* -Description-
|
||||
*
|
||||
* 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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @link https://www.librenms.org
|
||||
*
|
||||
* @copyright 2023 Steven Wilton
|
||||
* @author Steven Wilton <swilton@fluentit.com.au>
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class CustomMapBackground extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
protected $primaryKey = 'custom_map_background_id';
|
||||
|
||||
public function map(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CustomMap::class, 'custom_map_id');
|
||||
}
|
||||
}
|
61
app/Models/CustomMapEdge.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* CustomMapNode.php
|
||||
*
|
||||
* -Description-
|
||||
*
|
||||
* 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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @link https://www.librenms.org
|
||||
*
|
||||
* @copyright 2023 Steven Wilton
|
||||
* @author Steven Wilton <swilton@fluentit.com.au>
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class CustomMapEdge extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
protected $primaryKey = 'custom_map_edge_id';
|
||||
|
||||
public function map(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CustomMap::class, 'custom_map_id');
|
||||
}
|
||||
|
||||
public function port(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Port::class, 'port_id');
|
||||
}
|
||||
|
||||
public function edges(): HasMany
|
||||
{
|
||||
return $this->hasMany(CustomMapEdge::class, 'custom_map_edge_id');
|
||||
}
|
||||
|
||||
public function node1(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CustomMapNode::class, 'custom_map_node1_id');
|
||||
}
|
||||
|
||||
public function node2(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CustomMapNode::class, 'custom_map_node2_id');
|
||||
}
|
||||
}
|
71
app/Models/CustomMapNode.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* CustomMapNode.php
|
||||
*
|
||||
* -Description-
|
||||
*
|
||||
* 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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @link https://www.librenms.org
|
||||
*
|
||||
* @copyright 2023 Steven Wilton
|
||||
* @author Steven Wilton <swilton@fluentit.com.au>
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class CustomMapNode extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
protected $primaryKey = 'custom_map_node_id';
|
||||
|
||||
public function scopeHasAccess($query, User $user)
|
||||
{
|
||||
if ($user->hasGlobalRead()) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
// Allow only if the user has access to the node
|
||||
return $this->hasDeviceAccess($query, $user);
|
||||
}
|
||||
|
||||
public function map(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CustomMap::class, 'custom_map_id');
|
||||
}
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class, 'device_id');
|
||||
}
|
||||
|
||||
public function linked_map(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CustomMap::class, 'linked_custom_map_id');
|
||||
}
|
||||
|
||||
public function edges1(): HasMany
|
||||
{
|
||||
return $this->hasMany(CustomMapEdge::class, 'custom_map_node_id1');
|
||||
}
|
||||
|
||||
public function edges2(): HasMany
|
||||
{
|
||||
return $this->hasMany(CustomMapEdge::class, 'custom_map_node_id2');
|
||||
}
|
||||
}
|
74
app/Policies/CustomMapPolicy.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\CustomMap;
|
||||
use App\Models\User;
|
||||
|
||||
class CustomMapPolicy
|
||||
{
|
||||
public function before(User $user): ?bool
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->hasGlobalRead();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, CustomMap $customMap): bool
|
||||
{
|
||||
return $user->hasGlobalRead() || $customMap->hasAccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, CustomMap $customMap): bool
|
||||
{
|
||||
return $user->hasGlobalRead() || $customMap->hasAccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, CustomMap $customMap): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, CustomMap $customMap): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, CustomMap $customMap): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
*/
|
||||
protected $policies = [
|
||||
\App\Models\User::class => \App\Policies\UserPolicy::class,
|
||||
\App\Models\CustomMap::class => \App\Policies\CustomMapPolicy::class,
|
||||
\App\Models\Dashboard::class => \App\Policies\DashboardPolicy::class,
|
||||
\App\Models\Device::class => \App\Policies\DevicePolicy::class,
|
||||
\App\Models\DeviceGroup::class => \App\Policies\DeviceGroupPolicy::class,
|
||||
|
@ -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::create('custom_maps', function (Blueprint $table) {
|
||||
$table->increments('custom_map_id');
|
||||
$table->string('name', 100);
|
||||
$table->string('width', 10);
|
||||
$table->string('height', 10);
|
||||
$table->string('background_suffix', 10)->nullable();
|
||||
$table->integer('background_version')->unsigned();
|
||||
$table->longText('options')->nullable();
|
||||
$table->longText('newnodeconfig');
|
||||
$table->longText('newedgeconfig');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('custom_maps');
|
||||
}
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
<?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::create('custom_map_nodes', function (Blueprint $table) {
|
||||
$table->increments('custom_map_node_id');
|
||||
$table->integer('custom_map_id')->unsigned()->index();
|
||||
$table->integer('device_id')->nullable()->unsigned()->index();
|
||||
$table->string('label', 50);
|
||||
$table->string('style', 50);
|
||||
$table->string('icon', 8)->nullable();
|
||||
$table->integer('size');
|
||||
$table->integer('border_width');
|
||||
$table->string('text_face', 50);
|
||||
$table->integer('text_size');
|
||||
$table->string('text_colour', 10);
|
||||
$table->string('colour_bg', 10)->nullable();
|
||||
$table->string('colour_bdr', 10)->nullable();
|
||||
$table->integer('x_pos');
|
||||
$table->integer('y_pos');
|
||||
$table->timestamps();
|
||||
$table->foreign('device_id')->references('device_id')->on('devices')->onDelete('set null');
|
||||
$table->foreign('custom_map_id')->references('custom_map_id')->on('custom_maps')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('custom_map_nodes');
|
||||
}
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
<?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::create('custom_map_edges', function (Blueprint $table) {
|
||||
$table->increments('custom_map_edge_id');
|
||||
$table->integer('custom_map_id')->unsigned()->index();
|
||||
$table->integer('custom_map_node1_id')->unsigned()->index();
|
||||
$table->integer('custom_map_node2_id')->unsigned()->index();
|
||||
$table->integer('port_id')->unsigned()->nullable()->index();
|
||||
$table->boolean('reverse');
|
||||
$table->string('style', 50);
|
||||
$table->boolean('showpct');
|
||||
$table->string('text_face', 50);
|
||||
$table->integer('text_size');
|
||||
$table->string('text_colour', 10);
|
||||
$table->integer('mid_x');
|
||||
$table->integer('mid_y');
|
||||
$table->timestamps();
|
||||
$table->foreign('custom_map_id')->references('custom_map_id')->on('custom_maps')->onDelete('cascade');
|
||||
$table->foreign('port_id')->references('port_id')->on('ports')->onDelete('set null');
|
||||
$table->foreign('custom_map_node1_id')->references('custom_map_node_id')->on('custom_map_nodes')->onDelete('cascade');
|
||||
$table->foreign('custom_map_node2_id')->references('custom_map_node_id')->on('custom_map_nodes')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('custom_map_edges');
|
||||
}
|
||||
};
|
@ -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::create('custom_map_backgrounds', function (Blueprint $table) {
|
||||
$table->increments('custom_map_background_id');
|
||||
$table->timestamps();
|
||||
$table->integer('custom_map_id')->unsigned()->index()->unique();
|
||||
$table->binary('background_image');
|
||||
$table->foreign('custom_map_id')->references('custom_map_id')->on('custom_maps')->onDelete('cascade');
|
||||
});
|
||||
try {
|
||||
DB::statement('ALTER TABLE custom_map_backgrounds MODIFY background_image MEDIUMBLOB');
|
||||
} catch (Exception $e) {
|
||||
// SQLite can store large values in a BLOB column
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('custom_map_backgrounds');
|
||||
}
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
<?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->smallInteger('node_align')->default(0)->after('height');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('custom_maps', function (Blueprint $table) {
|
||||
$table->dropColumn(['node_align']);
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
<?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_nodes', function (Blueprint $table) {
|
||||
$table->string('image', 255)->default('')->after('icon');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('custom_map_nodes', function (Blueprint $table) {
|
||||
$table->dropColumn(['image']);
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
<?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_nodes', function (Blueprint $table) {
|
||||
$table->integer('linked_custom_map_id')->nullable()->unsigned()->index()->after('device_id');
|
||||
$table->foreign('linked_custom_map_id')->references('custom_map_id')->on('custom_maps')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('custom_map_nodes', function (Blueprint $table) {
|
||||
$table->dropForeign('custom_map_nodes_linked_custom_map_id_foreign');
|
||||
$table->dropColumn(['linked_custom_map_id']);
|
||||
});
|
||||
}
|
||||
};
|
27
doc/Extensions/Availability-Map.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Availability Map
|
||||
|
||||
LibreNMS has the following page to show an availability map:
|
||||
|
||||
- Overview -> Maps -> Availability
|
||||
|
||||
This map will show all devices on a single page, with each device
|
||||
having either a box or a coloured square representing its status.
|
||||
|
||||
## Widget
|
||||
There is an availability map widget that can be added to a dashboard
|
||||
to give a quick overview of the status of all devices on the network.
|
||||
|
||||
## Settings
|
||||
```bash
|
||||
# Set the compact view mode for the availability map
|
||||
lnms config:set webui.availability_map_compact false
|
||||
|
||||
# Size of the box for each device in the availability map (not compact)
|
||||
lnms config:set webui.availability_map_box_size 165
|
||||
|
||||
# Sort by status instead of hostname
|
||||
lnms config:set webui.availability_map_sort_status false
|
||||
|
||||
# Show the device group drop-down on the availabiltiy map page
|
||||
lnms config:set webui.availability_map_use_device_groups true
|
||||
```
|
139
doc/Extensions/Custom-Map.md
Normal file
@ -0,0 +1,139 @@
|
||||
# Custom Map
|
||||
|
||||
LibreNMS has the ability to create custom maps to give a quick
|
||||
overview of parts of the network including up/down status of devices
|
||||
and link utilisation. These are also referred to as weather maps.
|
||||
|
||||
## Viewer
|
||||
|
||||
Once some maps have been created, they will be visible to any users who
|
||||
have read access to all devices on a given map. Custom maps are available
|
||||
through the Overview -> Maps -> Custom Maps menu.
|
||||
|
||||
Some key points about the viewer are:
|
||||
|
||||
- Nodes will change colour if they are down or disabled
|
||||
- Links are only associated with a single network interface
|
||||
- Link utilisation can only be shown if the link speed is known
|
||||
- Link speed is decoded from SNMP if possible (Upload/Download) and defaults
|
||||
to the physical speed if SNMP data is not available, or cannot be decoded
|
||||
- Links will change colour as follows:
|
||||
- Black if the link is down, or the max speed is unknown
|
||||
- Green at 0% utilisation, with a gradual change to
|
||||
- Yellow at 50% utilisation, with a gradual change to
|
||||
- Orange at 75% utilisation, with a gradual change to
|
||||
- Red at 100% utilisation, with a gradual change to
|
||||
- Purple at 150% utilisation and above
|
||||
|
||||
## Editor
|
||||
|
||||
To access the custom map editor, a user must be an admin. The editor
|
||||
is accessed through the Overview -> Maps -> Custom Map Editor menu.
|
||||
|
||||
Once you are in the editor, you will be given a drop-down list of all
|
||||
the custom maps so you can choose one to edit, or select "Create New Map"
|
||||
to create a new map.
|
||||
|
||||
### Map Settings
|
||||
|
||||
When you create a new map, you will be presented with a page to set
|
||||
some global map settings. These are:
|
||||
|
||||
- *Name*: The name for the map
|
||||
- *Width*: The width of the map in pixels
|
||||
- *Height*: The height of the map in pixels
|
||||
- *Node Alignment*: When devices are added to the map, this will align
|
||||
the devices to an invisible grid this many pixels wide, which can help
|
||||
to make the maps look better. This can be set to 0 to disable.
|
||||
- *Background*: An image (PNG/JPG) up to 2MB can be uploaded as a background.
|
||||
|
||||
These settings can be changed at any stage by clicking on the "Edit Map Settings"
|
||||
button in the top-left of the editor.
|
||||
|
||||
### Nodes
|
||||
|
||||
Once you have a map, you can start by adding "nodes" to the map. A node
|
||||
represents a device, or an external point in the network (e.g. the internet)
|
||||
To add a node, you click on the "Add Node" button in the control bar, then
|
||||
click on the map area where you want to add the node. You will then be aked
|
||||
for the following information:
|
||||
|
||||
- *Label*: The text to display on this point in the network
|
||||
- *Device*: If this node represents a device, you can select the device from
|
||||
the drop-down. This will overwrite the label, which you can then change if
|
||||
you want to.
|
||||
- *Style*: You can select the style of the node. If a device has been selected
|
||||
you can choose the LibreNMS icon by choosing "Device Image". You can also
|
||||
choose "Icon" to select an image for the device.
|
||||
- *Icon*: If you choose "Icon" in the style box, you can select from a list of
|
||||
images to represent this node
|
||||
|
||||
There are also options to choose the size and colour of the node and the font.
|
||||
|
||||
Once you have finished choosing the options for the node, you can press Save to
|
||||
add it to the map. NOTE: This does not save anything to the database immediately.
|
||||
You need to click on the "Save Map" button in the top-right to save your changes
|
||||
to the database.
|
||||
|
||||
You can edit a node at any time by selecting it on the map and clicking on the
|
||||
"Edit Node" button in the control bar.
|
||||
|
||||
You can also modify the default settings for all new nodes by clicking on the
|
||||
"Edit Node Default" button at the top of the page.
|
||||
|
||||
### Edges
|
||||
|
||||
Once you have 2 or more nodes, you can add links between the nodes. These are
|
||||
called edges in the editor. To add a link, click on the "Add Edge" button in
|
||||
the control bar, then click on one of the nodes you want to link and drag the
|
||||
cursor to the second node that you want to link. You will then be prompted for
|
||||
the following information:
|
||||
|
||||
- *From*: The node that the link runs from (it will default to first node you selected)
|
||||
- *To*: The node that the link runs to (it will default to the second node you selected)
|
||||
- *Port*: If the From or To node is linked to a device, you can select an interface
|
||||
from one of the devices and the custom map will show traffic utilisation for
|
||||
the selected interface.
|
||||
- *Reverse Port Direction*: If the selected port displays data in the wrong
|
||||
direction for the link, you can reverse it by toggling this option.
|
||||
- *Line Style*: You can try different line styles, especially if you are running
|
||||
multiple links between the same 2 nodes
|
||||
- *Show percent usage*: Choose whether to have text on the lines showing the link
|
||||
utilisation as a percentage
|
||||
- *Recenter Line*: If you tick this box, the centre point of the line will be moved
|
||||
back to half way between the 2 nodes when you click on the save button.
|
||||
|
||||
Once you have finished choosing the options for the node, you can press Save to
|
||||
add it to the map. NOTE: This does not save anything to the database immediately.
|
||||
You need to click on the "Save Map" button in the top-right to save your changes
|
||||
to the database.
|
||||
|
||||
Once you press save, you it will create 3 objects on the screen, 2 arrows and a
|
||||
round node in the middle. Having the 3 objects allows you to move the mid point
|
||||
of the line off centre, and also allows us to display bandwidth information for
|
||||
both directions of the link.
|
||||
|
||||
You can edit an edge at any time by selecting it on the map and clicking on the
|
||||
"Edit Edge" button in the control bar.
|
||||
|
||||
You can also modify the default settings for all new edges by clicking on the
|
||||
"Edit Edge Default" button at the top of the page.
|
||||
|
||||
### Re-Render
|
||||
|
||||
When you drag items around the map, some of the lines will bend. This will cause a
|
||||
"Re-Render Map" button to appear at the top-right of the page. This button can be
|
||||
clicked on to cause all lines to be re-drawn the way they will be shown in the viewer.
|
||||
|
||||
### Save Map
|
||||
|
||||
Once you are happy with a set of changes that you have made, you can click on the
|
||||
"Save Map" button in the top-right of the page to commit changes to the database.
|
||||
This will cause anyone viewing the map to see the new version the next time their
|
||||
page refreshes.
|
||||
|
||||
## Adding Images
|
||||
|
||||
You can add your own images to use on the custom map by copying files into the
|
||||
html/images/custommap/icons/ directory. Any files with a .svg, .png or .jpg extension
|
||||
will be shown in the image selection drop-down in the custom map editor.
|
11
doc/Extensions/Dependency-Map.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Dependency Map
|
||||
|
||||
LibreNMS has the ability to show you a dynamic network map based on
|
||||
device dependencies that have been configure. These maps are accessed
|
||||
through the following menu options:
|
||||
|
||||
- Overview -> Maps -> Device Dependency
|
||||
- Overview -> Maps -> Device Groups Dependencies
|
||||
|
||||
## Settings
|
||||
The map display can be configured by altering the [VisJS-Config.md](Vis JS Options)
|
@ -1,89 +1,33 @@
|
||||
# Network Map
|
||||
|
||||
LibreNMS has the ability to show you a network map based on:
|
||||
LibreNMS has the ability to show you a dynamic network map based on
|
||||
data collected from devices. These maps are accessed through the
|
||||
following menu options:
|
||||
|
||||
- Overview -> Maps -> Network
|
||||
- Overview -> Maps -> Device Group Maps
|
||||
- The Neighbours -> Map tab when viewing a single device
|
||||
(the Neighbours tab will only show if a device has xDP neighbours)
|
||||
|
||||
These network maps can be based on:
|
||||
|
||||
- xDP Discovery
|
||||
- MAC addresses
|
||||
- MAC addresses (ARP entries matching interface IP and MAC)
|
||||
|
||||
By default, both are are included but you can enable / disable either
|
||||
one using the following config option:
|
||||
|
||||
```php
|
||||
$config['network_map_items'] = array('mac','xdp');
|
||||
```bash
|
||||
lnms config:set 'network_map_items' "('mac','xdp')"
|
||||
```
|
||||
|
||||
Either remove mac or xdp depending on which you want.
|
||||
XDP is based on FDP, CDP and LLDP support based on the device type.
|
||||
|
||||
A global map will be drawn from the information in the database, it is
|
||||
worth noting that this could lead to a large network map. Network maps
|
||||
for individual devices are available showing the relationship with
|
||||
other devices. Also you can Build Device Groups and those Device
|
||||
Groups can be drawn with Network Map.
|
||||
It is worth noting that the global map could lead to a large network
|
||||
map that is slow to render and interact with. The network map on the
|
||||
device neighbour page, or building device groups and using the device
|
||||
group maps will be more usable on large networks.
|
||||
|
||||
## Network Map Configurator
|
||||
|
||||
[This link](https://visjs.github.io/vis-network/docs/network/) will
|
||||
show you all the options and explain what they do.
|
||||
|
||||
You may also access the dynamic configuration interface [example
|
||||
here](https://visjs.github.io/vis-network/examples/network/other/configuration.html)
|
||||
from within LibreNMS by adding the following to config.php
|
||||
|
||||
```php
|
||||
$config['network_map_vis_options'] = '{
|
||||
"configure": { "enabled": true},
|
||||
}';
|
||||
```
|
||||
|
||||
### Note
|
||||
|
||||
You may want to disable the automatic page refresh while you're
|
||||
tweaking your configuration, as the refresh will reset the dynamic
|
||||
configuration UI to the values currently saved in config.php This can
|
||||
be done by clicking on the Settings Icon then Refresh Pause.
|
||||
|
||||
### Configurator Output
|
||||
|
||||
Once you've achieved your desired map appearance, click the generate
|
||||
options button at the bottom to be given the necessary parameters to
|
||||
add to your config.php file. You will need to paste the generated
|
||||
config into config.php the format will need to look something like
|
||||
this. Note that the configurator will output the config with `var options`
|
||||
you will need to strip them out and at the end of the config you need to
|
||||
add an `}';` see the example below.
|
||||
|
||||
```php
|
||||
$config['network_map_vis_options'] = '{
|
||||
"nodes": {
|
||||
"color": {
|
||||
"background": "rgba(20,252,18,1)"
|
||||
},
|
||||
"font": {
|
||||
"face": "tahoma"
|
||||
},
|
||||
"physics": false
|
||||
},
|
||||
"edges": {
|
||||
"smooth": {
|
||||
"forceDirection": "none"
|
||||
}
|
||||
},
|
||||
"interaction": {
|
||||
"hover": true,
|
||||
"multiselect": true,
|
||||
"navigationButtons": true
|
||||
},
|
||||
"manipulation": {
|
||||
"enabled": true
|
||||
},
|
||||
"physics": {
|
||||
"barnesHut": {
|
||||
"avoidOverlap": 0.11
|
||||
},
|
||||
"minVelocity": 0.75
|
||||
}
|
||||
}';
|
||||
```
|
||||
|
||||
![Example Network Map](/img/networkmap.png)
|
||||
## Settings
|
||||
The map display can be configured by altering the [VisJS-Config.md](Vis JS Options)
|
||||
|
71
doc/Extensions/VisJS-Config.md
Normal file
@ -0,0 +1,71 @@
|
||||
# Vis JS Configuration
|
||||
|
||||
The [Network Maps](Network-Map.md) and [Dependency Maps](Dependency-Map.md) all use a common configuration for
|
||||
the vis.js library, which affects the way the maps are rendered, as well
|
||||
as the way that users can interact with the maps. This configuration can
|
||||
be adjusted by following the instructions below.
|
||||
|
||||
[This link](https://visjs.github.io/vis-network/docs/network/) will
|
||||
show you all the options and explain what they do.
|
||||
|
||||
You may also access the dynamic configuration interface [example
|
||||
here](https://visjs.github.io/vis-network/examples/network/other/configuration.html)
|
||||
from within LibreNMS by adding the following to config.php
|
||||
|
||||
```php
|
||||
$config['network_map_vis_options'] = '{
|
||||
"configure": { "enabled": true},
|
||||
}';
|
||||
```
|
||||
|
||||
### Note
|
||||
|
||||
You may want to disable the automatic page refresh while you're
|
||||
tweaking your configuration, as the refresh will reset the dynamic
|
||||
configuration UI to the values currently saved in config.php This can
|
||||
be done by clicking on the Settings Icon then Refresh Pause.
|
||||
|
||||
### Configurator Output
|
||||
|
||||
Once you've achieved your desired map appearance, click the generate
|
||||
options button at the bottom to be given the necessary parameters to
|
||||
add to your config.php file. You will need to paste the generated
|
||||
config into config.php the format will need to look something like
|
||||
this. Note that the configurator will output the config with `var options`
|
||||
you will need to strip them out and at the end of the config you need to
|
||||
add an `}';` see the example below.
|
||||
|
||||
```php
|
||||
$config['network_map_vis_options'] = '{
|
||||
"nodes": {
|
||||
"color": {
|
||||
"background": "rgba(20,252,18,1)"
|
||||
},
|
||||
"font": {
|
||||
"face": "tahoma"
|
||||
},
|
||||
"physics": false
|
||||
},
|
||||
"edges": {
|
||||
"smooth": {
|
||||
"forceDirection": "none"
|
||||
}
|
||||
},
|
||||
"interaction": {
|
||||
"hover": true,
|
||||
"multiselect": true,
|
||||
"navigationButtons": true
|
||||
},
|
||||
"manipulation": {
|
||||
"enabled": true
|
||||
},
|
||||
"physics": {
|
||||
"barnesHut": {
|
||||
"avoidOverlap": 0.11
|
||||
},
|
||||
"minVelocity": 0.75
|
||||
}
|
||||
}';
|
||||
```
|
||||
|
||||
![Example Network Map](/img/networkmap.png)
|
@ -45,31 +45,52 @@ We have two current mapping engines available:
|
||||
zoom levels](https://wiki.openstreetmap.org/wiki/Zoom_levels).
|
||||
- *Grouping radius*: Markers are grouped by area. This value define
|
||||
the maximum size of grouping areas.
|
||||
- *Show devices*: Show devices based on there status.
|
||||
- *Show devices*: Show devices based on status.
|
||||
|
||||
Example Settings:
|
||||
|
||||
![Example World Map Settings](/img/world-map-widget-settings.png)
|
||||
|
||||
### Device Overview World Map Settings
|
||||
|
||||
If a device has a location with a valid latitude and logitude, the
|
||||
device overview page will have a panel showing the device on a world
|
||||
map. The following settings affect this map:
|
||||
|
||||
```bash
|
||||
# Does the world map start opened, or does the user need to clivk to view
|
||||
lnms config:set device_location_map_open false
|
||||
# Do we show all other devices on the map as well
|
||||
lnms config:set device_location_map_show_devices false
|
||||
# Do we show a network map based on device dependencies
|
||||
lnms config:set device_location_map_show_device_dependencies false
|
||||
```
|
||||
|
||||
## Offline OpenStreet Map
|
||||
|
||||
If you can't access OpenStreet map directly you can run a local [tile
|
||||
server](http://wiki.openstreetmap.org/wiki/Tile_servers). To specify a
|
||||
different url you can set:
|
||||
|
||||
```php
|
||||
$config['leaflet']['tile_url'] = 'localhost.com';
|
||||
```bash
|
||||
lnms config:set leaflet.tile_url 'localhost.com'
|
||||
```
|
||||
|
||||
## Additional Leaflet config
|
||||
|
||||
```php
|
||||
$config['map']['engine'] = "leaflet";
|
||||
$config['leaflet']['default_lat'] = "51.981074";
|
||||
$config['leaflet']['default_lng'] = "5.350342";
|
||||
$config['leaflet']['default_zoom'] = 8;
|
||||
// Device grouping radius in KM default 80KM
|
||||
$config['leaflet']['group_radius'] = 1;
|
||||
```bash
|
||||
lnms config:set map.engine leaflet
|
||||
lnms config:set leaflet.default_lat "51.981074"
|
||||
lnms config:set leaflet.default_lng "5.350342"
|
||||
lnms config:set leaflet.default_zoom 8
|
||||
# Device grouping radius in KM default 80KM
|
||||
lnms config:set leaflet.group_radius 1
|
||||
# Enable network map on world map
|
||||
lnms config:set network_map_show_on_worldmap true
|
||||
# Use CDP/LLDP for network map, or device dependencies
|
||||
lnms config:set network_map_worldmap_link_type xdp/depends
|
||||
# Do not show devices that have notifications disabled
|
||||
lnms config:set network_map_worldmap_show_disabled_alerts false
|
||||
```
|
||||
|
||||
## Geocode engine config
|
||||
@ -98,13 +119,13 @@ Further custom options are available to load different maps of the
|
||||
world, set default coordinates of where the map will zoom and the zoom
|
||||
level by default. An example of this is:
|
||||
|
||||
```php
|
||||
$config['map']['engine'] = "jquery-mapael";
|
||||
$config['mapael']['default_map'] = 'mapael-maps/united_kingdom/united_kingdom.js';
|
||||
$config['mapael']['map_width'] = 400;
|
||||
$config['mapael']['default_lat'] = '50.898482';
|
||||
$config['mapael']['default_lng'] = '-3.401402';
|
||||
$config['mapael']['default_zoom'] = 20;
|
||||
```bash
|
||||
lnms config:set map.engine jquery-mapael
|
||||
lnms config:set mapael.default_map 'mapael-maps/united_kingdom/united_kingdom.js'
|
||||
lnms config:set mapael.map_width 400
|
||||
lnms config:set mapael.default_lat '50.898482'
|
||||
lnms config:set mapael.default_lng '-3.401402'
|
||||
lnms config:set mapael.default_zoom 20
|
||||
```
|
||||
|
||||
A list of maps can be found in ```html/js/maps/``` or ```html/js/mapael-maps/```.
|
||||
|
BIN
html/css/img/network/acceptDeleteIcon.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
html/css/img/network/addNodeIcon.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
html/css/img/network/backIcon.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
html/css/img/network/connectIcon.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
html/css/img/network/cross.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
html/css/img/network/cross2.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
html/css/img/network/deleteIcon.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
html/css/img/network/editIcon.png
Normal file
After Width: | Height: | Size: 20 KiB |
4
html/images/custommap/background/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
11
html/images/custommap/icons/.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file and all included images
|
||||
!.gitignore
|
||||
!adc.svg
|
||||
!firewall.svg
|
||||
!gtm.svg
|
||||
!router-square.svg
|
||||
!router.svg
|
||||
!switch-l2.svg
|
||||
!switch-l3.svg
|
40
html/images/custommap/icons/adc.svg
Normal file
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
viewBox="0 0 146.47219 146.23581"
|
||||
stroke="#2c2c2c">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
transform="translate(-310.19216,-103.4594)"
|
||||
id="layer1" />
|
||||
<rect
|
||||
ry="8.9952965"
|
||||
y="2.8232944"
|
||||
x="2.8232944"
|
||||
height="140.58922"
|
||||
width="140.82561"
|
||||
id="rect4136-2-3-3"
|
||||
style="opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:5.6465888;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="opacity:1;fill:#3a3a3a;fill-opacity:1;stroke-width:1.91520727;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 73.03267,18.093578 c -1.306804,0.03207 -2.603151,0.580155 -3.473297,1.586156 L 54.480938,37.112419 c -1.485052,1.716916 -1.149852,4.097672 0.751547,5.337787 l 0.273654,0.178354 c 1.901399,1.240116 4.627658,0.856424 6.112709,-0.860503 l 6.812842,-7.876874 v 20.418744 c 0,0.200981 0.0096,0.399166 0.02624,0.594402 A 20.581787,20.360481 0 0 0 54.733372,65.240809 L 35.497337,58.534777 46.471378,54.181185 c 2.110089,-0.837098 3.369683,-3.285164 2.824596,-5.48881 l -0.07843,-0.317169 C 48.672457,46.17156 46.534627,45.071311 44.424538,45.90841 l -21.424513,8.49968 c -1.326081,0.526078 -2.315361,1.688667 -2.732905,3.026689 -0.0029,0.0067 -0.0067,0.01379 -0.0086,0.02088 -0.495904,1.30282 -0.439837,2.818985 0.2679,4.048654 l 11.497611,19.976439 c 1.132365,1.967473 3.490894,2.434688 5.2878,1.047494 l 0.258554,-0.199574 c 1.796904,-1.387194 2.331965,-4.087703 1.199588,-6.055176 l -5.195381,-9.026129 18.785463,6.548885 a 20.581787,20.360481 0 0 0 -0.03448,0.971633 20.581787,20.360481 0 0 0 16.215456,19.871796 v 9.746389 a 12.452256,12.318361 0 0 0 -7.845226,11.43935 12.452256,12.318361 0 0 0 12.451961,12.3182 12.452256,12.318361 0 0 0 12.452331,-12.3182 12.452256,12.318361 0 0 0 -8.379587,-11.62634 v -9.532796 a 20.581787,20.360481 0 0 0 16.268679,-19.898399 20.581787,20.360481 0 0 0 -0.08302,-1.637953 l 20.214851,-7.144735 -5.84482,10.257377 c -1.12392,1.97231 -0.57736,4.670665 1.22548,6.05014 l 0.25927,0.198492 c 1.80285,1.379477 4.15957,0.902169 5.28349,-1.070141 l 11.41131,-20.02571 c 0.70633,-1.239502 0.75203,-2.765501 0.24166,-4.070944 -0.002,-0.0076 -0.004,-0.01436 -0.006,-0.02155 -0.4272,-1.326932 -1.41871,-2.475367 -2.73973,-2.992885 l -21.46123,-8.407683 c -2.113653,-0.828031 -4.246602,0.281334 -4.782219,2.487299 l -0.07699,0.317522 c -0.535626,2.205974 0.734674,4.648553 2.848326,5.476593 l 9.696763,3.798736 -18.916705,6.685902 A 20.581787,20.360481 0 0 0 77.266867,54.894984 c 0.01647,-0.192201 0.02518,-0.387313 0.02518,-0.585057 v -21.47091 l 7.723323,8.92904 c 1.48505,1.716927 4.21131,2.100619 6.112709,0.860503 l 0.273654,-0.178354 c 1.901399,-1.240115 2.236598,-3.620871 0.751547,-5.337787 L 77.074858,19.679734 c -0.93328,-1.078999 -2.356844,-1.630551 -3.757742,-1.584365 -0.0076,-1.82e-4 -0.01503,-9.58e-4 -0.0227,-9.58e-4 -0.08705,-0.0029 -0.174303,-0.0029 -0.261426,-3.54e-4 z m -0.125495,44.35572 A 12.452256,12.318361 0 0 1 85.359144,74.767859 12.452256,12.318361 0 0 1 72.907175,87.086051 12.452256,12.318361 0 0 1 60.454852,74.767859 12.452256,12.318361 0 0 1 72.907175,62.449298 Z"
|
||||
id="path4150" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
128
html/images/custommap/icons/firewall.svg
Normal file
@ -0,0 +1,128 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="#2c2c2c"
|
||||
viewBox="0 0 158.45619 146.47219"
|
||||
id="svg4294"
|
||||
version="1.1">
|
||||
<defs
|
||||
id="defs4296" />
|
||||
<metadata
|
||||
id="metadata4299">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
transform="translate(4.2915344e-6)"
|
||||
id="g869">
|
||||
<rect
|
||||
ry="3.816956"
|
||||
y="4.0686131"
|
||||
x="4.0686092"
|
||||
height="138.33496"
|
||||
width="150.31897"
|
||||
id="rect6782-4-65-3-0-6"
|
||||
style="opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:8.13722706;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<rect
|
||||
style="opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-5"
|
||||
width="147.8936"
|
||||
height="28.35601"
|
||||
x="5.2812467"
|
||||
y="4.4881825" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-6-3"
|
||||
width="147.8936"
|
||||
height="28.35601"
|
||||
x="5.2812467"
|
||||
y="113.628" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-6-2-5"
|
||||
width="51.38068"
|
||||
height="28.189941"
|
||||
x="5.2812467"
|
||||
y="59.411709" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-6-2-9-6"
|
||||
width="49.467712"
|
||||
height="25.67437"
|
||||
x="29.213806"
|
||||
y="33.051807" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-6-2-9-2-2"
|
||||
width="24.194138"
|
||||
height="25.49596"
|
||||
x="5.2812467"
|
||||
y="33.141006" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-6-2-9-3-9"
|
||||
width="49.467712"
|
||||
height="25.67437"
|
||||
x="78.700119"
|
||||
y="33.051807" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-6-2-9-2-9-1"
|
||||
width="24.194138"
|
||||
height="25.49596"
|
||||
x="128.98065"
|
||||
y="33.141006" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-6-2-2-2"
|
||||
width="51.38068"
|
||||
height="28.189941"
|
||||
x="101.79416"
|
||||
y="59.411709" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-6-2-9-8-7"
|
||||
width="49.467712"
|
||||
height="25.67437"
|
||||
x="29.248823"
|
||||
y="88.420822" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-6-2-9-2-97-0"
|
||||
width="24.184027"
|
||||
height="25.964542"
|
||||
x="5.3212972"
|
||||
y="88.275734" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-6-2-9-3-3-9"
|
||||
width="49.467712"
|
||||
height="25.67437"
|
||||
x="78.735085"
|
||||
y="88.420822" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-6-2-9-2-9-6-3"
|
||||
width="24.184027"
|
||||
height="25.964542"
|
||||
x="128.9493"
|
||||
y="88.275734" />
|
||||
<rect
|
||||
style="display:inline;opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.50978184;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5963-6-2-1-6"
|
||||
width="45.02253"
|
||||
height="28.274075"
|
||||
x="56.716785"
|
||||
y="59.369617" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.3 KiB |
37
html/images/custommap/icons/gtm.svg
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="#2c2c2c"
|
||||
viewBox="0 0 146.47216 146.23577"
|
||||
id="svg4787"
|
||||
version="1.1">
|
||||
<defs
|
||||
id="defs4789" />
|
||||
<metadata
|
||||
id="metadata4792">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<rect
|
||||
style="opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:5.64658737;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4136-2-3-3"
|
||||
width="140.82558"
|
||||
height="140.58919"
|
||||
x="2.8232937"
|
||||
y="2.8232937"
|
||||
ry="8.9952946" />
|
||||
<path
|
||||
id="path829"
|
||||
d="m 73.218247,20.874536 a 11.187016,11.187016 0 0 0 -11.187118,11.187122 11.187016,11.187016 0 0 0 7.6783,10.613071 V 58.182476 A 18.979685,18.979685 0 0 0 56.722045,67.464033 L 41.231274,63.033006 A 11.187016,11.187016 0 0 0 30.057743,52.246701 11.187016,11.187016 0 0 0 18.870624,63.433822 11.187016,11.187016 0 0 0 30.057743,74.620937 11.187016,11.187016 0 0 0 39.27476,69.772111 l 15.17997,4.342714 a 18.979685,18.979685 0 0 0 -0.215692,2.708886 18.979685,18.979685 0 0 0 5.086598,12.907569 l -9.060767,13.94357 a 11.187016,11.187016 0 0 0 -3.823016,-0.68784 11.187016,11.187016 0 0 0 -11.187119,11.18712 11.187016,11.187016 0 0 0 11.187119,11.18711 11.187016,11.187016 0 0 0 11.187118,-11.18711 11.187016,11.187016 0 0 0 -1.834232,-6.1311 L 64.98628,93.899055 a 18.979685,18.979685 0 0 0 8.231967,1.903872 18.979685,18.979685 0 0 0 8.571639,-2.048227 l 9.009816,13.86543 a 11.187016,11.187016 0 0 0 -2.134842,6.554 11.187016,11.187016 0 0 0 11.187118,11.18711 11.187016,11.187016 0 0 0 11.187132,-11.18711 11.187016,11.187016 0 0 0 -11.187132,-11.18712 11.187016,11.187016 0 0 0 -3.350867,0.52648 L 87.360517,89.451047 A 18.979685,18.979685 0 0 0 92.197455,76.823711 18.979685,18.979685 0 0 0 91.966478,73.96367 l 15.144312,-4.332523 a 11.187016,11.187016 0 0 0 9.30363,4.98979 11.187016,11.187016 0 0 0 11.18712,-11.187115 11.187016,11.187016 0 0 0 -11.18712,-11.187121 11.187016,11.187016 0 0 0 -11.15824,10.616471 L 89.627832,67.33326 A 18.979685,18.979685 0 0 0 76.727065,58.182476 V 42.669628 a 11.187016,11.187016 0 0 0 7.6783,-10.60797 11.187016,11.187016 0 0 0 -11.187118,-11.187122 z"
|
||||
style="opacity:1;fill-opacity:1;stroke-width:2.30346966;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
40
html/images/custommap/icons/router-square.svg
Normal file
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="#2c2c2c"
|
||||
viewBox="0 0 146.47219 146.2358"
|
||||
id="svg7778"
|
||||
version="1.1">
|
||||
<defs
|
||||
id="defs7780" />
|
||||
<metadata
|
||||
id="metadata7783">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="g865">
|
||||
<rect
|
||||
style="opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:5.6465888;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4136-2-3-3"
|
||||
width="140.82561"
|
||||
height="140.58922"
|
||||
x="2.8232944"
|
||||
y="2.8232942"
|
||||
ry="8.9952965" />
|
||||
<path
|
||||
style="fill:#3a3a3a;fill-opacity:1;stroke-width:2.48305464;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
d="M 18.688665,18.570684 V 63.706326 L 34.993158,47.401864 62.996338,73.117916 35.001002,98.841801 18.688665,82.529505 V 127.66515 H 63.82428 L 47.511959,111.35277 73.236092,83.357644 98.951934,111.36053 82.647413,127.6651 l 45.133667,-0.002 0.002,-45.133597 -16.3197,16.319603 L 83.475883,73.117842 111.47117,47.393565 127.78353,63.705871 V 18.570709 L 82.647904,18.57066 98.960214,34.88304 73.236104,62.878113 47.504608,34.890884 63.824772,18.570709 Z"
|
||||
id="rect827-0-3" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
59
html/images/custommap/icons/router.svg
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
id="svg6315"
|
||||
viewBox="0 0 146.47212 146.45606"
|
||||
stroke="#2c2c2c">
|
||||
<defs
|
||||
id="defs6317">
|
||||
<linearGradient
|
||||
osb:paint="solid"
|
||||
id="linearGradient819">
|
||||
<stop
|
||||
id="stop817"
|
||||
offset="0"
|
||||
style="stop-color:#dddddd;stop-opacity:1;" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
y2="73.235916"
|
||||
x2="146.47183"
|
||||
y1="73.235916"
|
||||
x1="3.5762787e-06"
|
||||
id="linearGradient821"
|
||||
xlink:href="#linearGradient819"
|
||||
gradientTransform="matrix(0.99449835,0,0,0.99438474,0.4032344,0.40318833)" />
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata6320">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="g857">
|
||||
<ellipse
|
||||
ry="70.420441"
|
||||
rx="70.428482"
|
||||
style="opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:5.6151762;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:3.875;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path815"
|
||||
cx="73.236069"
|
||||
cy="73.228027" />
|
||||
<path
|
||||
id="rect827-0-3"
|
||||
d="M 33.088769,33.085566 V 66.301839 L 45.088962,54.303037 65.699451,73.22803 45.094734,92.158786 33.088769,80.154219 v 33.216291 h 33.220052 l -12.005955,-12.00464 18.9331,-20.602205 18.92699,20.607915 -12.000209,11.99889 33.218623,-0.002 0.002,-33.214769 L 101.37152,92.164122 80.772525,73.227966 101.37721,54.296922 113.38319,66.301496 V 33.085584 L 80.16311,33.085548 92.169056,45.090168 73.235974,65.69234 54.297455,45.095941 66.309184,33.085584 Z"
|
||||
style="opacity:1;fill:#3a3a3a;fill-opacity:1;stroke-width:1.82743692;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
44
html/images/custommap/icons/switch-l2.svg
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="#2c2c2c"
|
||||
viewBox="0 0 146.4722 146.23581"
|
||||
id="svg7778"
|
||||
version="1.1">
|
||||
<defs
|
||||
id="defs7780" />
|
||||
<metadata
|
||||
id="metadata7783">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="g818">
|
||||
<rect
|
||||
style="opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:5.6465888;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect4136-2-3-3"
|
||||
width="140.82561"
|
||||
height="140.58922"
|
||||
x="2.8232944"
|
||||
y="2.8232942"
|
||||
ry="8.9952965" />
|
||||
<path
|
||||
id="rect854"
|
||||
d="m 83.222724,28.396005 44.297706,19.922227 -44.297706,19.922228 -0.26184,-0.117259 1.2e-5,-15.384667 -64.623254,1.071754 V 42.826296 l 64.623254,1.071634 -1.2e-5,-15.384547 z"
|
||||
style="fill:#3a3a3a;fill-opacity:1;stroke-width:1.95391774;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
<path
|
||||
id="rect854-6"
|
||||
d="m 62.635193,77.995336 -44.297702,19.922228 44.297702,19.922226 0.261839,-0.11726 -1.1e-5,-15.38466 64.623249,1.07175 V 92.425628 l -64.623249,1.071634 1.1e-5,-15.384547 z"
|
||||
style="fill:#3a3a3a;fill-opacity:1;stroke-width:1.95391762;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
42
html/images/custommap/icons/switch-l3.svg
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="#2c2c2c"
|
||||
viewBox="0 0 146.47219 146.4722"
|
||||
id="svg6315"
|
||||
version="1.1">
|
||||
<defs
|
||||
id="defs6317" />
|
||||
<metadata
|
||||
id="metadata6320">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
transform="translate(-2.1457672e-6,-2.1457672e-6)"
|
||||
id="g873">
|
||||
<rect
|
||||
ry="8.9564304"
|
||||
y="-143.22699"
|
||||
x="-143.22699"
|
||||
height="139.98178"
|
||||
width="139.98178"
|
||||
id="rect4136-2-3-9-6"
|
||||
style="opacity:1;fill:#fdfdfd;fill-opacity:1;stroke-width:6.49041319;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="scale(-1)" />
|
||||
<path
|
||||
id="rect817-9"
|
||||
d="M 41.349312,18.700379 17.641749,42.141822 H 33.061002 V 57.822419 L 60.50529,72.870592 33.061002,87.91716 v 15.62762 H 17.641749 l 23.707563,23.44145 23.707557,-23.44145 H 50.192784 V 91.80802 l 22.426814,-12.29697 22.42682,12.29697 v 11.73676 H 80.182316 l 23.707564,23.44145 23.70757,-23.44145 H 112.17658 V 87.91716 L 84.732289,72.868989 112.17658,57.822419 V 42.141822 h 15.42087 L 103.88988,18.700379 80.182316,42.141822 H 95.046418 V 53.931564 L 72.619598,66.228528 50.192784,53.931564 V 42.141822 h 14.864085 z"
|
||||
style="fill:#3a3a3a;fill-opacity:1;stroke-width:1.78086972;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
141
lang/en/map.php
Normal file
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'custom' => [
|
||||
'title' => [
|
||||
'edit_dialog' => 'Select Custom Map To Edit',
|
||||
'create' => 'Create New Custom Map',
|
||||
'view' => ':name | Custom Map',
|
||||
'edit' => 'Edit Custom Map',
|
||||
'manage' => 'Manage Custom Maps',
|
||||
],
|
||||
'create_map' => 'New Map',
|
||||
'view' => [
|
||||
'loading' => 'Loading data',
|
||||
'no_devices' => 'No devices found',
|
||||
],
|
||||
'edit' => [
|
||||
'text_font' => 'Text Font',
|
||||
'text_size' => 'Text Size',
|
||||
'text_color' => 'Text Color',
|
||||
'defaults' => 'Set Defaults',
|
||||
'bg' => [
|
||||
'title' => 'Set Background',
|
||||
'background' => 'Background',
|
||||
'clear_bg' => 'Clear BG',
|
||||
'clear_background' => 'Clear Background',
|
||||
'keep_background' => 'Keep Background',
|
||||
'saving' => 'Saving...',
|
||||
'save_errors' => 'Save failed due to the following errors:',
|
||||
'save_error' => 'Save failed. Server returned error response code: :code',
|
||||
'save' => 'Save Background',
|
||||
],
|
||||
'map' => [
|
||||
'settings_title' => 'Map Settings',
|
||||
'name' => 'Name',
|
||||
'width' => 'Width',
|
||||
'height' => 'Height',
|
||||
'alignment' => 'Node Alignment',
|
||||
'saving' => 'Saving...',
|
||||
'save_errors' => 'Save failed due to the following errors:',
|
||||
'save_error' => 'Save failed. Server returned error response code: :code',
|
||||
'delete' => 'Delete Map',
|
||||
'list' => 'Return to map list',
|
||||
'unsavedchanges' => 'You have unsaved changes. Press confirm to discard changes and return to the map list, or cancel to return to the editor.',
|
||||
'edit' => 'Edit Map Settings',
|
||||
'rerender' => 'Re-Render Map',
|
||||
'save' => 'Save Map',
|
||||
],
|
||||
'node' => [
|
||||
'new' => 'New Node',
|
||||
'add' => 'Add Node',
|
||||
'edit' => 'Edit Node',
|
||||
'defaults_title' => 'Node Default Config',
|
||||
'label' => 'Label',
|
||||
'name' => 'Node Name',
|
||||
'device_select' => 'Select Device',
|
||||
'edit_defaults' => 'Edit Node Defaults',
|
||||
'map_link' => 'Link to Map',
|
||||
'map_select' => 'Select Map...',
|
||||
'style' => 'Style',
|
||||
'style_options' => [
|
||||
'box' => 'Box',
|
||||
'circle' => 'Circle',
|
||||
'database' => 'Database',
|
||||
'ellipse' => 'Ellipse',
|
||||
'text' => 'Text',
|
||||
'device_image' => 'Device Image',
|
||||
'device_image_circle' => 'Device Image (Circular)',
|
||||
'diamond' => 'Diamond',
|
||||
'dot' => 'Dot',
|
||||
'star' => 'Star',
|
||||
'triangle' => 'Triangle',
|
||||
'triangle_inverted' => 'Triangle Inverted',
|
||||
'hexagon' => 'Hexagon',
|
||||
'square' => 'Square',
|
||||
'icon' => 'Icon (select below)',
|
||||
],
|
||||
'icon' => 'Icon',
|
||||
'icon_options' => [
|
||||
'server' => 'Server',
|
||||
'desktop' => 'Desktop',
|
||||
'dish' => 'Satellite Dish',
|
||||
'satellite' => 'Satellite',
|
||||
'wifi' => 'Wifi',
|
||||
'cloud' => 'Cloud',
|
||||
'globe' => 'Globe',
|
||||
'tower' => 'Tower',
|
||||
'arrow_right' => 'Arrow - Right',
|
||||
'arrow_left' => 'Arrow - Left',
|
||||
'arrow_up' => 'Arrow - Up',
|
||||
'arrow_down' => 'Arrow - Down',
|
||||
],
|
||||
'image' => 'Image',
|
||||
'image_options' => [
|
||||
'adc' => 'Application Delivery Controller',
|
||||
'firewall' => 'Firewall',
|
||||
'gtm' => 'Global Traffic Manager',
|
||||
'router' => 'Router',
|
||||
'switch-l2' => 'Switch - L2',
|
||||
'switch-l3' => 'Switch - L3',
|
||||
],
|
||||
'size' => 'Node Size',
|
||||
'bg_color' => 'Background Color',
|
||||
'border_color' => 'Border Color',
|
||||
],
|
||||
'edge' => [
|
||||
'new' => 'New Edge',
|
||||
'add' => 'Add Edge',
|
||||
'defaults_title' => 'Edge Default Config',
|
||||
'from' => 'From',
|
||||
'to' => 'To',
|
||||
'port_select' => 'Select Port',
|
||||
'reverse' => 'Reverse Port Direction',
|
||||
'edit_defaults' => 'Edit Edge Defaults',
|
||||
'style' => 'Line Style',
|
||||
'style_options' => [
|
||||
'dynamic' => 'Dynamic',
|
||||
'continuous' => 'Continuous',
|
||||
'discrete' => 'Discrete',
|
||||
'diagonalCross' => 'Diagonal Cross',
|
||||
'straightCross' => 'Straight Cross',
|
||||
'horizontal' => 'Horizontal',
|
||||
'vertical' => 'Vertical',
|
||||
'curvedCW' => 'Curved Clockwise',
|
||||
'curvedCCW' => 'Curved Counter Clockwise',
|
||||
'cubicBezier' => 'Cubic Bezier',
|
||||
],
|
||||
'show_usage_percent' => 'Show percent usage',
|
||||
'recenter' => 'Recenter Line',
|
||||
],
|
||||
'validate' => [
|
||||
'width_format' => 'Width must be a number followed by px or %',
|
||||
'width_percent' => 'Width percent must be between 10 and 100',
|
||||
'width_pixels' => 'Width in pixels must be at least 200',
|
||||
'height_format' => 'Height must be a number followed by px or %',
|
||||
'height_percent' => 'Height percent must be between 10 and 100',
|
||||
'height_pixels' => 'Height in pixels must be at least 200',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
@ -515,6 +515,92 @@ customoids:
|
||||
- { Field: user_func, Type: varchar(100), 'Null': true, Extra: '' }
|
||||
Indexes:
|
||||
PRIMARY: { Name: PRIMARY, Columns: [customoid_id], Unique: true, Type: BTREE }
|
||||
custom_maps:
|
||||
Columns:
|
||||
- { Field: custom_map_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
|
||||
- { Field: name, Type: varchar(100), 'Null': false, Extra: '' }
|
||||
- { 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: background_suffix, Type: varchar(10), 'Null': true, Extra: '' }
|
||||
- { Field: background_version, Type: 'int unsigned', 'Null': false, Extra: '' }
|
||||
- { Field: options, Type: longtext, 'Null': true, Extra: '' }
|
||||
- { Field: newnodeconfig, Type: longtext, 'Null': false, Extra: '' }
|
||||
- { Field: newedgeconfig, Type: longtext, 'Null': false, Extra: '' }
|
||||
- { Field: created_at, Type: timestamp, 'Null': true, Extra: '' }
|
||||
- { Field: updated_at, Type: timestamp, 'Null': true, Extra: '' }
|
||||
Indexes:
|
||||
PRIMARY: { Name: PRIMARY, Columns: [custom_map_id], Unique: true, Type: BTREE }
|
||||
custom_map_backgrounds:
|
||||
Columns:
|
||||
- { Field: custom_map_background_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
|
||||
- { Field: created_at, Type: timestamp, 'Null': true, Extra: '' }
|
||||
- { Field: updated_at, Type: timestamp, 'Null': true, Extra: '' }
|
||||
- { Field: custom_map_id, Type: 'int unsigned', 'Null': false, Extra: '' }
|
||||
- { Field: background_image, Type: mediumblob, 'Null': true, Extra: '' }
|
||||
Indexes:
|
||||
PRIMARY: { Name: PRIMARY, Columns: [custom_map_background_id], Unique: true, Type: BTREE }
|
||||
custom_map_backgrounds_custom_map_id_unique: { Name: custom_map_backgrounds_custom_map_id_unique, Columns: [custom_map_id], Unique: true, Type: BTREE }
|
||||
Constraints:
|
||||
custom_map_backgrounds_custom_map_id_foreign: { name: custom_map_backgrounds_custom_map_id_foreign, foreign_key: custom_map_id, table: custom_maps, key: custom_map_id, extra: 'ON DELETE CASCADE' }
|
||||
custom_map_edges:
|
||||
Columns:
|
||||
- { Field: custom_map_edge_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
|
||||
- { Field: custom_map_id, Type: 'int unsigned', 'Null': false, Extra: '' }
|
||||
- { Field: custom_map_node1_id, Type: 'int unsigned', 'Null': false, Extra: '' }
|
||||
- { Field: custom_map_node2_id, Type: 'int unsigned', 'Null': false, Extra: '' }
|
||||
- { Field: port_id, Type: 'int unsigned', 'Null': true, Extra: '' }
|
||||
- { Field: reverse, Type: tinyint, 'Null': false, Extra: '' }
|
||||
- { Field: style, Type: varchar(50), 'Null': false, Extra: '' }
|
||||
- { Field: showpct, Type: tinyint, 'Null': false, Extra: '' }
|
||||
- { 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: '' }
|
||||
- { Field: mid_x, Type: int, 'Null': false, Extra: '' }
|
||||
- { Field: mid_y, Type: int, 'Null': false, Extra: '' }
|
||||
- { Field: created_at, Type: timestamp, 'Null': true, Extra: '' }
|
||||
- { Field: updated_at, Type: timestamp, 'Null': true, Extra: '' }
|
||||
Indexes:
|
||||
PRIMARY: { Name: PRIMARY, Columns: [custom_map_edge_id], Unique: true, Type: BTREE }
|
||||
custom_map_edges_custom_map_id_index: { Name: custom_map_edges_custom_map_id_index, Columns: [custom_map_id], Unique: false, Type: BTREE }
|
||||
custom_map_edges_custom_map_node1_id_index: { Name: custom_map_edges_custom_map_node1_id_index, Columns: [custom_map_node1_id], Unique: false, Type: BTREE }
|
||||
custom_map_edges_custom_map_node2_id_index: { Name: custom_map_edges_custom_map_node2_id_index, Columns: [custom_map_node2_id], Unique: false, Type: BTREE }
|
||||
custom_map_edges_port_id_index: { Name: custom_map_edges_port_id_index, Columns: [port_id], Unique: false, Type: BTREE }
|
||||
Constraints:
|
||||
custom_map_edges_custom_map_id_foreign: { name: custom_map_edges_custom_map_id_foreign, foreign_key: custom_map_id, table: custom_maps, key: custom_map_id, extra: 'ON DELETE CASCADE' }
|
||||
custom_map_edges_custom_map_node1_id_foreign: { name: custom_map_edges_custom_map_node1_id_foreign, foreign_key: custom_map_node1_id, table: custom_map_nodes, key: custom_map_node_id, extra: 'ON DELETE CASCADE' }
|
||||
custom_map_edges_custom_map_node2_id_foreign: { name: custom_map_edges_custom_map_node2_id_foreign, foreign_key: custom_map_node2_id, table: custom_map_nodes, key: custom_map_node_id, extra: 'ON DELETE CASCADE' }
|
||||
custom_map_edges_port_id_foreign: { name: custom_map_edges_port_id_foreign, foreign_key: port_id, table: ports, key: port_id, extra: 'ON DELETE SET NULL' }
|
||||
custom_map_nodes:
|
||||
Columns:
|
||||
- { Field: custom_map_node_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
|
||||
- { Field: custom_map_id, Type: 'int unsigned', 'Null': false, Extra: '' }
|
||||
- { Field: device_id, Type: 'int unsigned', 'Null': true, Extra: '' }
|
||||
- { Field: linked_custom_map_id, Type: 'int unsigned', 'Null': true, Extra: '' }
|
||||
- { Field: label, Type: varchar(50), 'Null': false, Extra: '' }
|
||||
- { Field: style, Type: varchar(50), 'Null': false, Extra: '' }
|
||||
- { Field: icon, Type: varchar(8), 'Null': true, Extra: '' }
|
||||
- { Field: image, Type: varchar(255), 'Null': false, Extra: '', Default: '' }
|
||||
- { Field: size, Type: int, 'Null': false, Extra: '' }
|
||||
- { Field: border_width, Type: int, 'Null': false, Extra: '' }
|
||||
- { 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: '' }
|
||||
- { Field: colour_bg, Type: varchar(10), 'Null': true, Extra: '' }
|
||||
- { Field: colour_bdr, Type: varchar(10), 'Null': true, Extra: '' }
|
||||
- { Field: x_pos, Type: int, 'Null': false, Extra: '' }
|
||||
- { Field: y_pos, Type: int, 'Null': false, Extra: '' }
|
||||
- { Field: created_at, Type: timestamp, 'Null': true, Extra: '' }
|
||||
- { Field: updated_at, Type: timestamp, 'Null': true, Extra: '' }
|
||||
Indexes:
|
||||
PRIMARY: { Name: PRIMARY, Columns: [custom_map_node_id], Unique: true, Type: BTREE }
|
||||
custom_map_nodes_custom_map_id_index: { Name: custom_map_nodes_custom_map_id_index, Columns: [custom_map_id], Unique: false, Type: BTREE }
|
||||
custom_map_nodes_device_id_index: { Name: custom_map_nodes_device_id_index, Columns: [device_id], Unique: false, Type: BTREE }
|
||||
custom_map_nodes_linked_custom_map_id_index: { Name: custom_map_nodes_linked_custom_map_id_index, Columns: [linked_custom_map_id], Unique: false, Type: BTREE }
|
||||
Constraints:
|
||||
custom_map_nodes_custom_map_id_foreign: { name: custom_map_nodes_custom_map_id_foreign, foreign_key: custom_map_id, table: custom_maps, key: custom_map_id, extra: 'ON DELETE CASCADE' }
|
||||
custom_map_nodes_device_id_foreign: { name: custom_map_nodes_device_id_foreign, foreign_key: device_id, table: devices, key: device_id, extra: 'ON DELETE SET NULL' }
|
||||
custom_map_nodes_linked_custom_map_id_foreign: { name: custom_map_nodes_linked_custom_map_id_foreign, foreign_key: linked_custom_map_id, table: custom_maps, key: custom_map_id, extra: 'ON DELETE SET NULL' }
|
||||
dashboards:
|
||||
Columns:
|
||||
- { Field: dashboard_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
|
||||
|
@ -162,9 +162,14 @@ nav:
|
||||
- Check_MK Setup: Extensions/Agent-Setup.md
|
||||
- Dashboards: Extensions/Dashboards.md
|
||||
- Interface Description Parsing: Extensions/Interface-Description-Parsing.md
|
||||
- Network Map: Extensions/Network-Map.md
|
||||
- Network Maps:
|
||||
- Availability Map: Extensions/Availability-Map.md
|
||||
- Dependency Map: Extensions/Dependency-Map.md
|
||||
- Network Map: Extensions/Network-Map.md
|
||||
- Custom Map: Extensions/Custom-Map.md
|
||||
- World Map: Extensions/World-Map.md
|
||||
- VisJS Config: Extensions/VisJS-Config.md
|
||||
- Syslog: Extensions/Syslog.md
|
||||
- World Map: Extensions/World-Map.md
|
||||
- Advanced Setup:
|
||||
- 1 Minute Polling: Support/1-Minute-Polling.md
|
||||
- Authentication Options: Extensions/Authentication.md
|
||||
|
@ -76,6 +76,23 @@
|
||||
@endforeach
|
||||
</ul></li>
|
||||
@endif
|
||||
@admin
|
||||
<li><a href="{{ route('maps.custom.index') }}"><i class="fa fa-map-marked fa-fw fa-lg"
|
||||
aria-hidden="true"></i> {{ __('Custom Map Editor') }}
|
||||
</a></li>
|
||||
@endadmin
|
||||
@if($custommaps->isNotEmpty())
|
||||
<li class="dropdown-submenu"><a><i class="fa fa-map-marked fa-fw fa-lg"
|
||||
aria-hidden="true"></i> {{ __('Custom Maps') }}
|
||||
</a>
|
||||
<ul class="dropdown-menu scrollable-menu">
|
||||
@foreach($custommaps as $map)
|
||||
<li><a href="{{ route('maps.custom.show', ['map' => $map->custom_map_id]) }}" title="{{ $map->name }}"><i class="fa fa-map-marked fa-fw fa-lg" aria-hidden="true"></i>
|
||||
{{ ucfirst($map->name) }}
|
||||
</a></li>
|
||||
@endforeach
|
||||
</ul></li>
|
||||
@endif
|
||||
<li><a href="{{ url('fullscreenmap') }}"><i class="fa fa-expand fa-fw fa-lg"
|
||||
aria-hidden="true"></i> {{ __('Geographical') }}
|
||||
</a></li>
|
||||
@ -784,5 +801,5 @@
|
||||
})
|
||||
})
|
||||
@endif
|
||||
|
||||
|
||||
</script>
|
||||
|
118
resources/views/map/custom-background-modal.blade.php
Normal file
@ -0,0 +1,118 @@
|
||||
<div class="modal fade" id="bgModal" tabindex="-1" role="dialog" aria-labelledby="bgModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="bgModalLabel">{{ __('map.custom.edit.bg.title') }}</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="well well-lg">
|
||||
<div class="form-group row" id="mapBackgroundRow">
|
||||
<label for="selectbackground" class="col-sm-3 control-label">{{ __('map.custom.edit.bg.background') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input id="mapBackgroundSelect" type="file" name="selectbackground" accept="image/png,image/jpeg,image/svg+xml,image/gif" class="form-control" onchange="mapChangeBackground();">
|
||||
<button id="mapBackgroundCancel" type="button" name="cancelbackground" class="btn btn-primary" onclick="mapChangeBackgroundCancel();" style="display:none">{{ __('Cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" id="mapBackgroundClearRow">
|
||||
<label for="clearbackground" class="col-sm-3 control-label">{{ __('map.custom.edit.bg.clear_bg') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="hidden" id="mapBackgroundClearVal">
|
||||
<button id="mapBackgroundClear" type="button" name="clearbackground" class="btn btn-primary" onclick="mapClearBackground();">{{ __('map.custom.edit.bg.clear_background') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-sm-12" id="savebg-alert">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<center>
|
||||
<button type=button value="save" id="map-savebgButton" class="btn btn-primary" onclick="saveMapBackground()">{{ __('Save') }}</button>
|
||||
<button type=button value="cancel" id="map-cancelbgButton" class="btn btn-primary" onclick="editMapBackgroundCancel()">{{ __('Cancel') }}</button>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function mapChangeBackground() {
|
||||
$("#mapBackgroundCancel").show();
|
||||
}
|
||||
|
||||
function mapChangeBackgroundCancel() {
|
||||
$("#mapBackgroundCancel").hide();
|
||||
$("#mapBackgroundSelect").val(null);
|
||||
}
|
||||
|
||||
function mapClearBackground() {
|
||||
if($('#mapBackgroundClearVal').val()) {
|
||||
$('#mapBackgroundClear').text('{{ __('map.custom.edit.bg.clear_background') }}');
|
||||
$('#mapBackgroundClearVal').val('');
|
||||
} else {
|
||||
$('#mapBackgroundClear').text('{{ __('map.custom.edit.bg.keep_background') }}');
|
||||
$('#mapBackgroundClearVal').val('clear');
|
||||
}
|
||||
}
|
||||
|
||||
function editMapBackgroundCancel() {
|
||||
$('#mapBackgroundClear').text('{{ __('map.custom.edit.bg.clear_background') }}');
|
||||
$('#mapBackgroundClearVal').val('');
|
||||
$("#mapBackgroundCancel").hide();
|
||||
$("#mapBackgroundSelect").val(null);
|
||||
$('#bgModal').modal('hide');
|
||||
}
|
||||
|
||||
function saveMapBackground() {
|
||||
$("#map-savebgButton").attr('disabled','disabled');
|
||||
$("#savebg-alert").text('{{ __('map.custom.edit.bg.saving') }}');
|
||||
$("#savebg-alert").attr("class", "col-sm-12 alert alert-info");
|
||||
|
||||
var clearbackground = $('#mapBackgroundClearVal').val() ? 1 : 0;
|
||||
var newbackground = $('#mapBackgroundSelect').prop('files').length ? $('#mapBackgroundSelect').prop('files')[0] : '';
|
||||
|
||||
var url = '{{ route('maps.custom.background.save', ['map' => $map_id]) }}';
|
||||
var fd = new FormData();
|
||||
fd.append('bgclear', clearbackground);
|
||||
fd.append('bgimage', newbackground);
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
data: fd,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
type: 'POST'
|
||||
}).done(function (data, status, resp) {
|
||||
canvas = $("#custom-map").children()[0].canvas;
|
||||
if(data['bgimage']) {
|
||||
$(canvas).css('background-image','url({{ route('maps.custom.background', ['map' => $map_id]) }}?ver=' + data['bgversion'] + ')').css('background-size', 'cover');
|
||||
bgimage = true;
|
||||
} else {
|
||||
$(canvas).css('background-image','');
|
||||
bgimage = false;
|
||||
}
|
||||
$("#savebg-alert").attr("class", "col-sm-12");
|
||||
$("#savebg-alert").text("");
|
||||
editMapBackgroundCancel();
|
||||
}).fail(function (resp, status, error) {
|
||||
var data = resp.responseJSON;
|
||||
if (data['message']) {
|
||||
let alert_content = $("#savebg-alert");
|
||||
alert_content.text(data['message']);
|
||||
alert_content.attr("class", "col-sm-12 alert alert-danger");
|
||||
} else {
|
||||
let alert_content = $("#savebg-alert");
|
||||
alert_content.text('{{ __('map.custom.edit.bg.save_error', ['code' => '?']) }}'.replace('?', resp.status));
|
||||
alert_content.attr("class", "col-sm-12 alert alert-danger");
|
||||
}
|
||||
}).always(function (resp, status, error) {
|
||||
$("#map-savebgButton").removeAttr('disabled');
|
||||
});
|
||||
}
|
||||
</script>
|
117
resources/views/map/custom-edge-modal.blade.php
Normal file
@ -0,0 +1,117 @@
|
||||
<div class="modal fade" id="edgeModal" tabindex="-1" role="dialog" aria-labelledby="edgeModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="edgeModalLabel">{{ __('map.custom.edit.edge.new') }}</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="well well-lg">
|
||||
<div class="form-group row" id="divEdgeFrom">
|
||||
<label for="edgefrom" class="col-sm-3 control-label">{{ __('map.custom.edit.edge.from') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<select id="edgefrom" class="form-control input-sm">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" id="divEdgeTo">
|
||||
<label for="edgeto" class="col-sm-3 control-label">{{ __('map.custom.edit.edge.to') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<select id="edgeto" class="form-control input-sm">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row single-node" id="edgePortSearchRow" style="display:none">
|
||||
<label for="portsearch" class="col-sm-3 control-label">{{ __('map.custom.edit.edge.port_select') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<select name="portsearch" id="portsearch" class="form-control"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" id="edgePortRow" style="display:none">
|
||||
<label for="portclear" class="col-sm-3 control-label">{{ __('Port') }}</label>
|
||||
<div class="col-sm-7">
|
||||
<div id="port_name">
|
||||
</div>
|
||||
<input type="hidden" id="port_id">
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<button type=button class="btn btn-primary" value="save" id="portclear" onclick="edgePortClear();">{{ __('Clear') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" id="edgePortReverseRow" style="display:none">
|
||||
<label for="portreverse" class="col-sm-3 control-label">{{ __('map.custom.edit.edge.reverse') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="portreverse">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="edgestyle" class="col-sm-3 control-label">{{ __('map.custom.edit.edge.style') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<select id="edgestyle" class="form-control input-sm">
|
||||
<option value="dynamic">{{ __('map.custom.edit.edge.style_options.dynamic') }}</option>
|
||||
<option value="continuous">{{ __('map.custom.edit.edge.style_options.continuous') }}</option>
|
||||
<option value="discrete">{{ __('map.custom.edit.edge.style_options.discrete') }}</option>
|
||||
<option value="diagonalCross">{{ __('map.custom.edit.edge.style_options.diagonalCross') }}</option>
|
||||
<option value="straightCross">{{ __('map.custom.edit.edge.style_options.straightCross') }}</option>
|
||||
<option value="horizontal">{{ __('map.custom.edit.edge.style_options.horizontal') }}</option>
|
||||
<option value="vertical">{{ __('map.custom.edit.edge.style_options.vertical') }}</option>
|
||||
<option value="curvedCW">{{ __('map.custom.edit.edge.style_options.curvedCW') }}</option>
|
||||
<option value="curvedCCW">{{ __('map.custom.edit.edge.style_options.curvedCCW') }}</option>
|
||||
<option value="cubicBezier">{{ __('map.custom.edit.edge.style_options.cubicBezier') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<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="edgetextface" class="col-sm-3 control-label">{{ __('map.custom.edit.text_font') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type=text id="edgetextface" class="form-control input-sm" value="arial" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="edgetextsize" class="col-sm-3 control-label">{{ __('map.custom.edit.text_size') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type=number id="edgetextsize" class="form-control input-sm" value=14 />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="edgetextcolour" class="col-sm-3 control-label">{{ __('map.custom.edit.text_color') }}</label>
|
||||
<div class="col-sm-2">
|
||||
<input type=color id="edgetextcolour" class="form-control input-sm" value="#343434" onchange="$('#edgecolourtextreset').removeAttr('disabled');" />
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<button type=button class="btn btn-primary" value="reset" id="edgecolourtextreset" onclick="$('#edgetextcolour').val(newedgeconf.font.color); $(this).attr('disabled','disabled');">{{ __('Reset') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" id="edgeRecenterRow">
|
||||
<label for="edgerecenter" class="col-sm-3 control-label">{{ __('map.custom.edit.edge.recenter') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type=checkbox class="form-check-input" value="recenter" id="edgerecenter">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12" id="saveedge-alert">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<center>
|
||||
<button type=button class="btn btn-primary" value="savedefaults" id="edge-saveDefaultsButton" data-dismiss="modal" style="display:none" onclick="editEdgeDefaultsSave();">{{ __('map.custom.edit.defaults') }}</button>
|
||||
<button type=button class="btn btn-primary" value="save" id="edge-saveButton" data-dismiss="modal">{{ __('Save') }}</button>
|
||||
<button type=button class="btn btn-primary" value="cancel" id="edge-cancelButton" data-dismiss="modal" onclick="editEdgeCancel();">{{ __('Cancel') }}</button>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
939
resources/views/map/custom-edit.blade.php
Normal file
@ -0,0 +1,939 @@
|
||||
@extends('layouts.librenmsv1')
|
||||
|
||||
@section('title', __('map.custom.title.edit'))
|
||||
|
||||
@section('content')
|
||||
|
||||
@include('map.custom-background-modal')
|
||||
@include('map.custom-node-modal')
|
||||
@include('map.custom-edge-modal')
|
||||
@include('map.custom-map-modal')
|
||||
@include('map.custom-map-list-modal')
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row" id="control-row">
|
||||
<div class="col-md-5">
|
||||
<button type=button value="mapedit" id="map-editButton" class="btn btn-primary" onclick="editMapSettings();">{{ __('map.custom.edit.map.edit') }}</button>
|
||||
<button type=button value="mapbg" id="map-bgButton" class="btn btn-primary" onclick="editMapBackground();">{{ __('map.custom.edit.bg.title') }}</button>
|
||||
<button type=button value="editnodedefaults" id="map-nodeDefaultsButton" class="btn btn-primary" onclick="editNodeDefaults();">{{ __('map.custom.edit.node.edit_defaults') }}</button>
|
||||
<button type=button value="editedgedefaults" id="map-edgeDefaultsButton" class="btn btn-primary" onclick="editEdgeDefaults();">{{ __('map.custom.edit.edge.edit_defaults') }}</button>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<center>
|
||||
<h4 id="title">{{ $name }}</h4>
|
||||
</center>
|
||||
</div>
|
||||
<div class="col-md-5 text-right">
|
||||
<button type=button value="maprender" id="map-renderButton" class="btn btn-primary" style="display: none" onclick="CreateNetwork();">{{ __('map.custom.edit.map.rerender') }}</button>
|
||||
<button type=button value="mapsave" id="map-saveDataButton" class="btn btn-primary" style="display: none" onclick="saveMapData();">{{ __('map.custom.edit.map.save') }}</button>
|
||||
<button type=button value="maplist" id="map-listButton" class="btn btn-primary" onclick="mapList();">{{ __('map.custom.edit.map.list') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" id="control-map-sep">
|
||||
<div class="col-md-12">
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
<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 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 node_device_map = {};
|
||||
var custom_image_base = "images/custommap/icons/";
|
||||
|
||||
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) !!};
|
||||
|
||||
// Set up the triggers for adding and editing map items
|
||||
options['manipulation']['addNode'] = function (data, callback) {
|
||||
callback(null);
|
||||
$("#nodeModalLabel").text('{{ __('map.custom.edit.node.add') }}');
|
||||
var node = structuredClone(newnodeconf);
|
||||
node.id = "new" + newcount++;
|
||||
node.label = "New Node";
|
||||
node.x = node_align ? Math.round(data.x / node_align) * node_align : data.x;
|
||||
node.y = node_align ? Math.round(data.y / node_align) * node_align : data.y;
|
||||
node.add = true;
|
||||
$(".single-node").show();
|
||||
editNode(node, editNodeSave);
|
||||
}
|
||||
options['manipulation']['editNode'] = function (data, callback) {
|
||||
callback(null);
|
||||
$("#nodeModalLabel").text('{{ __('map.custom.edit.node.edit') }}');
|
||||
$(".single-node").show();
|
||||
editNode(data, editNodeSave);
|
||||
}
|
||||
options['manipulation']['deleteNode'] = function (data, callback) {
|
||||
callback(null);
|
||||
$.each( data.edges, function( edge_idx, edgeid ) {
|
||||
edgeid = edgeid.split("_")[0];
|
||||
deleteEdge(edgeid);
|
||||
});
|
||||
$.each( data.nodes, function( node_idx, nodeid ) {
|
||||
network_nodes.remove(nodeid);
|
||||
network_nodes.flush();
|
||||
});
|
||||
$("#map-saveDataButton").show();
|
||||
}
|
||||
options['manipulation']['addEdge'] = function (data, callback) {
|
||||
// Because we deal with multiple edges, do not use the default callback
|
||||
callback(null);
|
||||
|
||||
// Do not allow linking to the same node
|
||||
if(data.to == data.from) {
|
||||
return;
|
||||
}
|
||||
// Do not allow linking to the mid point nodes
|
||||
if(isNaN(data.to) && data.to.endsWith("_mid")) {
|
||||
return;
|
||||
}
|
||||
if(isNaN(data.from) && data.from.endsWith("_mid")) {
|
||||
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++;
|
||||
|
||||
var mid = {id: edgeid + "_mid", shape: "dot", size: 3, x: mid_x, y: mid_y};
|
||||
|
||||
var edge1 = structuredClone(newedgeconf);
|
||||
edge1.id = edgeid + "_from";
|
||||
edge1.from = data.from;
|
||||
edge1.to = edgeid + "_mid";
|
||||
|
||||
var edge2 = structuredClone(newedgeconf);
|
||||
edge2.id = edgeid + "_to";
|
||||
edge2.from = data.to;
|
||||
edge2.to = edgeid + "_mid";
|
||||
|
||||
var edgedata = {id: edgeid, mid: mid, edge1: edge1, edge2: edge2, add: true}
|
||||
|
||||
$("#edgeModalLabel").text('{{ __('map.custom.edit.edge.add') }}');
|
||||
editEdge(edgedata, editEdgeSave);
|
||||
}
|
||||
options['manipulation']['editEdge'] = { editWithoutDrag: editExistingEdge };
|
||||
options['manipulation']['deleteEdge'] = function (data, callback) {
|
||||
callback(null);
|
||||
$.each( data.edges, function( edge_idx, edgeid ) {
|
||||
edgeid = edgeid.split("_")[0];
|
||||
deleteEdge(edgeid);
|
||||
});
|
||||
};
|
||||
|
||||
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) {
|
||||
node_id = properties.nodes[0];
|
||||
node = network_nodes.get(node_id);
|
||||
$("#nodeModalLabel").text('{{ __('map.custom.edit.node.edit') }}');
|
||||
$(".single-node").show();
|
||||
editNode(node, editNodeSave);
|
||||
} else if (properties.edges.length > 0) {
|
||||
edge_id = properties.edges[0].split("_")[0];
|
||||
edge = network_edges.get(edge_id + "_to");
|
||||
editExistingEdge(edge, null);
|
||||
}
|
||||
});
|
||||
|
||||
network.on('dragEnd', function (data) {
|
||||
if(data.edges.length > 0 || data.nodes.length > 0) {
|
||||
// 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 ( move ) {
|
||||
network.moveNode(nodeid, node.x, node.y);
|
||||
}
|
||||
node.id = nodeid;
|
||||
network_nodes.update(node);
|
||||
});
|
||||
$("#map-saveDataButton").show();
|
||||
$("#map-renderButton").show();
|
||||
}
|
||||
});
|
||||
$("#map-renderButton").hide();
|
||||
}
|
||||
|
||||
function editMapSettings() {
|
||||
$('#mapModal').modal({backdrop: 'static', keyboard: false}, 'show');
|
||||
}
|
||||
|
||||
var newedgeconf = @json($newedge_conf);
|
||||
var newnodeconf = @json($newnode_conf);
|
||||
var newcount = 1;
|
||||
var port_search_device_id_1 = 0;
|
||||
var port_search_device_id_2 = 0;
|
||||
|
||||
var edge_port_map = {};
|
||||
|
||||
function mapList() {
|
||||
if($("#map-saveDataButton").is(":visible")) {
|
||||
$('#mapListModal').modal({backdrop: 'static', keyboard: false}, 'show');
|
||||
} else {
|
||||
viewList();
|
||||
}
|
||||
}
|
||||
|
||||
function viewList() {
|
||||
window.location.href = "{{ route('maps.custom.index') }}";
|
||||
}
|
||||
|
||||
function editMapSuccess(data) {
|
||||
$("#title").text(data.name);
|
||||
$("#savemap-alert").attr("class", "col-sm-12");
|
||||
$("#savemap-alert").text("");
|
||||
network.setSize(data.width, data.height);
|
||||
|
||||
editMapCancel();
|
||||
}
|
||||
|
||||
function editMapCancel() {
|
||||
$('#mapModal').modal('hide');
|
||||
}
|
||||
|
||||
function saveMapData() {
|
||||
$("#map-saveDataButton").attr('disabled', 'disabled');
|
||||
var nodes = {};
|
||||
var edges = {};
|
||||
|
||||
$.each(network_nodes.get(), function (node_idx, node) {
|
||||
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)};
|
||||
} else {
|
||||
if(node.icon.code) {
|
||||
node.icon = node.icon.code.charCodeAt(0).toString(16);
|
||||
} else {
|
||||
node.icon = null;
|
||||
}
|
||||
if("unselected" in node.image) {
|
||||
if(node.image.unselected.indexOf(custom_image_base) == 0) {
|
||||
node.image.unselected = node.image.unselected.replace(custom_image_base, "");
|
||||
} else {
|
||||
node.image = {};
|
||||
}
|
||||
}
|
||||
nodes[node.id] = node;
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: '{{ route('maps.custom.data.save', ['map' => $map_id]) }}',
|
||||
data: {
|
||||
newnodeconf: newnodeconf,
|
||||
newedgeconf: newedgeconf,
|
||||
nodes: nodes,
|
||||
edges: edges,
|
||||
},
|
||||
dataType: 'json',
|
||||
type: 'POST'
|
||||
}).done(function (data, status, resp) {
|
||||
$("#map-saveDataButton").hide();
|
||||
$("#alert-row").hide();
|
||||
|
||||
// Re-read the map from the DB in case any items were modified
|
||||
refreshMap();
|
||||
}).fail(function (resp, status, error) {
|
||||
var data = resp.responseJSON;
|
||||
if (data['message']) {
|
||||
let alert_content = $("#alert");
|
||||
alert_content.text(data['message']);
|
||||
alert_content.attr("class", "col-sm-12 alert alert-danger");
|
||||
} else {
|
||||
let alert_content = $("#alert");
|
||||
alert_content.text('{{ __('map.custom.edit.map.save_error', ['code' => '?']) }}'.replace('?', resp.status));
|
||||
alert_content.attr("class", "col-sm-12 alert alert-danger");
|
||||
}
|
||||
}).always(function (resp, status, error) {
|
||||
$("#map-saveDataButton").removeAttr('disabled');
|
||||
});
|
||||
}
|
||||
|
||||
function editMapBackground() {
|
||||
$("#mapBackgroundCancel").hide();
|
||||
$("#mapBackgroundSelect").val(null);
|
||||
|
||||
if($("#custom-map").children()[0].canvas.style.backgroundImage) {
|
||||
$("#mapBackgroundClearRow").show();
|
||||
} else {
|
||||
$("#mapBackgroundClearRow").hide();
|
||||
}
|
||||
$('#bgModal').modal({backdrop: 'static', keyboard: false}, 'show');
|
||||
}
|
||||
|
||||
function nodeStyleChange() {
|
||||
var nodestyle = $("#nodestyle").val();
|
||||
if(nodestyle == 'icon') {
|
||||
$("#nodeIconRow").show();
|
||||
} else {
|
||||
$("#nodeIconRow").hide();
|
||||
}
|
||||
if(nodestyle == 'image' || nodestyle == 'circularImage') {
|
||||
$("#nodeImageRow").show();
|
||||
} else {
|
||||
$("#nodeImageRow").hide();
|
||||
}
|
||||
}
|
||||
|
||||
function nodeDeviceSelect(e) {
|
||||
var id = e.params.data.id;
|
||||
var name = e.params.data.text;
|
||||
$("#device_id").val(id);
|
||||
$("#device_name").text(name);
|
||||
$("#nodelabel").val(name.split(".")[0].split(" ")[0]);
|
||||
$("#device_image").val(e.params.data.icon);
|
||||
$("#nodeDeviceSearchRow").hide();
|
||||
$("#nodeMapLinkRow").hide();
|
||||
$("#deviceiconimage").show();
|
||||
$("#nodeDeviceRow").show();
|
||||
}
|
||||
|
||||
function nodeDeviceClear() {
|
||||
$("#devicesearch").val('');
|
||||
$("#devicesearch").trigger('change');
|
||||
$("#device_id").val("");
|
||||
$("#device_name").text("");
|
||||
$("#device_image").val("");
|
||||
$("#nodeDeviceRow").hide();
|
||||
$("#deviceiconimage").hide();
|
||||
$("#nodeDeviceSearchRow").show();
|
||||
$("#nodeMapLinkRow").show();
|
||||
|
||||
// Reset device style if we were using the device image
|
||||
if(($("#nodestyle").val() == "image" || $("#nodestyle").val() == "circularImage") && !$("#nodeimage").val()){
|
||||
$("#nodestyle").val(newnodeconf.shape);
|
||||
$("#nodeImageRow").hide();
|
||||
setNodeImage();
|
||||
}
|
||||
}
|
||||
|
||||
function nodeMapLinkChange() {
|
||||
if($("#maplink").val()) {
|
||||
$("#nodeDeviceSearchRow").hide();
|
||||
} else {
|
||||
$("#nodeDeviceSearchRow").show();
|
||||
}
|
||||
}
|
||||
|
||||
function setNodeImage() {
|
||||
// If the selected option is not visible, select the top option
|
||||
if($("#nodeimage option:selected").css('display') == 'none') {
|
||||
$("#nodeimage").val($("#nodeimage option:eq(1)").val());
|
||||
}
|
||||
// Set the image preview src
|
||||
if($("#nodeimage").val()) {
|
||||
$("#nodeimagepreview").attr("src", custom_image_base + $("#nodeimage").val());
|
||||
} else {
|
||||
$("#nodeimagepreview").attr("src", $("#device_image").val());
|
||||
}
|
||||
}
|
||||
|
||||
function setNodeIcon() {
|
||||
var newcode = $("#nodeicon").val();
|
||||
$("#nodeiconpreview").text(String.fromCharCode(parseInt(newcode, 16)));
|
||||
}
|
||||
|
||||
function editNodeDefaults() {
|
||||
$("#nodeModalLabel").text('{{ __('map.custom.edit.node.defaults_title') }}');
|
||||
$(".single-node").hide();
|
||||
var node = structuredClone(newnodeconf);
|
||||
editNode(node, editNodeDefaultsSave);
|
||||
}
|
||||
|
||||
function editNodeDefaultsSave() {
|
||||
newnodeconf.shape = $("#nodestyle").val();
|
||||
newnodeconf.font.face = $("#nodetextface").val();
|
||||
newnodeconf.font.size = $("#nodetextsize").val();
|
||||
newnodeconf.font.color = $("#nodetextcolour").val();
|
||||
newnodeconf.color.background = $("#nodecolourbg").val();
|
||||
newnodeconf.color.border = $("#nodecolourbdr").val();
|
||||
if(newnodeconf.shape == "icon") {
|
||||
newnodeconf.icon = {face: 'FontAwesome', code: String.fromCharCode(parseInt($("#nodeicon").val(), 16)), size: $("#nodesize").val(), color: newnodeconf.color.border};
|
||||
} else {
|
||||
newnodeconf.icon = {};
|
||||
}
|
||||
if(newnodeconf.shape == "image" || newnodeconf.shape == "circularImage") {
|
||||
newnodeconf.image = {unselected: custom_image_base + $("#nodeimage").val()};
|
||||
} else {
|
||||
delete newnodeconf.image;
|
||||
}
|
||||
$("#map-saveDataButton").show();
|
||||
}
|
||||
|
||||
function checkColourReset(itemColour, defaultColour, resetControlId) {
|
||||
if(!itemColour || itemColour.toLowerCase() == defaultColour.toLowerCase()) {
|
||||
$("#" + resetControlId).attr('disabled','disabled');
|
||||
} else {
|
||||
$("#" + resetControlId).removeAttr('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
function editNode(data, callback) {
|
||||
$("#devicesearch").val('');
|
||||
$("#devicesearch").trigger('change');
|
||||
if(data.id && isNaN(data.id) && data.id.endsWith("_mid")) {
|
||||
edge = network_edges.get((data.id.split("_")[0]) + "_to");
|
||||
editExistingEdge(edge, null);
|
||||
return;
|
||||
}
|
||||
if(data.id in node_device_map) {
|
||||
// Nodes is linked to a device
|
||||
$("#device_id").val(node_device_map[data.id].device_id);
|
||||
$("#device_name").text(node_device_map[data.id].device_name);
|
||||
// Hide device selection row
|
||||
$("#nodeDeviceSearchRow").hide();
|
||||
$("#nodeMapLinkRow").hide();
|
||||
// Show device image as an option
|
||||
$("#deviceiconimage").show();
|
||||
$("#device_image").val(node_device_map[data.id].device_image);
|
||||
} else {
|
||||
// Node is not linked to a device
|
||||
$("#device_id").val("");
|
||||
$("#device_name").text("");
|
||||
// Hide the selected device row
|
||||
$("#nodeDeviceRow").hide();
|
||||
// Hide device image as an option
|
||||
$("#deviceiconimage").hide();
|
||||
$("#device_image").val("");
|
||||
}
|
||||
if(data.title && data.title.toString().startsWith("map:")) {
|
||||
// Hide device selection row
|
||||
$("#nodeDeviceSearchRow").hide();
|
||||
$("#maplink").val(data.title.replace("map:",""));
|
||||
}
|
||||
$("#nodelabel").val(data.label);
|
||||
$("#nodestyle").val(data.shape);
|
||||
// Show or hide the image selection if the shape is an image type
|
||||
if(data.shape == "image" || data.shape == "circularImage") {
|
||||
$("#nodeImageRow").show();
|
||||
if(data.image.unselected.indexOf(custom_image_base) == 0) {
|
||||
$("#nodeimage").val(data.image.unselected.replace(custom_image_base, ""));
|
||||
} else {
|
||||
$("#nodeimage").val("");
|
||||
}
|
||||
} else {
|
||||
$("#nodeImageRow").hide();
|
||||
$("#nodeimage").val("");
|
||||
}
|
||||
setNodeImage();
|
||||
// Show or hide the icon selection if the shape is icon
|
||||
if(data.shape == "icon") {
|
||||
$("#nodeicon").val(data.icon.code.charCodeAt(0).toString(16));
|
||||
$("#nodeIconRow").show();
|
||||
} else {
|
||||
$("#nodeIconRow").hide();
|
||||
}
|
||||
$("#nodesize").val(data.size);
|
||||
$("#nodetextface").val(data.font.face);
|
||||
$("#nodetextsize").val(data.font.size);
|
||||
$("#nodetextcolour").val(data.font.color);
|
||||
if(data.color && data.color.background) {
|
||||
$("#nodecolourbg").val(data.color.background);
|
||||
$("#nodecolourbdr").val(data.color.border);
|
||||
} else {
|
||||
// The background colour is blank because a device has been selected - start with defaults
|
||||
$("#nodecolourbg").val(newnodeconf.color.background);
|
||||
$("#nodecolourbdr").val(newnodeconf.color.border);
|
||||
}
|
||||
|
||||
checkColourReset(data.font.color, newnodeconf.font.color, "nodecolourtextreset");
|
||||
checkColourReset(data.color.background, newnodeconf.color.background, "nodecolourbgreset");
|
||||
checkColourReset(data.color.border, newnodeconf.color.border, "nodecolourbdrreset");
|
||||
|
||||
if(data.id) {
|
||||
$("#node-saveButton").on("click", {data: data}, callback);
|
||||
$("#node-saveButton").show();
|
||||
$("#node-saveDefaultsButton").hide();
|
||||
} else {
|
||||
$("#node-saveButton").hide();
|
||||
$("#node-saveDefaultsButton").show();
|
||||
}
|
||||
$('#nodeModal').modal({backdrop: 'static', keyboard: false}, 'show');
|
||||
}
|
||||
|
||||
function editNodeSave(event) {
|
||||
node = event.data.data;
|
||||
|
||||
editNodeHide();
|
||||
|
||||
if($("#device_id").val()) {
|
||||
node.title = $("#device_id").val();
|
||||
} else if($("#maplink").val()) {
|
||||
node.title = "map:" + $("#maplink").val();
|
||||
} else {
|
||||
node.title = '';
|
||||
}
|
||||
// Update the node with the selected values on success and run the callback
|
||||
node.label = $("#nodelabel").val();
|
||||
node.shape = $("#nodestyle").val();
|
||||
node.font.face = $("#nodetextface").val();
|
||||
node.font.size = parseInt($("#nodetextsize").val());
|
||||
node.font.color = $("#nodetextcolour").val();
|
||||
node.color = {highlight: {}, hover: {}};
|
||||
node.color.background = node.color.highlight.background = node.color.hover.background = $("#nodecolourbg").val();
|
||||
node.color.border = node.color.highlight.border = node.color.hover.border = $("#nodecolourbdr").val();
|
||||
node.size = $("#nodesize").val();
|
||||
if(node.shape == "image" || node.shape == "circularImage") {
|
||||
if($("#nodeimage").val()) {
|
||||
node.image = {unselected: custom_image_base + $("#nodeimage").val()};
|
||||
} else {
|
||||
node.image = {unselected: $("#device_image").val()};
|
||||
}
|
||||
} else {
|
||||
node.image = {};
|
||||
}
|
||||
if(node.shape == "icon") {
|
||||
node.icon = {face: 'FontAwesome', code: String.fromCharCode(parseInt($("#nodeicon").val(), 16)), size: $("#nodesize").val(), color: node.color.border};
|
||||
} else {
|
||||
node.icon = {};
|
||||
}
|
||||
if(node.add) {
|
||||
delete node.add;
|
||||
network_nodes.add(node);
|
||||
} else {
|
||||
network_nodes.update(node);
|
||||
}
|
||||
|
||||
if(node.id) {
|
||||
if($("#device_id").val()) {
|
||||
node_device_map[node.id] = {device_id: $("#device_id").val(), device_name: $("#device_name").text(), device_image: $("#device_image").val()}
|
||||
} else {
|
||||
delete node_device_map[node.id];
|
||||
}
|
||||
}
|
||||
|
||||
$("#map-saveDataButton").show();
|
||||
$("#map-renderButton").show();
|
||||
}
|
||||
|
||||
function editNodeCancel(event) {
|
||||
editNodeHide();
|
||||
}
|
||||
|
||||
function editNodeHide() {
|
||||
$("#node-saveButton").off("click");
|
||||
}
|
||||
|
||||
function updateEdgePortSearch(node1_id, node2_id, edge_id) {
|
||||
node1 = network_nodes.get(node1_id);
|
||||
node2 = network_nodes.get(node2_id);
|
||||
|
||||
if(isNaN(node1.title) && isNaN(node2.title)) {
|
||||
// Neither node has a device - clear port config
|
||||
$("#port_id").val("");
|
||||
$("#edgePortRow").hide();
|
||||
$("#edgePortReverseRow").hide();
|
||||
$("#edgePortSearchRow").hide();
|
||||
return;
|
||||
}
|
||||
if(edge_id in edge_port_map) {
|
||||
$("#port_id").val(edge_port_map[edge_id].port_id);
|
||||
$("#port_name").text(edge_port_map[edge_id].port_name);
|
||||
$("#portreverse").bootstrapSwitch('state', edge_port_map[edge_id].reverse);
|
||||
$("#edgePortRow").show();
|
||||
$("#edgePortReverseRow").show();
|
||||
$("#edgePortSearchRow").hide();
|
||||
} else {
|
||||
$("#port_id").val("");
|
||||
$("#portreverse").bootstrapSwitch('state', false);
|
||||
$("#edgePortRow").hide();
|
||||
$("#edgePortReverseRow").hide();
|
||||
$("#edgePortSearchRow").show();
|
||||
}
|
||||
port_search_device_id_1 = (node1.id in node_device_map) ? node_device_map[node1.id].device_id : 0;
|
||||
port_search_device_id_2 = (node2.id in node_device_map) ? node_device_map[node2.id].device_id : 0;
|
||||
}
|
||||
|
||||
function edgePortSelect(e) {
|
||||
var id = e.params.data.id;
|
||||
var name = e.params.data.text;
|
||||
var reverse = e.params.data.device_id != port_search_device_id_1;
|
||||
$("#port_id").val(id);
|
||||
$("#port_name").text(name);
|
||||
$("#portreverse").bootstrapSwitch('state', reverse);
|
||||
|
||||
$("#edgePortSearchRow").hide();
|
||||
$("#edgePortRow").show();
|
||||
$("#edgePortReverseRow").show();
|
||||
}
|
||||
|
||||
function edgePortClear() {
|
||||
$("#portsearch").val('');
|
||||
$("#portsearch").trigger('change');
|
||||
$("#port_id").val("");
|
||||
$("#port_name").text("");
|
||||
$("#edgePortSearchRow").show();
|
||||
$("#edgePortRow").hide();
|
||||
$("#edgePortReverseRow").hide();
|
||||
}
|
||||
|
||||
function editEdgeDefaults() {
|
||||
$("#edgeModalLabel").text('{{ __('map.custom.edit.edge.defaults_title') }}');
|
||||
$("#divEdgeFrom").hide();
|
||||
$("#divEdgeTo").hide();
|
||||
$("#edgePortRow").hide();
|
||||
$("#edgePortReverseRow").hide();
|
||||
$("#edgePortSearchRow").hide();
|
||||
$("#edgeRecenterRow").hide();
|
||||
|
||||
$("#edgestyle").val(newedgeconf.smooth.type);
|
||||
$("#edgetextface").val(newedgeconf.font.face);
|
||||
$("#edgetextsize").val(newedgeconf.font.size);
|
||||
$("#edgetextcolour").val(newedgeconf.font.color);
|
||||
$("#edgetextshow").bootstrapSwitch('state', Boolean(newedgeconf.label));
|
||||
$('#edgecolourtextreset').attr('disabled', 'disabled');
|
||||
|
||||
$("#edge-saveButton").hide();
|
||||
$("#edge-saveDefaultsButton").show();
|
||||
$('#edgeModal').modal({backdrop: 'static', keyboard: false}, 'show');
|
||||
}
|
||||
|
||||
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');
|
||||
$("#map-saveDataButton").show();
|
||||
}
|
||||
|
||||
function editEdge(edgedata, callback) {
|
||||
$("#portsearch").val('');
|
||||
$("#portsearch").trigger('change');
|
||||
var nodes = network_nodes.get({
|
||||
fields: ['id', 'label'],
|
||||
filter: function (item) {
|
||||
// We do not want to be able to link to the mid nodes
|
||||
return (!item.id.endsWith("_mid"));
|
||||
},
|
||||
});
|
||||
$("#edgefrom").find('option').remove().end();
|
||||
$("#edgeto").find('option').remove().end();
|
||||
$.each( nodes, function( node_idx, node ) {
|
||||
$("#edgefrom").append('<option value="' + node.id + '">' + node.label+ '</option>');
|
||||
$("#edgeto").append('<option value="' + node.id + '">' + node.label+ '</option>');
|
||||
});
|
||||
$("#edgefrom").val(edgedata.edge1.from);
|
||||
$("#edgeto").val(edgedata.edge2.from);
|
||||
|
||||
updateEdgePortSearch($("#edgefrom").val(), $("#edgeto").val(), edgedata.id);
|
||||
checkColourReset(edgedata.edge1.font.color, newedgeconf.font.color, "edgecolourtextreset");
|
||||
|
||||
$("#edgestyle").val(edgedata.edge1.smooth.type);
|
||||
$("#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));
|
||||
|
||||
$("#edgeRecenterRow").show();
|
||||
$("#divEdgeFrom").show();
|
||||
$("#divEdgeTo").show();
|
||||
$("#edge-saveButton").show();
|
||||
$("#edge-saveDefaultsButton").hide();
|
||||
$("#edge-saveButton").on("click", {data: edgedata}, callback);
|
||||
|
||||
$('#edgeModal').modal({backdrop: 'static', keyboard: false}, 'show');
|
||||
}
|
||||
|
||||
function editEdgeSave(event) {
|
||||
edgedata = event.data.data;
|
||||
|
||||
editEdgeHide();
|
||||
edgedata.edge1.smooth.type = $("#edgestyle").val();
|
||||
edgedata.edge2.smooth.type = $("#edgestyle").val();
|
||||
edgedata.edge1.from = $("#edgefrom").val();
|
||||
edgedata.edge2.from = $("#edgeto").val();
|
||||
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.title = edgedata.edge2.title = $("#port_id").val();
|
||||
|
||||
if(edgedata.id) {
|
||||
if($("#port_id").val()) {
|
||||
edge_port_map[edgedata.id] = {port_id: $("#port_id").val(), port_name: $("#port_name").text(), reverse: $("#portreverse")[0].checked}
|
||||
} else {
|
||||
delete edge_port_map[edgedata.id];
|
||||
}
|
||||
}
|
||||
|
||||
// Special case for curved lines
|
||||
if(edgedata.edge2.smooth.type == "curvedCW") {
|
||||
edgedata.edge2.smooth.type = "curvedCCW";
|
||||
} else if (edgedata.edge2.smooth.type == "curvedCCW") {
|
||||
edgedata.edge2.smooth.type = "curvedCW";
|
||||
}
|
||||
|
||||
if(edgedata.add) {
|
||||
network_nodes.add([edgedata.mid]);
|
||||
network_nodes.flush();
|
||||
network_edges.add([edgedata.edge1, edgedata.edge2]);
|
||||
network_edges.flush();
|
||||
} else {
|
||||
network_edges.update([edgedata.edge1, edgedata.edge2]);
|
||||
|
||||
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;
|
||||
|
||||
edgedata.mid.x = mid_x;
|
||||
edgedata.mid.y = mid_y;
|
||||
network_nodes.update([edgedata.mid]);
|
||||
$("#map-renderButton").show();
|
||||
}
|
||||
|
||||
// Blank labels need to be selected to update. Select both to ensure this happens
|
||||
if(! edgedata.edge1.label) {
|
||||
network_edges.flush();
|
||||
network.selectEdges([edgedata.edge2.id]);
|
||||
// Redraw to make sure the above change is reflected in the view before we select the next edge
|
||||
network.redraw();
|
||||
// Select the first edge, which will trigger another update
|
||||
network.selectEdges([edgedata.edge1.id]);
|
||||
}
|
||||
}
|
||||
$("#edgerecenter").prop( "checked", false );
|
||||
$("#map-saveDataButton").show();
|
||||
}
|
||||
|
||||
function editEdgeCancel(event) {
|
||||
editEdgeHide();
|
||||
}
|
||||
|
||||
function editEdgeHide() {
|
||||
$("#edge-saveButton").off("click");
|
||||
}
|
||||
|
||||
function editExistingEdge (edge, callback) {
|
||||
if(callback) {
|
||||
callback(null);
|
||||
}
|
||||
var edgeinfo = edge.id.split("_");
|
||||
|
||||
if(edgeinfo[1] == "to") {
|
||||
edge1 = network_edges.get(edgeinfo[0] + "_from");
|
||||
edge2 = network_edges.get(edge.id);
|
||||
} else {
|
||||
edge1 = network_edges.get(edge.id);
|
||||
edge2 = network_edges.get(edgeinfo[0] + "_to");
|
||||
}
|
||||
var mid = network_nodes.get(edgeinfo[0] + "_mid");
|
||||
|
||||
var edgedata = {id: edgeinfo[0], mid: mid, edge1: edge1, edge2: edge2}
|
||||
|
||||
$("#edgeModalLabel").text("Edit Edge");
|
||||
editEdge(edgedata, editEdgeSave);
|
||||
}
|
||||
|
||||
function deleteEdge(edgeid) {
|
||||
network_edges.remove(edgeid + "_to");
|
||||
network_edges.remove(edgeid + "_from");
|
||||
network_edges.flush();
|
||||
network_nodes.remove(edgeid + "_mid");
|
||||
network_nodes.flush();
|
||||
$("#map-saveDataButton").show();
|
||||
}
|
||||
|
||||
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, device_image: node.device_image};
|
||||
node_cfg.title = node.device_id;
|
||||
} else if(node.linked_map_id) {
|
||||
node_cfg.title = "map:" + node.linked_map_id;
|
||||
} else {
|
||||
node_cfg.title = null;
|
||||
}
|
||||
node_cfg.label = 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, border: node.colour_bdr};
|
||||
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};
|
||||
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}};
|
||||
|
||||
// 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) {
|
||||
edge_port_map[edgeid] = {port_id: edge.port_id, port_name: edge.port_name, reverse: edge.reverse};
|
||||
edge1.title = edge2.title = edge.port_id;
|
||||
} else {
|
||||
edge1.title = edge2.title = '';
|
||||
}
|
||||
if(edge.showpct) {
|
||||
edge1.label = edge2.label = 'xx%';
|
||||
} else {
|
||||
edge1.label = edge2.label = '';
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Flush in order to make sure nodes exist for edges to connect to
|
||||
network_nodes.flush();
|
||||
network_edges.flush();
|
||||
$("#alert").empty();
|
||||
$("#alert-row").hide();
|
||||
});
|
||||
|
||||
// Initialise map if it does not exist
|
||||
if (! network) {
|
||||
CreateNetwork();
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
init_select2('#devicesearch', 'device', {limit: 100}, '', '{{ __('map.custom.edit.node.device_select') }}', {dropdownParent: $('#nodeModal')});
|
||||
$("#devicesearch").on("select2:select", nodeDeviceSelect);
|
||||
|
||||
init_select2('#portsearch', 'port', function(params) {
|
||||
return {
|
||||
limit: 100,
|
||||
devices: [port_search_device_id_1, port_search_device_id_2],
|
||||
term: params.term,
|
||||
page: params.page || 1
|
||||
}
|
||||
}, '', '{{ __('map.custom.edit.edge.port_select') }}', {dropdownParent: $('#edgeModal')});
|
||||
$("#portsearch").on("select2:select", edgePortSelect);
|
||||
|
||||
refreshMap();
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
73
resources/views/map/custom-manage.blade.php
Normal file
@ -0,0 +1,73 @@
|
||||
@extends('layouts.librenmsv1')
|
||||
|
||||
@section('title', __('map.custom.title.manage'))
|
||||
|
||||
@section('content')
|
||||
<div class="container-fluid">
|
||||
@include('map.custom-map-modal')
|
||||
@include('map.custom-map-delete-modal')
|
||||
|
||||
<x-panel body-class="!tw-p-0" id="manage-custom-maps">
|
||||
<x-slot name="title">
|
||||
<div class="tw-flex tw-justify-between tw-items-center">
|
||||
<div>
|
||||
<i class="fa fa-map-marked fa-fw fa-lg" aria-hidden="true"></i> {{ __('map.custom.title.manage') }}
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary" onclick="$('#mapModal').modal({backdrop: 'static', keyboard: false}, 'show');">{{ __('map.custom.create_map') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<table class="table table-striped table-condensed" style="margin-bottom: 0; margin-top: -1px">
|
||||
@foreach($maps as $map)
|
||||
<tr id="map{{ $map->custom_map_id }}">
|
||||
<td style="vertical-align: middle">
|
||||
<a href="{{ route('maps.custom.show', $map->custom_map_id) }}">{{ $map->name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a class="btn btn-primary" href="{{ route('maps.custom.edit', $map->custom_map_id) }}"><i class="fa fa-pencil"></i> {{ __('Edit') }}</a>
|
||||
<button class="btn btn-danger"
|
||||
onclick="startMapDelete(this)"
|
||||
data-map-id="{{ $map->custom_map_id }}"
|
||||
><i class="fa fa-trash"></i> {{ __('Delete') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</table>
|
||||
</x-panel>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
@routes
|
||||
<script type="text/javascript">
|
||||
$("#mapBackgroundClearRow").hide();
|
||||
|
||||
function editMapSuccess(data) {
|
||||
$('#mapModal').modal('hide');
|
||||
window.location.href = "{{ @route('maps.custom.edit', ['map' => '?']) }}".replace('?', data['id']);
|
||||
}
|
||||
|
||||
function editMapCancel() {
|
||||
$('#mapModal').modal('hide');
|
||||
}
|
||||
|
||||
var pendingDeleteId;
|
||||
function startMapDelete(target) {
|
||||
pendingDeleteId = $(target).data('map-id');
|
||||
$('#mapDeleteModal').modal({backdrop: 'static', keyboard: false}, 'show');
|
||||
}
|
||||
|
||||
function deleteMap() {
|
||||
$.ajax({
|
||||
url: "{{ route('maps.custom.destroy', ['map' => '?']) }}".replace('?', pendingDeleteId),
|
||||
type: 'DELETE'
|
||||
}).done(() => {
|
||||
$('#map' + pendingDeleteId).remove();
|
||||
pendingDeleteId = null;
|
||||
$('#mapDeleteModal').modal('hide');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endsection
|
15
resources/views/map/custom-map-delete-modal.blade.php
Normal file
@ -0,0 +1,15 @@
|
||||
<div class="modal fade" id="mapDeleteModal" tabindex="-1" role="dialog" aria-labelledby="mapDeleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="mapDeleteModalLabel">{{ __('map.custom.edit.map.delete') }}</h5>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<center>
|
||||
<button type=button value="delete" id="map-deleteConfirmButton" class="btn btn-danger" onclick="deleteMap()">{{ __('Delete') }}</button>
|
||||
<button type=button value="cancel" id="map-deleteCancelButton" class="btn btn-primary" onclick="$('#mapDeleteModal').modal('hide');">{{ __('Cancel') }}</button>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
15
resources/views/map/custom-map-list-modal.blade.php
Normal file
@ -0,0 +1,15 @@
|
||||
<div class="modal fade" id="mapListModal" tabindex="-1" role="dialog" aria-labelledby="mapListModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="mapListModalLabel">{{ __('map.custom.edit.map.unsavedchanges') }}</h5>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<center>
|
||||
<button type=button value="list" id="map-listConfirmButton" class="btn btn-danger" onclick="viewList()">{{ __('Confirm') }}</button>
|
||||
<button type=button value="cancel" id="map-listCancelButton" class="btn btn-primary" onclick="$('#mapListModal').modal('hide');">{{ __('Cancel') }}</button>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
108
resources/views/map/custom-map-modal.blade.php
Normal file
@ -0,0 +1,108 @@
|
||||
<div class="modal fade" id="mapModal" tabindex="-1" role="dialog" aria-labelledby="mapModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="mapModalLabel">{{ __('map.custom.edit.map.settings_title') }}</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="well well-lg">
|
||||
<input type="hidden" id="mapid" name="mapid" />
|
||||
<div class="form-group row">
|
||||
<label for="mapname" class="col-sm-3 control-label">{{ __('map.custom.edit.map.name') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" id="mapname" name="mapname" class="form-control input-sm" value="{{ $name ?? '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="mapwidth" class="col-sm-3 control-label">{{ __('map.custom.edit.map.width') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" id="mapwidth" name="mapwidth" class="form-control input-sm" value="{{ $map_conf['width'] ?? '1800px' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="mapheight" class="col-sm-3 control-label">{{ __('map.custom.edit.map.height') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" id="mapheight" name="mapheight" class="form-control input-sm" value="{{ $map_conf['height'] ?? '800px' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="mapnodealign" class="col-sm-3 control-label">{{ __('map.custom.edit.map.alignment') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="number" id="mapnodealign" name="mapnodealign" class="form-control input-sm" value="{{ $node_align ?? 10 }}">
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-sm-12" id="savemap-alert">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<center>
|
||||
<button type=button value="save" id="map-saveButton" class="btn btn-primary" onclick="saveMapSettings()">{{ __('Save') }}</button>
|
||||
<button type=button value="cancel" id="map-cancelButton" class="btn btn-primary" onclick="editMapCancel()">{{ __('Cancel') }}</button>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function saveMapSettings() {
|
||||
$("#map-saveButton").attr('disabled','disabled');
|
||||
$("#savemap-alert").text('{{ __('map.custom.edit.map.saving') }}');
|
||||
$("#savemap-alert").attr("class", "col-sm-12 alert alert-info");
|
||||
|
||||
var name = $("#mapname").val();
|
||||
var width = $("#mapwidth").val();
|
||||
var height = $("#mapheight").val();
|
||||
var node_align = $("#mapnodealign").val();
|
||||
|
||||
if(!isNaN(width)) {
|
||||
width = width + "px";
|
||||
}
|
||||
if(!isNaN(height)) {
|
||||
height = height + "px";
|
||||
}
|
||||
|
||||
@if(isset($map_id))
|
||||
var url = '{{ route('maps.custom.update', ['map' => $map_id]) }}';
|
||||
var method = 'PUT';
|
||||
@else
|
||||
var url = '{{ route('maps.custom.store') }}';
|
||||
var method = 'POST';
|
||||
@endif
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
name: name,
|
||||
width: width,
|
||||
height: height,
|
||||
node_align: node_align
|
||||
},
|
||||
dataType: 'json',
|
||||
type: method
|
||||
}).done(function (data, status, resp) {
|
||||
editMapSuccess(data);
|
||||
}).fail(function (resp, status, error) {
|
||||
var data = resp.responseJSON;
|
||||
if (data['message']) {
|
||||
let alert_content = $("#savemap-alert");
|
||||
alert_content.text(data['message']);
|
||||
alert_content.attr("class", "col-sm-12 alert alert-danger");
|
||||
} else {
|
||||
let alert_content = $("#savemap-alert");
|
||||
alert_content.text('{{ __('map.custom.edit.map.save_error', ['code' => '?']) }}'.replace('?', resp.status));
|
||||
alert_content.attr("class", "col-sm-12 alert alert-danger");
|
||||
}
|
||||
}).always(function (resp, status, error) {
|
||||
$("#map-saveButton").removeAttr('disabled');
|
||||
});
|
||||
}
|
||||
</script>
|
24
resources/views/map/custom-new.blade.php
Normal file
@ -0,0 +1,24 @@
|
||||
@extends('layouts.librenmsv1')
|
||||
|
||||
@section('title', __('map.custom.title.create'))
|
||||
|
||||
@section('content')
|
||||
@include('map.custom-map-modal')
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script type="text/javascript">
|
||||
// Pop up the modal to set initial settings
|
||||
$('#mapModal').modal({backdrop: 'static', keyboard: false}, 'show');
|
||||
$("#mapBackgroundClearRow").hide();
|
||||
|
||||
function editMapSuccess(data) {
|
||||
window.location.href = "{{ @route('maps.custom.edit', ['map' => '?']) }}".replace('?', data['id']);
|
||||
}
|
||||
|
||||
function editMapCancel() {
|
||||
window.location.href = "{{ route("maps.custom.index") }}";
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
168
resources/views/map/custom-node-modal.blade.php
Normal file
@ -0,0 +1,168 @@
|
||||
<div class="modal fade" id="nodeModal" tabindex="-1" role="dialog" aria-labelledby="nodeModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="nodeModalLabel">{{ __('map.custom.edit.node.new') }}</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="well well-lg">
|
||||
<div class="form-group row single-node" id="nodeDeviceSearchRow">
|
||||
<label for="devicesearch" class="col-sm-3 control-label">{{ __('map.custom.edit.node.device_select') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<select name="devicesearch" id="devicesearch" class="form-control"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row single-node" id="nodeDeviceRow" style="display:none">
|
||||
<label for="deviceclear" class="col-sm-3 control-label">{{ __('Device') }}</label>
|
||||
<div class="col-sm-7">
|
||||
<div id="device_name">
|
||||
</div>
|
||||
<input type="hidden" id="device_id">
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<button type=button class="btn btn-primary" value="save" id="deviceclear" onclick="nodeDeviceClear();">{{ __('Clear') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row single-node" id="nodeDeviceLabelRow">
|
||||
<label for="nodelabel" class="col-sm-3 control-label">{{ __('map.custom.edit.node.label') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type=text id="nodelabel" class="form-control input-sm" value="Node Name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row single-node" id="nodeMapLinkRow">
|
||||
<label for="maplink" class="col-sm-3 control-label">{{ __('map.custom.edit.node.map_link') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<select name="maplink" id="maplink" class="form-control" onchange="nodeMapLinkChange();">
|
||||
<option value="" style="color:#999;">{{ __('map.custom.edit.node.map_select') }}</option>
|
||||
@foreach($maps as $map)
|
||||
<option value="{{$map->custom_map_id}}">{{$map->name}}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="nodestyle" class="col-sm-3 control-label">{{ __('map.custom.edit.node.style') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<select id="nodestyle" class="form-control input-sm" onchange="nodeStyleChange();">
|
||||
<option value="box">{{ __('map.custom.edit.node.style_options.box') }}</option>
|
||||
<option value="circle">{{ __('map.custom.edit.node.style_options.circle') }}</option>
|
||||
<option value="database">{{ __('map.custom.edit.node.style_options.database') }}</option>
|
||||
<option value="ellipse">{{ __('map.custom.edit.node.style_options.ellipse') }}</option>
|
||||
<option value="text">{{ __('map.custom.edit.node.style_options.text') }}</option>
|
||||
<option value="image">{{ __('map.custom.edit.node.style_options.device_image') }}</option>
|
||||
<option value="circularImage">{{ __('map.custom.edit.node.style_options.device_image_circle') }}</option>
|
||||
<option value="diamond">{{ __('map.custom.edit.node.style_options.diamond') }}</option>
|
||||
<option value="dot">{{ __('map.custom.edit.node.style_options.dot') }}</option>
|
||||
<option value="star">{{ __('map.custom.edit.node.style_options.star') }}</option>
|
||||
<option value="triangle">{{ __('map.custom.edit.node.style_options.triangle') }}</option>
|
||||
<option value="triangleDown">{{ __('map.custom.edit.node.style_options.triangle_inverted') }}</option>
|
||||
<option value="hexagon">{{ __('map.custom.edit.node.style_options.hexagon') }}</option>
|
||||
<option value="square">{{ __('map.custom.edit.node.style_options.square') }}</option>
|
||||
<option value="icon">{{ __('map.custom.edit.node.style_options.icon') }}</option>
|
||||
</select>
|
||||
<input type="hidden" id="device_image">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" id="nodeImageRow">
|
||||
<label for="nodeimage" class="col-sm-3 control-label">{{ __('map.custom.edit.node.image') }}</label>
|
||||
<div class="col-sm-6">
|
||||
<select id="nodeimage" class="form-control input-sm" onchange="setNodeImage();">
|
||||
<option value="" id="deviceiconimage">{{ __('map.custom.edit.node.style_options.device_image') }}</option>
|
||||
@foreach($images as $imgfile => $imglabel)
|
||||
<option value="{{$imgfile}}">{{$imglabel}}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<img id="nodeimagepreview" width=28 height=28>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" id="nodeIconRow">
|
||||
<label for="nodeicon" class="col-sm-3 control-label">{{ __('map.custom.edit.node.icon') }}</label>
|
||||
<div class="col-sm-6">
|
||||
<select id="nodeicon" class="form-control input-sm" onchange="setNodeIcon();">
|
||||
<option value="f233">{{ __('map.custom.edit.node.icon_options.server') }}</option>
|
||||
<option value="f390">{{ __('map.custom.edit.node.icon_options.desktop') }}</option>
|
||||
<option value="f7c0">{{ __('map.custom.edit.node.icon_options.dish') }}</option>
|
||||
<option value="f7bf">{{ __('map.custom.edit.node.icon_options.satellite') }}</option>
|
||||
<option value="f1eb">{{ __('map.custom.edit.node.icon_options.wifi') }}</option>
|
||||
<option value="f0c2">{{ __('map.custom.edit.node.icon_options.cloud') }}</option>
|
||||
<option value="f0ac">{{ __('map.custom.edit.node.icon_options.globe') }}</option>
|
||||
<option value="f519">{{ __('map.custom.edit.node.icon_options.tower') }}</option>
|
||||
<option value="f061">{{ __('map.custom.edit.node.icon_options.arrow_right') }}</option>
|
||||
<option value="f060">{{ __('map.custom.edit.node.icon_options.arrow_left') }}</option>
|
||||
<option value="f062">{{ __('map.custom.edit.node.icon_options.arrow_up') }}</option>
|
||||
<option value="f063">{{ __('map.custom.edit.node.icon_options.arrow_down') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<i class="fa" id="nodeiconpreview"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="nodesize" class="col-sm-3 control-label">{{ __('map.custom.edit.node.size') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type=number id="nodesize" class="form-control input-sm" value=50 />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="nodetextface" class="col-sm-3 control-label">{{ __('map.custom.edit.text_font') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type=text id="nodetextface" class="form-control input-sm" value="arial" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="nodetextsize" class="col-sm-3 control-label">{{ __('map.custom.edit.text_size') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type=number id="nodetextsize" class="form-control input-sm" value=14 />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="nodetextcolour" class="col-sm-3 control-label">{{ __('map.custom.edit.text_color') }}</label>
|
||||
<div class="col-sm-2">
|
||||
<input type=color id="nodetextcolour" class="form-control input-sm" value="#343434" onchange="$('#nodecolourtextreset').removeAttr('disabled');" />
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<button type=button class="btn btn-primary" value="reset" id="nodecolourtextreset" onclick="$('#nodetextcolour').val(newnodeconf.font.color); $(this).attr('disabled','disabled');">{{ __('Reset') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" id="nodeColourBgRow">
|
||||
<label for="nodecolourbg" class="col-sm-3 control-label">{{ __('map.custom.edit.node.bg_color') }}</label>
|
||||
<div class="col-sm-2">
|
||||
<input type=color id="nodecolourbg" class="form-control input-sm" value="#343434" onchange="$('#nodecolourbgreset').removeAttr('disabled');" />
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<button type=button class="btn btn-primary" value="reset" id="nodecolourbgreset" onclick="$('#nodecolourbg').val(newnodeconf.color.background); $(this).attr('disabled','disabled');">{{ __('Reset') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" id="nodeColourBdrRow">
|
||||
<label for="nodecolourbdr" class="col-sm-3 control-label">{{ __('map.custom.edit.node.border_color') }}</label>
|
||||
<div class="col-sm-2">
|
||||
<input type=color id="nodecolourbdr" class="form-control input-sm" value="#343434" onchange="$('#nodecolourbdrreset').removeAttr('disabled');" />
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<button type=button class="btn btn-primary" value="reset" id="nodecolourbdrreset" onclick="$('#nodecolourbdr').val(newnodeconf.color.border); $(this).attr('disabled','disabled');">{{ __('Reset') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<center>
|
||||
<button type=button class="btn btn-primary" value="savedefaults" id="node-saveDefaultsButton" data-dismiss="modal" style="display:none" onclick="editNodeDefaultsSave();">{{ __('map.custom.edit.defaults') }}</button>
|
||||
<button type=button class="btn btn-primary" value="save" id="node-saveButton" data-dismiss="modal">{{ __('Save') }}</button>
|
||||
<button type=button class="btn btn-primary" value="cancel" id="node-cancelButton" data-dismiss="modal" onclick="editNodeCancel();">{{ __('Cancel') }}</button>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
232
resources/views/map/custom-view.blade.php
Normal file
@ -0,0 +1,232 @@
|
||||
@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>
|
||||
|
||||
@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 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 = "images/custommap/";
|
||||
|
||||
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 = 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};
|
||||
|
||||
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}};
|
||||
|
||||
// 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) {
|
||||
edge1.title = edge2.title = edge.port_info;
|
||||
if(edge.showpct) {
|
||||
edge1.label = edge.port_frompct + "%";
|
||||
edge2.label = edge.port_topct + "%";
|
||||
}
|
||||
edge1.color = {color: edge.colour_from};
|
||||
edge1.width = edge.width_from;
|
||||
edge2.color = {color: edge.colour_to};
|
||||
edge2.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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
|
@ -4,6 +4,10 @@ use App\Http\Controllers\AboutController;
|
||||
use App\Http\Controllers\AlertController;
|
||||
use App\Http\Controllers\AlertTransportController;
|
||||
use App\Http\Controllers\Auth\SocialiteController;
|
||||
use App\Http\Controllers\Maps\CustomMapBackgroundController;
|
||||
use App\Http\Controllers\Maps\CustomMapController;
|
||||
use App\Http\Controllers\Maps\CustomMapDataController;
|
||||
use App\Http\Controllers\Maps\DeviceDependencyController;
|
||||
use App\Http\Controllers\PushNotificationController;
|
||||
use App\Http\Controllers\ValidateController;
|
||||
use App\Http\Middleware\AuthenticateGraph;
|
||||
@ -77,9 +81,15 @@ Route::middleware(['auth'])->group(function () {
|
||||
->name('device')->where('vars', '.*');
|
||||
|
||||
// Maps
|
||||
Route::prefix('maps')->namespace('Maps')->group(function () {
|
||||
Route::get('devicedependency', 'DeviceDependencyController@dependencyMap');
|
||||
Route::prefix('maps')->group(function () {
|
||||
Route::resource('custom', CustomMapController::class, ['as' => 'maps'])
|
||||
->parameters(['custom' => 'map'])->except('create');
|
||||
Route::get('custom/{map}/background', [CustomMapBackgroundController::class, 'get'])->name('maps.custom.background');
|
||||
Route::post('custom/{map}/background', [CustomMapBackgroundController::class, 'save'])->name('maps.custom.background.save');
|
||||
Route::get('custom/{map}/data', [CustomMapDataController::class, 'get'])->name('maps.custom.data');
|
||||
Route::post('custom/{map}/data', [CustomMapDataController::class, 'save'])->name('maps.custom.data.save');
|
||||
});
|
||||
Route::get('maps/devicedependency', [DeviceDependencyController::class, 'dependencyMap']);
|
||||
|
||||
// dashboard
|
||||
Route::resource('dashboard', 'DashboardController')->except(['create', 'edit']);
|
||||
|