Initial commit
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
3
MANIFEST.in
Normal file
@ -0,0 +1,3 @@
|
||||
include README.md
|
||||
recursive-include netbox_device_map/templates *
|
||||
recursive-include netbox_device_map/static *
|
85
README.md
Normal file
@ -0,0 +1,85 @@
|
||||
# NetBox device map
|
||||
A simple device map plugin with filtering criteria for NetBox
|
||||
|
||||
![Map screenshot](docs/images/screenshot_map.png)
|
||||
![Map filters screenshot](docs/images/screenshot_map_filters.png)
|
||||
|
||||
## Installation
|
||||
1. Download plugin distribution from releases.
|
||||
2. If your NetBox installation uses virtualenv, activate it like this:
|
||||
```
|
||||
source /opt/netbox/venv/bin/activate
|
||||
```
|
||||
3. Install the plugin from the distribution:
|
||||
```
|
||||
sudo pip install /path/to/netbox-plugin-device-map.tar.gz
|
||||
```
|
||||
4. Add plugin to `local_requirements.txt`:
|
||||
|
||||
To ensure plugin is automatically re-installed during future NetBox upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory
|
||||
and list the `nextbox-plugin-device-map` package:
|
||||
```
|
||||
echo "/path/to/netbox-plugin-device-map.tar.gz" | sudo tee -a /opt/netbox/local_requirements.txt
|
||||
```
|
||||
5. Collect static files:
|
||||
```
|
||||
sudo python /opt/netbox/netbox/manage.py collectstatic
|
||||
```
|
||||
6. To enable plugin, add the plugin's name to the PLUGINS list in `configuration.py` (it's usually located in `/opt/netbox/netbox/netbox/`) like so:
|
||||
```
|
||||
PLUGINS = [
|
||||
'netbox_device_map'
|
||||
]
|
||||
```
|
||||
7. Restart NetBox WSGI service to apply changes:
|
||||
```
|
||||
sudo systemctl restart netbox
|
||||
```
|
||||
|
||||
## Configuration
|
||||
You can customize plugin behavior according to your needs. For example, change the custom field that contains device coordinates or install custom map tiles.
|
||||
Update PLUGINS_CONFIG parameter in the `configuration.py` like this:
|
||||
```python
|
||||
PLUGINS_CONFIG = {
|
||||
'netbox_device_map': {
|
||||
'device_geolocation_cf': 'coordinates',
|
||||
'cpe_device_role': 'CPE',
|
||||
'geomap_settings': {
|
||||
'attribution': '<a href="https://osm.yourdomain.net">Your company</a> | © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
'tiles': {
|
||||
'url_template': 'https://osm.yourdomain.net/hot/{z}/{x}/{y}.png',
|
||||
'options': {
|
||||
'maxZoom': 19
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
### Settings
|
||||
| Setting | Default value | Description |
|
||||
|-----------------------|---------------|-------------------------------------------------------------------------------------------------------------|
|
||||
| device_geolocation_cf | `geolocation` | NetBox custom field for storing geographical location of devices (in the `"<latitude>,<longitude>"` format) |
|
||||
| cpe_device_role | `CPE` | Name of the NetBox device role that contains CPE devices |
|
||||
| geomap_settings | … | Geographical map settings |
|
||||
|
||||
#### Geographical map settings
|
||||
| Setting | Default value | Description |
|
||||
|-------------|------------------------------------------------------------------------|-------------------------------------------------------|
|
||||
| attribution | `Data by © <a href="https://openstreetmap.org">OpenStreetMap</a>` | Attribution text in the lower right corner of the map |
|
||||
| crs | `EPSG3857` | Coordinate reference system |
|
||||
| tiles | … | Tiles layer settings |
|
||||
|
||||
Custom tiles layer settings:
|
||||
|
||||
| Setting | Example value | Description |
|
||||
|--------------|-----------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| url_template | `https://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png` | `{s}` means one of the available subdomains (used sequentially to help with browser parallel requests per domain limitation; subdomain values are specified in options; a, b or c by default, can be omitted), `{z}` — zoom level, `{x}` and `{y}` — tile coordinates. `{r}` can be used to add "@2x" to the URL to load retina tiles. |
|
||||
| options | `{'subdomains' : ['a', 'b', 'c'], 'minZoom': 0, 'maxZoom': 18}` | [Leaflet TileLayer](https://leafletjs.com/SlavaUkraini/reference.html#tilelayer) options |
|
||||
|
||||
## Acknowledgements
|
||||
- [Leaflet](https://leafletjs.com/)
|
||||
### Leaflet plugins
|
||||
- [leaflet.fullscreen](https://github.com/brunob/leaflet.fullscreen)
|
||||
- [Leaflet-SVGIcon](https://github.com/iatkin/leaflet-svgicon)
|
||||
- [leaflet-sidebar](https://github.com/Turbo87/leaflet-sidebar)
|
BIN
docs/images/screenshot_map.png
Normal file
After Width: | Height: | Size: 1011 KiB |
BIN
docs/images/screenshot_map_filters.png
Normal file
After Width: | Height: | Size: 79 KiB |
27
netbox_device_map/__init__.py
Normal file
@ -0,0 +1,27 @@
|
||||
from extras.plugins import PluginConfig
|
||||
|
||||
|
||||
class DeviceMapConfig(PluginConfig):
|
||||
name = 'netbox_device_map'
|
||||
verbose_name = 'Device map'
|
||||
version = '0.1'
|
||||
author = 'Victor Golovanenko'
|
||||
author_email = 'drygdryg2014@yandex.com'
|
||||
base_url = 'device-map'
|
||||
default_settings = {
|
||||
'device_geolocation_cf': 'geolocation',
|
||||
'cpe_device_role': 'CPE',
|
||||
'geomap_settings': {
|
||||
'attribution': 'Data by © <a href="https://openstreetmap.org">OpenStreetMap</a>',
|
||||
'crs': 'EPSG3857',
|
||||
'tiles': {
|
||||
'url_template': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
'options': {
|
||||
'subdomains': 'abc',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
config = DeviceMapConfig
|
35
netbox_device_map/forms.py
Normal file
@ -0,0 +1,35 @@
|
||||
from django import forms
|
||||
|
||||
from dcim.models import DeviceRole, Device
|
||||
from ipam.models import VLANGroup, VLAN
|
||||
from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
|
||||
|
||||
class DeviceMapFilterForm(BootstrapMixin, forms.Form):
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
label="VLAN group",
|
||||
help_text="VLAN group for VLAN selection"
|
||||
)
|
||||
vlan = DynamicModelChoiceField(
|
||||
queryset=VLAN.objects.all(),
|
||||
label="VLAN",
|
||||
help_text="Filter devices by VLAN attached to any device interface",
|
||||
query_params={"group_id": "$vlan_group"}
|
||||
)
|
||||
device_roles = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
label="Device roles",
|
||||
help_text="Display devices of only the specified device roles"
|
||||
)
|
||||
calculate_connections = forms.BooleanField(
|
||||
required=False,
|
||||
label="Calculate connections between devices",
|
||||
initial=True
|
||||
)
|
||||
|
||||
|
||||
class ConnectedCpeForm(forms.Form):
|
||||
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False)
|
39
netbox_device_map/geographical_map.py
Normal file
@ -0,0 +1,39 @@
|
||||
from dcim.models import Device
|
||||
|
||||
from .settings import plugin_settings
|
||||
from .helpers import get_connected_devices, LatLon
|
||||
|
||||
|
||||
geomap_settings = plugin_settings['geomap_settings']
|
||||
CPE_DEVICE_ROLE_NAME = plugin_settings['cpe_device_role']
|
||||
|
||||
|
||||
def configure_leaflet_map(map_id: str, devices: dict[Device, LatLon], calculate_connections=True) -> dict:
|
||||
"""Generate Leaflet map of devices and the connections between them.
|
||||
:param map_id: initialize the map on the div with this id
|
||||
:param devices: list of target devices to display on the map
|
||||
:param calculate_connections: calculate connections between devices
|
||||
"""
|
||||
device_id_to_latlon = {device.id: position for device, position in devices.items()}
|
||||
map_config = dict(**geomap_settings, map_id=map_id)
|
||||
markers: list[dict] = []
|
||||
connections: set[frozenset[LatLon, LatLon]] = set()
|
||||
for device, position in devices.items():
|
||||
markers.append(dict(
|
||||
position=position,
|
||||
icon=device.device_role.slug,
|
||||
device=dict(
|
||||
id=device.id,
|
||||
name=device.name,
|
||||
url=device.get_absolute_url(),
|
||||
role=device.device_role.name
|
||||
)
|
||||
))
|
||||
if calculate_connections:
|
||||
for peer_device_id in get_connected_devices(device).values_list('id', flat=True).order_by():
|
||||
if peer_position := device_id_to_latlon.get(peer_device_id):
|
||||
connections.add(frozenset((position, peer_position)))
|
||||
|
||||
map_config.update(markers=markers, connections=[tuple(c) for c in connections])
|
||||
|
||||
return map_config
|
32
netbox_device_map/helpers.py
Normal file
@ -0,0 +1,32 @@
|
||||
from dcim.models import Device
|
||||
from django.db.models import QuerySet, Q
|
||||
from ipam.models import VLAN
|
||||
|
||||
from .settings import plugin_settings
|
||||
|
||||
|
||||
LOCATION_CF_NAME = plugin_settings['device_geolocation_cf']
|
||||
LatLon = tuple[float, float]
|
||||
|
||||
|
||||
def get_device_location(device: Device) -> LatLon | None:
|
||||
"""Extract device geolocation from special custom field"""
|
||||
if location_cf := device.custom_field_data.get(LOCATION_CF_NAME):
|
||||
return tuple(map(float, location_cf.replace(' ', '').split(',', maxsplit=1)))
|
||||
|
||||
|
||||
def get_connected_devices(device: Device, vlan: VLAN = None) -> QuerySet[Device]:
|
||||
"""Get list of connected devices to the specified device.
|
||||
If the vlan is specified, return only devices connected to the interfaces of the specified device
|
||||
containing the specified VLAN"""
|
||||
included_interfaces = device.interfaces.all()
|
||||
if vlan is not None:
|
||||
included_interfaces = included_interfaces.filter(Q(untagged_vlan=vlan) | Q(tagged_vlans=vlan))
|
||||
return Device.objects.filter(interfaces___link_peer_id__in=included_interfaces)
|
||||
|
||||
|
||||
def are_devices_connected(device_a: Device, device_b: Device) -> bool:
|
||||
"""Determines whether devices are connected to each other by a direct connection"""
|
||||
return bool(
|
||||
Device.objects.filter(interfaces___link_peer_id__in=device_a.interfaces.all(), id=device_b.id).values('pk')
|
||||
)
|
9
netbox_device_map/navigation.py
Normal file
@ -0,0 +1,9 @@
|
||||
from extras.plugins import PluginMenuItem
|
||||
|
||||
|
||||
menu_items = (
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_device_map:map',
|
||||
link_text='Device map',
|
||||
),
|
||||
)
|
7
netbox_device_map/settings.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.conf import settings
|
||||
|
||||
from . import config
|
||||
|
||||
# Overlay custom settings over default settings
|
||||
plugin_settings = config.default_settings | settings.PLUGINS_CONFIG[config.name]
|
||||
plugin_settings['geomap_settings'] = config.default_settings['geomap_settings'] | plugin_settings['geomap_settings']
|
119
netbox_device_map/static/netbox_device_map/js/map.js
Normal file
@ -0,0 +1,119 @@
|
||||
let default_marker_icon = {
|
||||
iconSize: [22, 33],
|
||||
shadowEnable: true,
|
||||
shadowOpacity: 0.25,
|
||||
shadowAngle: 27,
|
||||
shadowLength: 0.64,
|
||||
shadowBlur: 1.5
|
||||
}
|
||||
let marker_icon_configs = {
|
||||
'access-switch': Object.assign({color: "#2da652"}, default_marker_icon),
|
||||
'core-switch': Object.assign({color: "#d30b0b"}, default_marker_icon),
|
||||
'distribution-switch': Object.assign({color: "#277fca"}, default_marker_icon),
|
||||
olt: Object.assign({color: "#c5ba26"}, default_marker_icon),
|
||||
router: Object.assign({color: "#26A69A"}, default_marker_icon),
|
||||
wifi: Object.assign({color: "#8111ea"}, default_marker_icon)
|
||||
}
|
||||
|
||||
const map_data = JSON.parse(document.getElementById('map-data').textContent)
|
||||
|
||||
let geomap = L.map(map_data.map_id,
|
||||
{
|
||||
crs: L.CRS[map_data.crs],
|
||||
layers: [L.tileLayer(map_data.tiles.url_template, map_data.tiles.options)],
|
||||
fullscreenControl: true,
|
||||
fullscreenControlOptions: {position: 'topright'}
|
||||
}
|
||||
)
|
||||
geomap.attributionControl.setPrefix(`<a href="https://leafletjs.com" title="A JavaScript library for interactive maps">Leaflet</a>`)
|
||||
geomap.attributionControl.addAttribution(map_data.attribution)
|
||||
|
||||
let sidebar = L.control.sidebar('map-sidebar', {
|
||||
closeButton: true,
|
||||
position: 'left'
|
||||
})
|
||||
geomap.addControl(sidebar);
|
||||
|
||||
let bounds = new L.LatLngBounds()
|
||||
|
||||
// Preparing to place markers with the same coordinates in clusters
|
||||
let markers = {}
|
||||
map_data.markers.forEach(function(entry) {
|
||||
let key = entry.position.toString()
|
||||
if (key in markers) {
|
||||
markers[key].push(entry)
|
||||
} else {
|
||||
markers[key] = [entry]
|
||||
}
|
||||
})
|
||||
|
||||
for (let key in markers) {
|
||||
const marker_parent_layer = markers[key].length > 1 ? L.markerClusterGroup() : geomap;
|
||||
for (let marker_data of markers[key]) {
|
||||
let iconOptions = {}
|
||||
if (marker_data.icon && marker_data.icon in marker_icon_configs) {
|
||||
iconOptions = marker_icon_configs[marker_data.icon]
|
||||
} else {
|
||||
iconOptions = default_marker_icon
|
||||
}
|
||||
let markerObj = L.marker(marker_data.position, {icon: L.divIcon.svgIcon(iconOptions), device: marker_data.device})
|
||||
.bindTooltip(`${marker_data.device.name}<br><span class="text-muted">${marker_data.device.role}</span>`)
|
||||
markerObj.on('click', function (event) {
|
||||
let device = event.target.options.device
|
||||
if (sidebar.isVisible() && (sidebar.displayed_device === device.id)) {
|
||||
sidebar.displayed_device = undefined
|
||||
sidebar.hide()
|
||||
} else {
|
||||
sidebar.displayed_device = device.id
|
||||
document.querySelector('.sidebar-device-name').innerHTML = `<a href="${device.url}" target="_blank">${device.name}</a>`
|
||||
document.querySelector('.sidebar-device-role').innerHTML = device.role
|
||||
sidebar.show()
|
||||
fetch(`connected-cpe/${device.id}?vlan=${map_data.vlan}`)
|
||||
.then(response => response.json()).then(
|
||||
function (response) {
|
||||
if (response.status === true) {
|
||||
document.querySelector('.sidebar-device-type').innerHTML = response.device_type
|
||||
let cpe_list = document.querySelector('.sidebar-cpe-list')
|
||||
cpe_list.innerHTML = ""
|
||||
if (response.cpe_devices?.length) {
|
||||
cpe_list.innerHTML = `<div class="mb-2">Connected CPEs in the selected VLAN:</div>`
|
||||
let ul = document.createElement('ul')
|
||||
ul.setAttribute('class', 'mb-0')
|
||||
cpe_list.appendChild(ul)
|
||||
for (let cpe_device of response.cpe_devices) {
|
||||
let li = document.createElement('li');
|
||||
li.innerHTML = `<a href="${cpe_device.url}" target="_blank">${cpe_device.name}</a>
|
||||
<span class="separator">·</span>
|
||||
<span class="text-muted">${cpe_device.comments}</span>`
|
||||
ul.appendChild(li)
|
||||
}
|
||||
} else {
|
||||
cpe_list.innerHTML = "<i>There are no connected CPEs in the selected VLAN</i>"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
bounds.extend(marker_data.position)
|
||||
marker_parent_layer.addLayer(markerObj)
|
||||
}
|
||||
if (markers[key].length > 1) {
|
||||
geomap.addLayer(marker_parent_layer)
|
||||
}
|
||||
}
|
||||
|
||||
const normalLineStyle = {weight: 3, color: '#3388ff'}
|
||||
const boldLineStyle ={weight: 5, color:'#0c10ff'};
|
||||
|
||||
for (let connection of map_data.connections) {
|
||||
let line = L.polyline(connection, normalLineStyle).addTo(geomap)
|
||||
line.on('mouseover', function () {this.setStyle(boldLineStyle); this.bringToFront()})
|
||||
line.on('mouseout', function () {this.setStyle(normalLineStyle)})
|
||||
}
|
||||
|
||||
if (bounds.isValid()) {
|
||||
geomap.fitBounds(bounds)
|
||||
} else {
|
||||
geomap.fitWorld()
|
||||
}
|
267
netbox_device_map/static/netbox_device_map/js/svg-icon.js
Normal file
@ -0,0 +1,267 @@
|
||||
//Leaflet-SVGIcon
|
||||
//SVG icon for any marker class
|
||||
//Ilya Atkin
|
||||
//ilya.atkin@unh.edu
|
||||
|
||||
L.DivIcon.SVGIcon = L.DivIcon.extend({
|
||||
options: {
|
||||
"className": "svg-icon",
|
||||
"circleAnchor": null, //defaults to [iconSize.x/2, iconSize.x/2]
|
||||
"circleColor": null, //defaults to color
|
||||
"circleFillColor": "rgb(255,255,255)",
|
||||
"circleFillOpacity": null, //default to opacity
|
||||
"circleImageAnchor": null, //defaults to [(iconSize.x - circleImageSize.x)/2, (iconSize.x - circleImageSize.x)/2]
|
||||
"circleImagePath": null, //no default, preference over circleText
|
||||
"circleImageSize": null, //defaults to [iconSize.x/4, iconSize.x/4] if circleImage is supplied
|
||||
"circleOpacity": null, // defaults to opacity
|
||||
"circleRatio": 0.5,
|
||||
"circleText": "",
|
||||
"circleWeight": null, //defaults to weight
|
||||
"color": "rgb(0,102,255)",
|
||||
"fillColor": null, // defaults to color
|
||||
"fillOpacity": 0.4,
|
||||
"fontColor": "rgb(0, 0, 0)",
|
||||
"fontOpacity": "1",
|
||||
"fontSize": null, // defaults to iconSize.x/4
|
||||
"fontWeight": "normal",
|
||||
"iconAnchor": null, //defaults to [iconSize.x/2, iconSize.y] (point tip)
|
||||
"iconSize": L.point(32,48),
|
||||
"opacity": 1,
|
||||
"popupAnchor": null,
|
||||
"shadowAngle": 45,
|
||||
"shadowBlur": 1,
|
||||
"shadowColor": "rgb(0,0,10)",
|
||||
"shadowEnable": false,
|
||||
"shadowLength": .75,
|
||||
"shadowOpacity": 0.5,
|
||||
"shadowTranslate": L.point(0,0),
|
||||
"weight": 2
|
||||
},
|
||||
initialize: function(options) {
|
||||
options = L.Util.setOptions(this, options)
|
||||
|
||||
//iconSize needs to be converted to a Point object if it is not passed as one
|
||||
options.iconSize = L.point(options.iconSize)
|
||||
|
||||
//in addition to setting option dependant defaults, Point-based options are converted to Point objects
|
||||
if (!options.circleAnchor) {
|
||||
options.circleAnchor = L.point(Number(options.iconSize.x)/2, Number(options.iconSize.x)/2)
|
||||
}
|
||||
else {
|
||||
options.circleAnchor = L.point(options.circleAnchor)
|
||||
}
|
||||
if (!options.circleColor) {
|
||||
options.circleColor = options.color
|
||||
}
|
||||
if (!options.circleFillOpacity) {
|
||||
options.circleFillOpacity = options.opacity
|
||||
}
|
||||
if (!options.circleOpacity) {
|
||||
options.circleOpacity = options.opacity
|
||||
}
|
||||
if (!options.circleWeight) {
|
||||
options.circleWeight = options.weight
|
||||
}
|
||||
if (!options.fillColor) {
|
||||
options.fillColor = options.color
|
||||
}
|
||||
if (!options.fontSize) {
|
||||
options.fontSize = Number(options.iconSize.x/4)
|
||||
}
|
||||
if (!options.iconAnchor) {
|
||||
options.iconAnchor = L.point(Number(options.iconSize.x)/2, Number(options.iconSize.y))
|
||||
}
|
||||
else {
|
||||
options.iconAnchor = L.point(options.iconAnchor)
|
||||
}
|
||||
if (!options.popupAnchor) {
|
||||
options.popupAnchor = L.point(0, (-0.75)*(options.iconSize.y))
|
||||
}
|
||||
else {
|
||||
options.popupAnchor = L.point(options.popupAnchor)
|
||||
}
|
||||
if (options.circleImagePath && !options.circleImageSize) {
|
||||
options.circleImageSize = L.point(Number(options.iconSize.x)/4, Number(options.iconSize.x)/4)
|
||||
}
|
||||
else {
|
||||
options.circleImageSize = L.point(options.circleImageSize)
|
||||
}
|
||||
if (options.circleImagePath && !options.circleImageAnchor) {
|
||||
options.circleImageAnchor = L.point(
|
||||
(Number(options.iconSize.x) - Number(options.circleImageSize.x))/2,
|
||||
(Number(options.iconSize.x) - Number(options.circleImageSize.y))/2
|
||||
)
|
||||
}
|
||||
else {
|
||||
options.circleImageAnchor = L.point(options.circleImageAnchor)
|
||||
}
|
||||
|
||||
options.html = this._createSVG()
|
||||
},
|
||||
_createCircle: function() {
|
||||
var cx = Number(this.options.circleAnchor.x)
|
||||
var cy = Number(this.options.circleAnchor.y)
|
||||
var radius = this.options.iconSize.x/2 * Number(this.options.circleRatio)
|
||||
var fill = this.options.circleFillColor
|
||||
var fillOpacity = this.options.circleFillOpacity
|
||||
var stroke = this.options.circleColor
|
||||
var strokeOpacity = this.options.circleOpacity
|
||||
var strokeWidth = this.options.circleWeight
|
||||
var className = this.options.className + "-circle"
|
||||
|
||||
var circle = '<circle class="' + className + '" cx="' + cx + '" cy="' + cy + '" r="' + radius +
|
||||
'" fill="' + fill + '" fill-opacity="'+ fillOpacity +
|
||||
'" stroke="' + stroke + '" stroke-opacity=' + strokeOpacity + '" stroke-width="' + strokeWidth + '"/>'
|
||||
|
||||
return circle
|
||||
},
|
||||
_createCircleImage: function() {
|
||||
var x = this.options.circleImageAnchor.x
|
||||
var y = this.options.circleImageAnchor.y
|
||||
var height = this.options.circleImageSize.y
|
||||
var width = this.options.circleImageSize.x
|
||||
var href = this.options.circleImagePath
|
||||
|
||||
var image = '<image x="' + x + '" y="' + y + '" height="' + height + '" width="' + width + '" href="' + href + '"</image>'
|
||||
|
||||
return image
|
||||
},
|
||||
_createPathDescription: function() {
|
||||
var height = Number(this.options.iconSize.y)
|
||||
var width = Number(this.options.iconSize.x)
|
||||
var weight = Number(this.options.weight)
|
||||
var margin = weight / 2
|
||||
|
||||
var startPoint = "M " + margin + " " + (width/2) + " "
|
||||
var leftLine = "L " + (width/2) + " " + (height - weight) + " "
|
||||
var rightLine = "L " + (width - margin) + " " + (width/2) + " "
|
||||
var arc = "A " + (width/4) + " " + (width/4) + " 0 0 0 " + margin + " " + (width/2) + " Z"
|
||||
|
||||
var d = startPoint + leftLine + rightLine + arc
|
||||
|
||||
return d
|
||||
},
|
||||
_createPath: function() {
|
||||
var pathDescription = this._createPathDescription()
|
||||
var strokeWidth = this.options.weight
|
||||
var stroke = this.options.color
|
||||
var strokeOpacity = this.options.opacity
|
||||
var fill = this.options.fillColor
|
||||
var fillOpacity = this.options.fillOpacity
|
||||
var className = this.options.className + "-path"
|
||||
|
||||
var path = '<path class="' + className + '" d="' + pathDescription +
|
||||
'" stroke-width="' + strokeWidth + '" stroke="' + stroke + '" stroke-opacity="' + strokeOpacity +
|
||||
'" fill="' + fill + '" fill-opacity="' + fillOpacity + '"/>'
|
||||
|
||||
return path
|
||||
},
|
||||
_createShadow: function() {
|
||||
var pathDescription = this._createPathDescription()
|
||||
var strokeWidth = this.options.weight
|
||||
var stroke = this.options.shadowColor
|
||||
var fill = this.options.shadowColor
|
||||
var className = this.options.className + "-shadow"
|
||||
|
||||
var origin = (this.options.iconSize.x / 2) + "px " + (this.options.iconSize.y) + "px"
|
||||
var rotation = this.options.shadowAngle
|
||||
var height = this.options.shadowLength
|
||||
var opacity = this.options.shadowOpacity
|
||||
var blur = this.options.shadowBlur
|
||||
var translate = this.options.shadowTranslate.x + "px, " + this.options.shadowTranslate.y + "px"
|
||||
|
||||
var blurFilter = "<filter id='iconShadowBlur'><feGaussianBlur in='SourceGraphic' stdDeviation='" + blur + "'/></filter>"
|
||||
|
||||
var shadow = '<path filter="url(#iconShadowBlur") class="' + className + '" d="' + pathDescription +
|
||||
'" fill="' + fill + '" stroke-width="' + strokeWidth + '" stroke="' + stroke +
|
||||
'" style="opacity: ' + opacity + '; ' + 'transform-origin: ' + origin +'; transform: rotate(' + rotation + 'deg) translate(' + translate + ') scale(1, '+ height +')' +
|
||||
'"/>'
|
||||
|
||||
return blurFilter+shadow
|
||||
},
|
||||
_createSVG: function() {
|
||||
var path = this._createPath()
|
||||
var circle = this._createCircle()
|
||||
var shadow = this.options.shadowEnable ? this._createShadow() : ""
|
||||
var innerCircle = this.options.circleImagePath ? this._createCircleImage() : this._createText()
|
||||
var className = this.options.className + "-svg"
|
||||
var width = this.options.iconSize.x
|
||||
var height = this.options.iconSize.y
|
||||
|
||||
if (this.options.shadowEnable) {
|
||||
width += this.options.iconSize.y * this.options.shadowLength - (this.options.iconSize.x / 2)
|
||||
width = Math.max(width, 32)
|
||||
height += this.options.iconSize.y * this.options.shadowLength
|
||||
}
|
||||
|
||||
var style = "width:" + width + "px; height:" + height
|
||||
|
||||
var svg = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="' + className + '" style="' + style + '">' + shadow + path + circle + innerCircle + '</svg>'
|
||||
|
||||
return svg
|
||||
},
|
||||
_createText: function() {
|
||||
var fontSize = this.options.fontSize + "px"
|
||||
var fontWeight = this.options.fontWeight
|
||||
var lineHeight = Number(this.options.fontSize)
|
||||
|
||||
var x = this.options.circleAnchor.x
|
||||
var y = this.options.circleAnchor.y + (lineHeight * 0.35) //35% was found experimentally
|
||||
var circleText = this.options.circleText
|
||||
var textColor = this.options.fontColor.replace("rgb(", "rgba(").replace(")", "," + this.options.fontOpacity + ")")
|
||||
|
||||
var text = '<text text-anchor="middle" x="' + x + '" y="' + y + '" style="font-size: ' + fontSize + '; font-weight: ' + fontWeight +'" fill="' + textColor + '">' + circleText + '</text>'
|
||||
|
||||
return text
|
||||
}
|
||||
})
|
||||
|
||||
L.divIcon.svgIcon = function(options) {
|
||||
return new L.DivIcon.SVGIcon(options)
|
||||
}
|
||||
|
||||
L.Marker.SVGMarker = L.Marker.extend({
|
||||
options: {
|
||||
"iconFactory": L.divIcon.svgIcon,
|
||||
"iconOptions": {}
|
||||
},
|
||||
initialize: function(latlng, options) {
|
||||
options = L.Util.setOptions(this, options)
|
||||
options.icon = options.iconFactory(options.iconOptions)
|
||||
this._latlng = latlng
|
||||
},
|
||||
onAdd: function(map) {
|
||||
L.Marker.prototype.onAdd.call(this, map)
|
||||
},
|
||||
setStyle: function(style) {
|
||||
if (this._icon) {
|
||||
var svg = this._icon.children[0]
|
||||
var iconBody = this._icon.children[0].children[0]
|
||||
var iconCircle = this._icon.children[0].children[1]
|
||||
|
||||
if (style.color && !style.iconOptions) {
|
||||
var stroke = style.color.replace("rgb","rgba").replace(")", ","+this.options.icon.options.opacity+")")
|
||||
var fill = style.color.replace("rgb","rgba").replace(")", ","+this.options.icon.options.fillOpacity+")")
|
||||
iconBody.setAttribute("stroke", stroke)
|
||||
iconBody.setAttribute("fill", fill)
|
||||
iconCircle.setAttribute("stroke", stroke)
|
||||
|
||||
this.options.icon.fillColor = fill
|
||||
this.options.icon.color = stroke
|
||||
this.options.icon.circleColor = stroke
|
||||
}
|
||||
if (style.opacity) {
|
||||
this.setOpacity(style.opacity)
|
||||
}
|
||||
if (style.iconOptions) {
|
||||
if (style.color) { style.iconOptions.color = style.color }
|
||||
var iconOptions = L.Util.setOptions(this.options.icon, style.iconOptions)
|
||||
this.setIcon(L.divIcon.svgIcon(iconOptions))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
L.marker.svgMarker = function(latlng, options) {
|
||||
return new L.Marker.SVGMarker(latlng, options)
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
.leaflet-sidebar {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
z-index: 2000; }
|
||||
.leaflet-sidebar.left {
|
||||
left: -500px;
|
||||
transition: left 0.5s, width 0.5s;
|
||||
padding-right: 0; }
|
||||
.leaflet-sidebar.left.visible {
|
||||
left: 0; }
|
||||
.leaflet-sidebar.right {
|
||||
right: -500px;
|
||||
transition: right 0.5s, width 0.5s;
|
||||
padding-left: 0; }
|
||||
.leaflet-sidebar.right.visible {
|
||||
right: 0; }
|
||||
.leaflet-sidebar > .leaflet-control {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 24px;
|
||||
font-size: 1.1em;
|
||||
background: white;
|
||||
box-shadow: 0 1px 7px rgba(0, 0, 0, 0.65);
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px; }
|
||||
.leaflet-touch .leaflet-sidebar > .leaflet-control {
|
||||
box-shadow: none;
|
||||
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||
background-clip: padding-box; }
|
||||
@media (max-width: 767px) {
|
||||
.leaflet-sidebar {
|
||||
width: 100%;
|
||||
padding: 0; }
|
||||
.leaflet-sidebar.left.visible ~ .leaflet-left {
|
||||
left: 100%; }
|
||||
.leaflet-sidebar.right.visible ~ .leaflet-right {
|
||||
right: 100%; }
|
||||
.leaflet-sidebar.left {
|
||||
left: -100%; }
|
||||
.leaflet-sidebar.left.visible {
|
||||
left: 0; }
|
||||
.leaflet-sidebar.right {
|
||||
right: -100%; }
|
||||
.leaflet-sidebar.right.visible {
|
||||
right: 0; }
|
||||
.leaflet-sidebar > .leaflet-control {
|
||||
box-shadow: none;
|
||||
-webkit-border-radius: 0;
|
||||
border-radius: 0; }
|
||||
.leaflet-touch .leaflet-sidebar > .leaflet-control {
|
||||
border: 0; } }
|
||||
@media (min-width: 768px) and (max-width: 991px) {
|
||||
.leaflet-sidebar {
|
||||
width: 305px; }
|
||||
.leaflet-sidebar.left.visible ~ .leaflet-left {
|
||||
left: 305px; }
|
||||
.leaflet-sidebar.right.visible ~ .leaflet-right {
|
||||
right: 305px; } }
|
||||
@media (min-width: 992px) and (max-width: 1199px) {
|
||||
.leaflet-sidebar {
|
||||
width: 390px; }
|
||||
.leaflet-sidebar.left.visible ~ .leaflet-left {
|
||||
left: 390px; }
|
||||
.leaflet-sidebar.right.visible ~ .leaflet-right {
|
||||
right: 390px; } }
|
||||
@media (min-width: 1200px) {
|
||||
.leaflet-sidebar {
|
||||
width: 460px; }
|
||||
.leaflet-sidebar.left.visible ~ .leaflet-left {
|
||||
left: 460px; }
|
||||
.leaflet-sidebar.right.visible ~ .leaflet-right {
|
||||
right: 460px; } }
|
||||
.leaflet-sidebar .close {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
color: #333;
|
||||
font-size: 25px;
|
||||
line-height: 1em;
|
||||
text-align: center;
|
||||
background: white;
|
||||
-webkit-border-radius: 16px;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
z-index: 1000; }
|
||||
|
||||
.leaflet-left {
|
||||
transition: left 0.5s; }
|
||||
|
||||
.leaflet-right {
|
||||
transition: right 0.5s; }
|
@ -0,0 +1,202 @@
|
||||
L.Control.Sidebar = L.Control.extend({
|
||||
|
||||
includes: L.Evented.prototype || L.Mixin.Events,
|
||||
|
||||
options: {
|
||||
closeButton: true,
|
||||
position: 'left',
|
||||
autoPan: true,
|
||||
},
|
||||
|
||||
initialize: function (placeholder, options) {
|
||||
L.setOptions(this, options);
|
||||
|
||||
// Find content container
|
||||
var content = this._contentContainer = L.DomUtil.get(placeholder);
|
||||
|
||||
// Remove the content container from its original parent
|
||||
if(content.parentNode != undefined){
|
||||
content.parentNode.removeChild(content);
|
||||
}
|
||||
var l = 'leaflet-';
|
||||
|
||||
// Create sidebar container
|
||||
var container = this._container =
|
||||
L.DomUtil.create('div', l + 'sidebar ' + this.options.position);
|
||||
|
||||
// Style and attach content container
|
||||
L.DomUtil.addClass(content, l + 'control');
|
||||
container.appendChild(content);
|
||||
|
||||
// Create close button and attach it if configured
|
||||
if (this.options.closeButton) {
|
||||
var close = this._closeButton =
|
||||
L.DomUtil.create('a', 'close', container);
|
||||
close.innerHTML = '×';
|
||||
}
|
||||
},
|
||||
|
||||
addTo: function (map) {
|
||||
var container = this._container;
|
||||
var content = this._contentContainer;
|
||||
|
||||
// Attach event to close button
|
||||
if (this.options.closeButton) {
|
||||
var close = this._closeButton;
|
||||
|
||||
L.DomEvent.on(close, 'click', this.hide, this);
|
||||
}
|
||||
|
||||
L.DomEvent
|
||||
.on(container, 'transitionend',
|
||||
this._handleTransitionEvent, this)
|
||||
.on(container, 'webkitTransitionEnd',
|
||||
this._handleTransitionEvent, this);
|
||||
|
||||
// Attach sidebar container to controls container
|
||||
var controlContainer = map._controlContainer;
|
||||
controlContainer.insertBefore(container, controlContainer.firstChild);
|
||||
|
||||
this._map = map;
|
||||
|
||||
// Make sure we don't drag the map when we interact with the content
|
||||
var stop = L.DomEvent.stopPropagation;
|
||||
var fakeStop = L.DomEvent._fakeStop || stop;
|
||||
L.DomEvent
|
||||
.on(content, 'contextmenu', stop)
|
||||
.on(content, 'click', fakeStop)
|
||||
.on(content, 'mousedown', stop)
|
||||
.on(content, 'touchstart', stop)
|
||||
.on(content, 'dblclick', fakeStop)
|
||||
.on(content, 'mousewheel', stop)
|
||||
.on(content, 'wheel', stop)
|
||||
.on(content, 'scroll', stop)
|
||||
.on(content, 'MozMousePixelScroll', stop);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
removeFrom: function (map) {
|
||||
//if the control is visible, hide it before removing it.
|
||||
this.hide();
|
||||
|
||||
var container = this._container;
|
||||
var content = this._contentContainer;
|
||||
|
||||
// Remove sidebar container from controls container
|
||||
var controlContainer = map._controlContainer;
|
||||
controlContainer.removeChild(container);
|
||||
|
||||
//disassociate the map object
|
||||
this._map = null;
|
||||
|
||||
// Unregister events to prevent memory leak
|
||||
var stop = L.DomEvent.stopPropagation;
|
||||
var fakeStop = L.DomEvent._fakeStop || stop;
|
||||
L.DomEvent
|
||||
.off(content, 'contextmenu', stop)
|
||||
.off(content, 'click', fakeStop)
|
||||
.off(content, 'mousedown', stop)
|
||||
.off(content, 'touchstart', stop)
|
||||
.off(content, 'dblclick', fakeStop)
|
||||
.off(content, 'mousewheel', stop)
|
||||
.off(content, 'wheel', stop)
|
||||
.off(content, 'scroll', stop)
|
||||
.off(content, 'MozMousePixelScroll', stop);
|
||||
|
||||
L.DomEvent
|
||||
.off(container, 'transitionend',
|
||||
this._handleTransitionEvent, this)
|
||||
.off(container, 'webkitTransitionEnd',
|
||||
this._handleTransitionEvent, this);
|
||||
|
||||
if (this._closeButton && this._close) {
|
||||
var close = this._closeButton;
|
||||
|
||||
L.DomEvent.off(close, 'click', this.hide, this);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
isVisible: function () {
|
||||
return L.DomUtil.hasClass(this._container, 'visible');
|
||||
},
|
||||
|
||||
show: function () {
|
||||
if (!this.isVisible()) {
|
||||
L.DomUtil.addClass(this._container, 'visible');
|
||||
if (this.options.autoPan) {
|
||||
this._map.panBy([-this.getOffset() / 2, 0], {
|
||||
duration: 0.5
|
||||
});
|
||||
}
|
||||
this.fire('show');
|
||||
}
|
||||
},
|
||||
|
||||
hide: function (e) {
|
||||
if (this.isVisible()) {
|
||||
L.DomUtil.removeClass(this._container, 'visible');
|
||||
if (this.options.autoPan) {
|
||||
this._map.panBy([this.getOffset() / 2, 0], {
|
||||
duration: 0.5
|
||||
});
|
||||
}
|
||||
this.fire('hide');
|
||||
}
|
||||
if(e) {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
}
|
||||
},
|
||||
|
||||
toggle: function () {
|
||||
if (this.isVisible()) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
},
|
||||
|
||||
getContainer: function () {
|
||||
return this._contentContainer;
|
||||
},
|
||||
|
||||
getCloseButton: function () {
|
||||
return this._closeButton;
|
||||
},
|
||||
|
||||
setContent: function (content) {
|
||||
var container = this.getContainer();
|
||||
|
||||
if (typeof content === 'string') {
|
||||
container.innerHTML = content;
|
||||
} else {
|
||||
// clean current content
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
|
||||
container.appendChild(content);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getOffset: function () {
|
||||
if (this.options.position === 'right') {
|
||||
return -this._container.offsetWidth;
|
||||
} else {
|
||||
return this._container.offsetWidth;
|
||||
}
|
||||
},
|
||||
|
||||
_handleTransitionEvent: function (e) {
|
||||
if (e.propertyName == 'left' || e.propertyName == 'right')
|
||||
this.fire(this.isVisible() ? 'shown' : 'hidden');
|
||||
}
|
||||
});
|
||||
|
||||
L.control.sidebar = function (placeholder, options) {
|
||||
return new L.Control.Sidebar(placeholder, options);
|
||||
};
|
10
netbox_device_map/static/netbox_device_map/leaflet.fullscreen/Control.FullScreen.css
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
.fullscreen-icon { background-image: url(icon-fullscreen.svg); background-size:26px 52px; }
|
||||
.fullscreen-icon.leaflet-fullscreen-on { background-position:0 -26px; }
|
||||
.leaflet-touch .fullscreen-icon { background-position: 2px 2px; }
|
||||
.leaflet-touch .fullscreen-icon.leaflet-fullscreen-on { background-position: 2px -24px; }
|
||||
/* one selector per rule as explained here : http://www.sitepoint.com/html5-full-screen-api/ */
|
||||
.leaflet-container:-webkit-full-screen { width: 100% !important; height: 100% !important; z-index: 99999; }
|
||||
.leaflet-container:-ms-fullscreen { width: 100% !important; height: 100% !important; z-index: 99999; }
|
||||
.leaflet-container:full-screen { width: 100% !important; height: 100% !important; z-index: 99999; }
|
||||
.leaflet-container:fullscreen { width: 100% !important; height: 100% !important; z-index: 99999; }
|
||||
.leaflet-pseudo-fullscreen { position: fixed !important; width: 100% !important; height: 100% !important; top: 0px !important; left: 0px !important; z-index: 99999; }
|
341
netbox_device_map/static/netbox_device_map/leaflet.fullscreen/Control.FullScreen.js
vendored
Normal file
@ -0,0 +1,341 @@
|
||||
/*!
|
||||
* Based on package 'screenfull'
|
||||
* v5.2.0 - 2021-11-03
|
||||
* (c) Sindre Sorhus; MIT License
|
||||
* Added definition for using screenfull as an amd module
|
||||
* Must be placed before the definition of leaflet.fullscreen
|
||||
* as it is required by that
|
||||
*/
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('screenfull', factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
module.exports.screenfull = factory();
|
||||
} else {
|
||||
// Save 'screenfull' into global window variable
|
||||
root.screenfull = factory();
|
||||
}
|
||||
}(typeof self !== 'undefined' ? self : this, function () {
|
||||
'use strict';
|
||||
|
||||
var document = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {};
|
||||
|
||||
var fn = (function () {
|
||||
var val;
|
||||
|
||||
var fnMap = [
|
||||
[
|
||||
'requestFullscreen',
|
||||
'exitFullscreen',
|
||||
'fullscreenElement',
|
||||
'fullscreenEnabled',
|
||||
'fullscreenchange',
|
||||
'fullscreenerror'
|
||||
],
|
||||
// New WebKit
|
||||
[
|
||||
'webkitRequestFullscreen',
|
||||
'webkitExitFullscreen',
|
||||
'webkitFullscreenElement',
|
||||
'webkitFullscreenEnabled',
|
||||
'webkitfullscreenchange',
|
||||
'webkitfullscreenerror'
|
||||
|
||||
],
|
||||
// Old WebKit
|
||||
[
|
||||
'webkitRequestFullScreen',
|
||||
'webkitCancelFullScreen',
|
||||
'webkitCurrentFullScreenElement',
|
||||
'webkitCancelFullScreen',
|
||||
'webkitfullscreenchange',
|
||||
'webkitfullscreenerror'
|
||||
|
||||
],
|
||||
[
|
||||
'mozRequestFullScreen',
|
||||
'mozCancelFullScreen',
|
||||
'mozFullScreenElement',
|
||||
'mozFullScreenEnabled',
|
||||
'mozfullscreenchange',
|
||||
'mozfullscreenerror'
|
||||
],
|
||||
[
|
||||
'msRequestFullscreen',
|
||||
'msExitFullscreen',
|
||||
'msFullscreenElement',
|
||||
'msFullscreenEnabled',
|
||||
'MSFullscreenChange',
|
||||
'MSFullscreenError'
|
||||
]
|
||||
];
|
||||
|
||||
var i = 0;
|
||||
var l = fnMap.length;
|
||||
var ret = {};
|
||||
|
||||
for (; i < l; i++) {
|
||||
val = fnMap[i];
|
||||
if (val && val[1] in document) {
|
||||
for (i = 0; i < val.length; i++) {
|
||||
ret[fnMap[0][i]] = val[i];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})();
|
||||
|
||||
var eventNameMap = {
|
||||
change: fn.fullscreenchange,
|
||||
error: fn.fullscreenerror
|
||||
};
|
||||
|
||||
var screenfull = {
|
||||
request: function (element, options) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var onFullScreenEntered = function () {
|
||||
this.off('change', onFullScreenEntered);
|
||||
resolve();
|
||||
}.bind(this);
|
||||
|
||||
this.on('change', onFullScreenEntered);
|
||||
|
||||
element = element || document.documentElement;
|
||||
|
||||
var returnPromise = element[fn.requestFullscreen](options);
|
||||
|
||||
if (returnPromise instanceof Promise) {
|
||||
returnPromise.then(onFullScreenEntered).catch(reject);
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
exit: function () {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (!this.isFullscreen) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
var onFullScreenExit = function () {
|
||||
this.off('change', onFullScreenExit);
|
||||
resolve();
|
||||
}.bind(this);
|
||||
|
||||
this.on('change', onFullScreenExit);
|
||||
|
||||
var returnPromise = document[fn.exitFullscreen]();
|
||||
|
||||
if (returnPromise instanceof Promise) {
|
||||
returnPromise.then(onFullScreenExit).catch(reject);
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
toggle: function (element, options) {
|
||||
return this.isFullscreen ? this.exit() : this.request(element, options);
|
||||
},
|
||||
onchange: function (callback) {
|
||||
this.on('change', callback);
|
||||
},
|
||||
onerror: function (callback) {
|
||||
this.on('error', callback);
|
||||
},
|
||||
on: function (event, callback) {
|
||||
var eventName = eventNameMap[event];
|
||||
if (eventName) {
|
||||
document.addEventListener(eventName, callback, false);
|
||||
}
|
||||
},
|
||||
off: function (event, callback) {
|
||||
var eventName = eventNameMap[event];
|
||||
if (eventName) {
|
||||
document.removeEventListener(eventName, callback, false);
|
||||
}
|
||||
},
|
||||
raw: fn
|
||||
};
|
||||
|
||||
if (!fn) {
|
||||
return {isEnabled: false};
|
||||
} else {
|
||||
Object.defineProperties(screenfull, {
|
||||
isFullscreen: {
|
||||
get: function () {
|
||||
return Boolean(document[fn.fullscreenElement]);
|
||||
}
|
||||
},
|
||||
element: {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return document[fn.fullscreenElement];
|
||||
}
|
||||
},
|
||||
isEnabled: {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
// Coerce to boolean in case of old WebKit
|
||||
return Boolean(document[fn.fullscreenEnabled]);
|
||||
}
|
||||
}
|
||||
});
|
||||
return screenfull;
|
||||
}
|
||||
}));
|
||||
|
||||
/*!
|
||||
* leaflet.fullscreen
|
||||
*/
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// define an AMD module that requires 'leaflet' and 'screenfull'
|
||||
// and resolve to an object containing leaflet and screenfull
|
||||
define('leafletFullScreen', ['leaflet', 'screenfull'], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// define a CommonJS module that requires 'leaflet' and 'screenfull'
|
||||
module.exports = factory(require('leaflet'), require('screenfull'));
|
||||
} else {
|
||||
// Assume 'leaflet' and 'screenfull' are loaded into global variable already
|
||||
factory(root.L, root.screenfull);
|
||||
}
|
||||
}(typeof self !== 'undefined' ? self : this, function (leaflet, screenfull) {
|
||||
'use strict';
|
||||
|
||||
leaflet.Control.FullScreen = leaflet.Control.extend({
|
||||
options: {
|
||||
position: 'topleft',
|
||||
title: 'Full Screen',
|
||||
titleCancel: 'Exit Full Screen',
|
||||
forceSeparateButton: false,
|
||||
forcePseudoFullscreen: false,
|
||||
fullscreenElement: false
|
||||
},
|
||||
|
||||
_screenfull: screenfull,
|
||||
|
||||
onAdd: function (map) {
|
||||
var className = 'leaflet-control-zoom-fullscreen', container, content = '';
|
||||
|
||||
if (map.zoomControl && !this.options.forceSeparateButton) {
|
||||
container = map.zoomControl._container;
|
||||
} else {
|
||||
container = leaflet.DomUtil.create('div', 'leaflet-bar');
|
||||
}
|
||||
|
||||
if (this.options.content) {
|
||||
content = this.options.content;
|
||||
} else {
|
||||
className += ' fullscreen-icon';
|
||||
}
|
||||
|
||||
this._createButton(this.options.title, className, content, container, this.toggleFullScreen, this);
|
||||
this._map.fullscreenControl = this;
|
||||
|
||||
this._map.on('enterFullscreen exitFullscreen', this._toggleState, this);
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
onRemove: function () {
|
||||
leaflet.DomEvent
|
||||
.off(this.link, 'click', leaflet.DomEvent.stop)
|
||||
.off(this.link, 'click', this.toggleFullScreen, this);
|
||||
|
||||
leaflet.DomEvent
|
||||
.off(this._container, this._screenfull.raw.fullscreenchange, leaflet.DomEvent.stop)
|
||||
.off(this._container, this._screenfull.raw.fullscreenchange, this._handleFullscreenChange, this);
|
||||
|
||||
leaflet.DomEvent
|
||||
.off(document, this._screenfull.raw.fullscreenchange, leaflet.DomEvent.stop)
|
||||
.off(document, this._screenfull.raw.fullscreenchange, this._handleFullscreenChange, this);
|
||||
},
|
||||
|
||||
_createButton: function (title, className, content, container, fn, context) {
|
||||
this.link = leaflet.DomUtil.create('a', className, container);
|
||||
this.link.href = '#';
|
||||
this.link.title = title;
|
||||
this.link.innerHTML = content;
|
||||
|
||||
this.link.setAttribute('role', 'button');
|
||||
this.link.setAttribute('aria-label', title);
|
||||
|
||||
L.DomEvent.disableClickPropagation(container);
|
||||
|
||||
leaflet.DomEvent
|
||||
.on(this.link, 'click', leaflet.DomEvent.stop)
|
||||
.on(this.link, 'click', fn, context);
|
||||
|
||||
leaflet.DomEvent
|
||||
.on(container, this._screenfull.raw.fullscreenchange, leaflet.DomEvent.stop)
|
||||
.on(container, this._screenfull.raw.fullscreenchange, this._handleFullscreenChange, context);
|
||||
|
||||
leaflet.DomEvent
|
||||
.on(document, this._screenfull.raw.fullscreenchange, leaflet.DomEvent.stop)
|
||||
.on(document, this._screenfull.raw.fullscreenchange, this._handleFullscreenChange, context);
|
||||
|
||||
return this.link;
|
||||
},
|
||||
|
||||
toggleFullScreen: function () {
|
||||
var map = this._map;
|
||||
map._exitFired = false;
|
||||
if (map._isFullscreen) {
|
||||
if (this._screenfull.isEnabled && !this.options.forcePseudoFullscreen) {
|
||||
this._screenfull.exit();
|
||||
} else {
|
||||
leaflet.DomUtil.removeClass(this.options.fullscreenElement ? this.options.fullscreenElement : map._container, 'leaflet-pseudo-fullscreen');
|
||||
map.invalidateSize();
|
||||
}
|
||||
map.fire('exitFullscreen');
|
||||
map._exitFired = true;
|
||||
map._isFullscreen = false;
|
||||
}
|
||||
else {
|
||||
if (this._screenfull.isEnabled && !this.options.forcePseudoFullscreen) {
|
||||
this._screenfull.request(this.options.fullscreenElement ? this.options.fullscreenElement : map._container);
|
||||
} else {
|
||||
leaflet.DomUtil.addClass(this.options.fullscreenElement ? this.options.fullscreenElement : map._container, 'leaflet-pseudo-fullscreen');
|
||||
map.invalidateSize();
|
||||
}
|
||||
map.fire('enterFullscreen');
|
||||
map._isFullscreen = true;
|
||||
}
|
||||
},
|
||||
|
||||
_toggleState: function () {
|
||||
this.link.title = this._map._isFullscreen ? this.options.title : this.options.titleCancel;
|
||||
this._map._isFullscreen ? L.DomUtil.removeClass(this.link, 'leaflet-fullscreen-on') : L.DomUtil.addClass(this.link, 'leaflet-fullscreen-on');
|
||||
},
|
||||
|
||||
_handleFullscreenChange: function () {
|
||||
var map = this._map;
|
||||
map.invalidateSize();
|
||||
if (!this._screenfull.isFullscreen && !map._exitFired) {
|
||||
map.fire('exitFullscreen');
|
||||
map._exitFired = true;
|
||||
map._isFullscreen = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
leaflet.Map.include({
|
||||
toggleFullscreen: function () {
|
||||
this.fullscreenControl.toggleFullScreen();
|
||||
}
|
||||
});
|
||||
|
||||
leaflet.Map.addInitHook(function () {
|
||||
if (this.options.fullscreenControl) {
|
||||
this.addControl(leaflet.control.fullscreen(this.options.fullscreenControlOptions));
|
||||
}
|
||||
});
|
||||
|
||||
leaflet.control.fullscreen = function (options) {
|
||||
return new leaflet.Control.FullScreen(options);
|
||||
};
|
||||
|
||||
// must return an object containing also screenfull to make screenfull
|
||||
// available outside of this package, if used as an amd module,
|
||||
// as webpack cannot handle amd define with moduleid
|
||||
return {leaflet: leaflet, screenfull: screenfull};
|
||||
}));
|
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 26 52" xmlns="http://www.w3.org/2000/svg"><path d="M20.6 36.7H16a.9.9 0 0 1-.8-.8v-4.5c0-.2.2-.4.4-.4h1.4c.3 0 .5.2.5.4v3h3c.2 0 .4.2.4.5v1.4c0 .2-.2.4-.4.4zm-9.9-.8v-4.5c0-.2-.2-.4-.4-.4H8.9c-.3 0-.5.2-.5.4v3h-3c-.2 0-.4.2-.4.5v1.4c0 .2.2.4.4.4H10c.4 0 .8-.4.8-.8zm0 10.7V42c0-.4-.4-.8-.8-.8H5.4c-.2 0-.4.2-.4.4v1.4c0 .3.2.5.4.5h3v3c0 .2.2.4.5.4h1.4c.2 0 .4-.2.4-.4zm6.9 0v-3h3c.2 0 .4-.2.4-.5v-1.4c0-.2-.2-.4-.4-.4H16c-.4 0-.8.4-.8.8v4.5c0 .2.2.4.4.4h1.4c.3 0 .5-.2.5-.4zM5 10.3V5.9c0-.5.4-.9.9-.9h4.4c.2 0 .4.2.4.4V7c0 .2-.2.4-.4.4h-3v3c0 .2-.2.4-.4.4H5.4a.4.4 0 0 1-.4-.4zm10.3-4.9V7c0 .2.2.4.4.4h3v3c0 .2.2.4.4.4h1.5c.2 0 .4-.2.4-.4V5.9c0-.5-.4-.9-.9-.9h-4.4c-.2 0-.4.2-.4.4zm5.3 9.9H19c-.2 0-.4.2-.4.4v3h-3c-.2 0-.4.2-.4.4v1.5c0 .2.2.4.4.4h4.4c.5 0 .9-.4.9-.9v-4.4c0-.2-.2-.4-.4-.4zm-9.9 5.3V19c0-.2-.2-.4-.4-.4h-3v-3c0-.2-.2-.4-.4-.4H5.4c-.2 0-.4.2-.4.4v4.4c0 .5.4.9.9.9h4.4c.2 0 .4-.2.4-.4z" fill="currentColor"/></svg>
|
After Width: | Height: | Size: 945 B |
@ -0,0 +1,60 @@
|
||||
.marker-cluster-small {
|
||||
background-color: rgba(181, 226, 140, 0.6);
|
||||
}
|
||||
.marker-cluster-small div {
|
||||
background-color: rgba(110, 204, 57, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-medium {
|
||||
background-color: rgba(241, 211, 87, 0.6);
|
||||
}
|
||||
.marker-cluster-medium div {
|
||||
background-color: rgba(240, 194, 12, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-large {
|
||||
background-color: rgba(253, 156, 115, 0.6);
|
||||
}
|
||||
.marker-cluster-large div {
|
||||
background-color: rgba(241, 128, 23, 0.6);
|
||||
}
|
||||
|
||||
/* IE 6-8 fallback colors */
|
||||
.leaflet-oldie .marker-cluster-small {
|
||||
background-color: rgb(181, 226, 140);
|
||||
}
|
||||
.leaflet-oldie .marker-cluster-small div {
|
||||
background-color: rgb(110, 204, 57);
|
||||
}
|
||||
|
||||
.leaflet-oldie .marker-cluster-medium {
|
||||
background-color: rgb(241, 211, 87);
|
||||
}
|
||||
.leaflet-oldie .marker-cluster-medium div {
|
||||
background-color: rgb(240, 194, 12);
|
||||
}
|
||||
|
||||
.leaflet-oldie .marker-cluster-large {
|
||||
background-color: rgb(253, 156, 115);
|
||||
}
|
||||
.leaflet-oldie .marker-cluster-large div {
|
||||
background-color: rgb(241, 128, 23);
|
||||
}
|
||||
|
||||
.marker-cluster {
|
||||
background-clip: padding-box;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.marker-cluster div {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 15px;
|
||||
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
.marker-cluster span {
|
||||
line-height: 30px;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
|
||||
-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
|
||||
-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
|
||||
-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
|
||||
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
|
||||
}
|
||||
|
||||
.leaflet-cluster-spider-leg {
|
||||
/* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
|
||||
-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
|
||||
-moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
|
||||
-o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
|
||||
transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
|
||||
}
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 696 B |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 618 B |
14033
netbox_device_map/static/netbox_device_map/leaflet/leaflet-src.esm.js
Normal file
14126
netbox_device_map/static/netbox_device_map/leaflet/leaflet-src.js
Normal file
657
netbox_device_map/static/netbox_device_map/leaflet/leaflet.css
Normal file
@ -0,0 +1,657 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
svg.leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover,
|
||||
.leaflet-bar a:focus {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover,
|
||||
.leaflet-control-attribution a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-control-attribution svg {
|
||||
display: inline !important;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 24px 13px 20px;
|
||||
line-height: 1.3;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
min-height: 1px;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 17px 0;
|
||||
margin: 1.3em 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-top: -1px;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
pointer-events: auto;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||
color: #757575;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||
color: #585858;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
-ms-zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
|
||||
/* Printing */
|
||||
|
||||
@media print {
|
||||
/* Prevent printers from removing background-images of controls. */
|
||||
.leaflet-control {
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 696 B |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 6.3 KiB |
14062
netbox_device_map/static/netbox_device_map/leaflet/v1.7.1/leaflet-src.js
Normal file
@ -0,0 +1,640 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg,
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-tile {
|
||||
will-change: opacity;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline: 0;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-container a.leaflet-active {
|
||||
outline: 2px solid orange;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-bar a:hover {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path {
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-container .leaflet-control-attribution,
|
||||
.leaflet-container .leaflet-control-scale {
|
||||
font-size: 11px;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 19px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 18px 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 4px 4px 0 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
font: 16px/14px Tahoma, Verdana, sans-serif;
|
||||
color: #c3c3c3;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover {
|
||||
color: #999;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
-ms-zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip-container {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-clickable {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
23
netbox_device_map/static/netbox_device_map/style.css
Normal file
@ -0,0 +1,23 @@
|
||||
.text-toggle[aria-expanded=false] .text-expanded {
|
||||
display: none;
|
||||
}
|
||||
.text-toggle[aria-expanded=true] .text-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-device-type {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.sidebar-device-role {
|
||||
font-size: .9rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.leaflet-sidebar > .close {
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.sidebar-cpe-list li {
|
||||
list-style-type: decimal;
|
||||
}
|
86
netbox_device_map/templates/netbox_device_map/main.html
Normal file
@ -0,0 +1,86 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Device map{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{# Leaflet #}
|
||||
<link rel="stylesheet" href="{% static 'netbox_device_map/leaflet/leaflet.css' %}">
|
||||
<script src="{% static 'netbox_device_map/leaflet/leaflet.js' %}"></script>
|
||||
{# Leaflet plugins #}
|
||||
<link rel="stylesheet" href="{% static 'netbox_device_map/leaflet.fullscreen/Control.FullScreen.css' %}">
|
||||
<script src="{% static 'netbox_device_map/leaflet.fullscreen/Control.FullScreen.js' %}"></script>
|
||||
<script src="{% static 'netbox_device_map/js/svg-icon.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'netbox_device_map/leaflet-sidebar/L.Control.Sidebar.css' %}">
|
||||
<script src="{% static 'netbox_device_map/leaflet-sidebar/L.Control.Sidebar.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'netbox_device_map/leaflet.markercluster/MarkerCluster.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'netbox_device_map/leaflet.markercluster/MarkerCluster.Default.css' %}">
|
||||
<script src="{% static 'netbox_device_map/leaflet.markercluster/leaflet.markercluster.js' %}"></script>
|
||||
|
||||
<link rel="stylesheet" href="{% static 'netbox_device_map/style.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<ul class="nav nav-tabs px-3">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="map-tab" data-bs-toggle="tab" data-bs-target="#map"
|
||||
type="button" role="tab" aria-controls="map" aria-selected="true">
|
||||
Geographical map
|
||||
{% if map_data %}<span class="badge bg-secondary">{{ map_data.markers|length }}</span>{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form"
|
||||
type="button" role="tab" aria-controls="filters-form" aria-selected="true">
|
||||
Filters
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="map" role="tabpanel" aria-labelledby="map-tab">
|
||||
{% if map_data %}
|
||||
<div class="mb-3">
|
||||
{% if non_geolocated_devices %}
|
||||
<p>
|
||||
<a class="btn btn-sm btn-outline-secondary text-toggle" data-bs-toggle="collapse"
|
||||
href="#notShownDevices"
|
||||
role="button" aria-expanded="false" aria-controls="collapseExample">
|
||||
<span class="text-collapsed">Show</span>
|
||||
<span class="text-expanded">Hide</span>
|
||||
devices are not geolocated ({{ non_geolocated_devices|length }})
|
||||
</a>
|
||||
</p>
|
||||
<div class="collapse" id="notShownDevices">
|
||||
<div class="card card-body">
|
||||
<ul>
|
||||
{% for device in non_geolocated_devices %}
|
||||
<li>
|
||||
<a href="{{ device.get_absolute_url }}"
|
||||
target="_blank">{{ device.name }}</a>
|
||||
<span class="separator">·</span>
|
||||
<span class="text-muted">{{ device.device_role.name }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="{{ map_data.map_id }}" style="height: 700px"></div>
|
||||
<div id="map-sidebar">
|
||||
<div class="h3 sidebar-device-name"></div>
|
||||
<div class="h6 sidebar-device-type"></div>
|
||||
<div class="h5 sidebar-device-role"></div>
|
||||
<div class="sidebar-cpe-list"></div>
|
||||
</div>
|
||||
{{ map_data|json_script:"map-data" }}
|
||||
<script src="{% static 'netbox_device_map/js/map.js' %}"></script>
|
||||
{% else %}
|
||||
Please specify filtering criteria for displaying the map
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
|
||||
{% include 'inc/filter_list.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
7
netbox_device_map/urls.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.MapView.as_view(), name='map'),
|
||||
path('connected-cpe/<int:pk>', views.ConnectedCpeAjaxView.as_view(), name='connected-cpe')
|
||||
]
|
74
netbox_device_map/views.py
Normal file
@ -0,0 +1,74 @@
|
||||
import re
|
||||
|
||||
from dcim.models import Device, Interface
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import View
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models import Q
|
||||
|
||||
from . import forms
|
||||
from .geographical_map import configure_leaflet_map
|
||||
from .helpers import get_device_location, get_connected_devices
|
||||
from .settings import plugin_settings
|
||||
|
||||
|
||||
INTEGER_REGEXP = re.compile(r'\d+')
|
||||
|
||||
|
||||
class MapView(PermissionRequiredMixin, View):
|
||||
permission_required = ('ipam.view_vlan', 'dcim.view_device', 'dcim.view_devicerole', 'dcim.view_cable')
|
||||
template_name = 'netbox_device_map/main.html'
|
||||
form = forms.DeviceMapFilterForm
|
||||
|
||||
def get(self, request):
|
||||
"""Device map view"""
|
||||
form = self.form(request.GET)
|
||||
if form.is_valid():
|
||||
interfaces = Interface.objects.all()
|
||||
vlan = form.cleaned_data['vlan']
|
||||
|
||||
interfaces = interfaces.filter(Q(untagged_vlan=vlan) | Q(tagged_vlans=vlan))
|
||||
devices = Device.objects.filter(interfaces__in=interfaces).distinct()
|
||||
if device_roles := form.cleaned_data['device_roles']:
|
||||
devices = devices.filter(device_role__in=device_roles)
|
||||
|
||||
geolocated_devices = {d: coords for d in devices if (coords := get_device_location(d))}
|
||||
non_geolocated_devices = set(devices) - set(geolocated_devices.keys())
|
||||
|
||||
map_data = configure_leaflet_map("geomap", geolocated_devices, form.cleaned_data['calculate_connections'])
|
||||
map_data['vlan'] = vlan.id
|
||||
return render(request, self.template_name, context=dict(
|
||||
filter_form=form, map_data=map_data, non_geolocated_devices=non_geolocated_devices
|
||||
))
|
||||
|
||||
return render(
|
||||
request, self.template_name,
|
||||
context=dict(filter_form=self.form(initial=request.GET))
|
||||
)
|
||||
|
||||
|
||||
class ConnectedCpeAjaxView(PermissionRequiredMixin, View):
|
||||
permission_required = ('dcim.view_device', 'dcim.view_cable')
|
||||
form = forms.ConnectedCpeForm
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
"""List of CPE devices connected to the specified node device"""
|
||||
try:
|
||||
device = Device.objects.get(pk=kwargs.get('pk'))
|
||||
except Device.DoesNotExist:
|
||||
return JsonResponse({'status': False, 'error': 'Device not found'}, status=404)
|
||||
form = self.form(request.GET)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
connected_devices_qs = get_connected_devices(device, vlan=data['vlan'])\
|
||||
.filter(device_role__name=plugin_settings['cpe_device_role']).order_by()
|
||||
connected_devices = [dict(id=d.id, name=d.name, url=d.get_absolute_url(), comments=d.comments)
|
||||
for d in connected_devices_qs]
|
||||
# Sorting list of CPE devices by the sequence of integers contained in the comments
|
||||
connected_devices.sort(key=lambda d: tuple(int(n) for n in INTEGER_REGEXP.findall(d['comments'])))
|
||||
return JsonResponse(dict(status=True, cpe_devices=connected_devices,
|
||||
device_type=f'{device.device_type.manufacturer.name} {device.device_type.model}'))
|
||||
else:
|
||||
return JsonResponse({'status': False, 'error': 'Form fields filled out incorrectly',
|
||||
'form_errors': form.errors}, status=404)
|
18
setup.py
Normal file
@ -0,0 +1,18 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
with open('README.md', encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
name='netbox-plugin-device-map',
|
||||
version='0.1.0',
|
||||
description='A simple device map with filter criteria',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
author='Victor Golovanenko',
|
||||
author_email='drygdryg2014@yandex.com',
|
||||
license='GPL-3.0',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
zip_safe=False
|
||||
)
|