├── .flake8
├── .github
└── workflows
│ ├── manifest-modified.yaml
│ ├── pub-pypi.yml
│ └── tests.yml
├── .gitignore
├── CODEOWNERS
├── LICENSE
├── MANIFEST.in
├── README.md
├── media
└── demo.gif
├── netbox-plugin.yaml
├── netbox_floorplan
├── __init__.py
├── admin.py
├── api
│ ├── __init__.py
│ ├── serializers.py
│ ├── urls.py
│ └── views.py
├── filtersets.py
├── forms.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_floorplan_measurement_unit_alter_floorplan_scale.py
│ ├── 0003_floorplan_canvas.py
│ ├── 0004_alter_floorplan_options.py
│ ├── 0005_alter_floorplan_site.py
│ ├── 0006_delete_floorplanobject.py
│ ├── 0007_alter_floorplan_options_and_more.py
│ ├── 0008_floorplanimage.py
│ ├── 0009_floorplanimage_file.py
│ ├── 0010_floorplanimage_external_url.py
│ ├── 0011_floorplanimage_comments.py
│ ├── 0012_alter_floorplan_options_and_more.py
│ └── __init__.py
├── models.py
├── navigation.py
├── static
│ └── netbox_floorplan
│ │ ├── floorplan
│ │ ├── edit.js
│ │ ├── utils.js
│ │ └── view.js
│ │ └── vendors
│ │ ├── fabric-js-6.0.2.js
│ │ ├── htmx.min.js
│ │ └── jquery-3.7.1.js
├── tables.py
├── templates
│ └── netbox_floorplan
│ │ ├── floorplan.html
│ │ ├── floorplan_edit.html
│ │ ├── floorplan_list.html
│ │ ├── floorplan_ui_listview.html
│ │ ├── floorplan_view.html
│ │ ├── floorplanimage.html
│ │ ├── floorplanimage_edit.html
│ │ └── inc
│ │ └── pro_tips.html
├── templatetags
│ ├── __init__.py
│ └── template_utils.py
├── urls.py
├── utils.py
├── version.py
└── views.py
├── setup.cfg
└── setup.py
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 140
3 | ignore = E126, E722
--------------------------------------------------------------------------------
/.github/workflows/manifest-modified.yaml:
--------------------------------------------------------------------------------
1 | name: NetBox plugin manifest modified
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | paths:
7 | - netbox-plugin.yaml
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}
11 | cancel-in-progress: false
12 |
13 | jobs:
14 | manifest-modified:
15 | uses: netboxlabs/public-workflows/.github/workflows/reusable-plugin-manifest-modified.yml@release
16 |
--------------------------------------------------------------------------------
/.github/workflows/pub-pypi.yml:
--------------------------------------------------------------------------------
1 | name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | build-n-publish:
9 | name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
10 | runs-on: ubuntu-latest
11 | environment: release
12 | permissions:
13 | id-token: write
14 | contents: read
15 | steps:
16 | - uses: actions/checkout@main
17 | - name: Set up Python 3.12
18 | uses: actions/setup-python@v1
19 | with:
20 | python-version: 3.12
21 | - name: Install pypa/build
22 | run: >-
23 | python -m
24 | pip install
25 | build
26 | --user
27 | - name: Build a binary wheel and a source tarball
28 | run: >-
29 | python -m
30 | build
31 | --sdist
32 | --wheel
33 | --outdir dist/
34 | .
35 | - name: Publish distribution 📦 to PyPI
36 | uses: pypa/gh-action-pypi-publish@release/v1
37 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | flake8-lint:
7 | runs-on: ubuntu-latest
8 | name: Lint
9 | steps:
10 | - name: Check out source repository
11 | uses: actions/checkout@v3
12 | - name: Set up Python environment
13 | uses: actions/setup-python@v4
14 | with:
15 | python-version: "3.12"
16 | - name: flake8 Lint
17 | uses: py-actions/flake8@v2
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | share/python-wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | *.py,cover
52 | .hypothesis/
53 | .pytest_cache/
54 | cover/
55 |
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # Django stuff:
61 | *.log
62 | local_settings.py
63 | db.sqlite3
64 | db.sqlite3-journal
65 |
66 | # Flask stuff:
67 | instance/
68 | .webassets-cache
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | .pybuilder/
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | # For a library or package, you might want to ignore these files since the code is
89 | # intended to run in multiple environments; otherwise, check them in:
90 | # .python-version
91 |
92 | # pipenv
93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
96 | # install all needed dependencies.
97 | #Pipfile.lock
98 |
99 | # poetry
100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
101 | # This is especially recommended for binary packages to ensure reproducibility, and is more
102 | # commonly ignored for libraries.
103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
104 | #poetry.lock
105 |
106 | # pdm
107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
108 | #pdm.lock
109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
110 | # in version control.
111 | # https://pdm.fming.dev/#use-with-ide
112 | .pdm.toml
113 |
114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115 | __pypackages__/
116 |
117 | # Celery stuff
118 | celerybeat-schedule
119 | celerybeat.pid
120 |
121 | # SageMath parsed files
122 | *.sage.py
123 |
124 | # Environments
125 | .env
126 | .venv
127 | env/
128 | venv/
129 | ENV/
130 | env.bak/
131 | venv.bak/
132 |
133 | # Spyder project settings
134 | .spyderproject
135 | .spyproject
136 |
137 | # Rope project settings
138 | .ropeproject
139 |
140 | # mkdocs documentation
141 | /site
142 |
143 | # mypy
144 | .mypy_cache/
145 | .dmypy.json
146 | dmypy.json
147 |
148 | # Pyre type checker
149 | .pyre/
150 |
151 | # pytype static type analyzer
152 | .pytype/
153 |
154 | # Cython debug symbols
155 | cython_debug/
156 |
157 | # PyCharm
158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160 | # and can be added to the global gitignore or merged into this file. For a more nuclear
161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162 | #.idea/
163 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @tbotnz @cruse1977
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
4 |
5 | Originally Forked from https://github.com/tbotnz/netbox_floorplan
6 |
7 | ## Demo
8 | 
9 |
10 | ## Summary
11 | A netbox plugin providing floorplan mapping capability for locations and sites
12 |
13 | - provides graphical ability to draw racks & unracked devices on a floorplan
14 | - support for metadata such as labels, areas, walls, coloring
15 | - floorplan object mapped to sites or locations and click through rack/devices
16 | - keyboard controls supported
17 | - export to svg
18 |
19 | ## Compatibility
20 |
21 | | NetBox Version | Plugin Version |
22 | |-------------|-----------|
23 | | 3.5 | >= 0.3.2 |
24 | | 3.6 | >= 0.3.2 |
25 | | 4.0.x | 0.4.1 |
26 | | 4.1.x | 0.5.0 |
27 | | 4.2.x | 0.6.0 |
28 | | 4.3.x | 0.7.0 |
29 |
30 | ## Installing
31 |
32 | The plugin is available as a Python package in pypi and can be installed with pip
33 |
34 |
35 | ```
36 | sudo pip install netbox-floorplan-plugin
37 | ```
38 | Enable the plugin in /opt/netbox/netbox/netbox/configuration.py:
39 | ```
40 | PLUGINS = ['netbox_floorplan']
41 | ```
42 | Enable Migrations:
43 | ```
44 | cd /opt/netbox
45 | sudo ./venv/bin/python3 netbox/manage.py makemigrations netbox_floorplan_plugin
46 | sudo ./venv/bin/python3 netbox/manage.py migrate
47 | sudo ./venv/bin/python3 netbox/manage.py collectstatic
48 | ```
49 |
50 | Restart NetBox and add `netbox-floorplan-plugin` to your local_requirements.txt
51 |
52 | See [NetBox Documentation](https://docs.netbox.dev/en/stable/plugins/#installing-plugins) for details
53 |
54 | >[!IMPORTANT]
55 | >In order for racks to display properly, the rack type of the rack should be specified and a width/height set within the type.
56 |
57 | ## Mentions
58 |
59 | Forked from https://github.com/tbotnz/netbox_floorplan
60 |
61 | Special thanks to Ziply Fiber network automation team for helping originally helping to conceive this during the NANOG hackathon
62 |
63 | ## Release process
64 |
65 | Update `netbox_floorplan/version.py` with a new version number, create a new Github release with the same number, the pypi publish workflow will run.
66 |
--------------------------------------------------------------------------------
/media/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-floorplan-plugin/68cef7fd35350a4fad427670285c62000dffc1e9/media/demo.gif
--------------------------------------------------------------------------------
/netbox-plugin.yaml:
--------------------------------------------------------------------------------
1 | version: 0.1
2 | package_name: netbox-floorplan-plugin
3 | compatibility:
4 | - release: 0.7.0
5 | netbox_min: 4.3.0
6 | netbox_max: 4.3.99
7 | - release: 0.6.0
8 | netbox_min: 4.2.0
9 | netbox_max: 4.2.99
10 | - release: 0.5.0
11 | netbox_min: 4.1.0
12 | netbox_max: 4.1.11
13 | - release: 0.4.1
14 | netbox_min: 4.0.2
15 | netbox_max: 4.0.11
16 | - release: 0.4.0
17 | netbox_min: 4.0.2
18 | netbox_max: 4.0.10
19 | - release: 0.3.6
20 | netbox_min: 3.5.2
21 | netbox_max: 3.7.8
22 | - release: 0.3.3
23 | netbox_min: 3.7.2
24 | netbox_max: 3.7.5
25 | - release: 0.3.2
26 | netbox_min: 3.5.1
27 | netbox_max: 3.7.2
28 |
--------------------------------------------------------------------------------
/netbox_floorplan/__init__.py:
--------------------------------------------------------------------------------
1 | from netbox.plugins import PluginConfig
2 | from .version import __version__
3 |
4 |
5 | class FloorplanConfig(PluginConfig):
6 |
7 | name = "netbox_floorplan"
8 | verbose_name = "Netbox Floorplan"
9 | description = ""
10 | version = __version__
11 | base_url = "floorplan"
12 | min_version = "4.3.0"
13 | max_version = "4.3.99"
14 |
15 |
16 | config = FloorplanConfig
17 |
--------------------------------------------------------------------------------
/netbox_floorplan/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Floorplan
3 |
4 |
5 | @admin.register(Floorplan)
6 | class FloorplanAdmin(admin.ModelAdmin):
7 | list_display = (
8 | "pk",
9 | )
10 |
--------------------------------------------------------------------------------
/netbox_floorplan/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-floorplan-plugin/68cef7fd35350a4fad427670285c62000dffc1e9/netbox_floorplan/api/__init__.py
--------------------------------------------------------------------------------
/netbox_floorplan/api/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from netbox.api.serializers import NetBoxModelSerializer
3 | from ..models import Floorplan, FloorplanImage
4 |
5 |
6 | class FloorplanImageSerializer(NetBoxModelSerializer):
7 | url = serializers.HyperlinkedIdentityField(
8 | view_name='plugins-api:netbox_floorplan-api:floorplanimage-detail')
9 |
10 | class Meta:
11 | model = FloorplanImage
12 | fields = ['id', 'url', 'name', 'file', 'external_url', 'filename', 'comments', 'tags', 'custom_fields', 'created', 'last_updated']
13 | brief_fields = ['id', 'url', 'name', 'file', 'filename', 'external_url']
14 |
15 |
16 | class FloorplanSerializer(NetBoxModelSerializer):
17 | url = serializers.HyperlinkedIdentityField(
18 | view_name='plugins-api:netbox_floorplan-api:floorplan-detail')
19 | assigned_image = FloorplanImageSerializer(nested=True, required=False, allow_null=True)
20 |
21 | class Meta:
22 | model = Floorplan
23 | fields = ['id', 'url', 'site', 'location', 'assigned_image',
24 | 'width', 'height', 'tags', 'custom_fields', 'created',
25 | 'last_updated', 'canvas', 'measurement_unit']
26 |
--------------------------------------------------------------------------------
/netbox_floorplan/api/urls.py:
--------------------------------------------------------------------------------
1 | from netbox.api.routers import NetBoxRouter
2 | from . import views
3 |
4 | app_name = 'netbox_floorplan'
5 |
6 | router = NetBoxRouter()
7 | router.register('floorplans', views.FloorplanViewSet)
8 | router.register('floorplanimages', views.FloorplanImageViewSet)
9 | urlpatterns = router.urls
10 |
--------------------------------------------------------------------------------
/netbox_floorplan/api/views.py:
--------------------------------------------------------------------------------
1 | from netbox.api.viewsets import NetBoxModelViewSet
2 |
3 | from .. import filtersets, models
4 | from .serializers import FloorplanSerializer, FloorplanImageSerializer
5 |
6 |
7 | class FloorplanViewSet(NetBoxModelViewSet):
8 | queryset = models.Floorplan.objects.all()
9 | serializer_class = FloorplanSerializer
10 | filterset_class = filtersets.FloorplanFilterSet
11 |
12 |
13 | class FloorplanImageViewSet(NetBoxModelViewSet):
14 | queryset = models.FloorplanImage.objects.prefetch_related('tags')
15 | serializer_class = FloorplanImageSerializer
16 |
--------------------------------------------------------------------------------
/netbox_floorplan/filtersets.py:
--------------------------------------------------------------------------------
1 | from netbox.filtersets import NetBoxModelFilterSet
2 | from .models import Floorplan
3 |
4 |
5 | class FloorplanFilterSet(NetBoxModelFilterSet):
6 | class Meta:
7 | model = Floorplan
8 | fields = ['id', 'site', 'location']
9 |
10 | def search(self, queryset, name, value):
11 | return queryset.filter(description__icontains=value)
12 |
--------------------------------------------------------------------------------
/netbox_floorplan/forms.py:
--------------------------------------------------------------------------------
1 | from netbox.forms import NetBoxModelForm
2 | from .models import Floorplan, FloorplanImage
3 | from dcim.models import Rack, Device
4 | from utilities.forms.rendering import FieldSet
5 | from utilities.forms.fields import CommentField
6 |
7 |
8 | class FloorplanImageForm(NetBoxModelForm):
9 |
10 | comments = CommentField()
11 |
12 | fieldsets = (
13 | FieldSet(('name', 'file', 'external_url', 'comments'), name='General'),
14 | FieldSet(('comments', 'tags'), name='')
15 | )
16 |
17 | class Meta:
18 | model = FloorplanImage
19 | fields = [
20 | 'name',
21 | 'file',
22 | 'external_url'
23 | ]
24 |
25 |
26 | class FloorplanForm(NetBoxModelForm):
27 | class Meta:
28 | model = Floorplan
29 | fields = ['site', 'location', 'assigned_image', 'width', 'height']
30 |
31 |
32 | class FloorplanRackFilterForm(NetBoxModelForm):
33 | class Meta:
34 | model = Rack
35 | fields = ['name']
36 |
37 |
38 | class FloorplanDeviceFilterForm(NetBoxModelForm):
39 | class Meta:
40 | model = Device
41 | fields = ['name']
42 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.9 on 2023-06-10 22:50
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 | import netbox_floorplan.utils
6 | import taggit.managers
7 | import utilities.json
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | initial = True
13 |
14 | dependencies = [
15 | ('dcim', '0172_larger_power_draw_values'),
16 | ('extras', '0092_delete_jobresult'),
17 | ]
18 |
19 | operations = [
20 | migrations.CreateModel(
21 | name='Floorplan',
22 | fields=[
23 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
24 | ('created', models.DateTimeField(auto_now_add=True, null=True)),
25 | ('last_updated', models.DateTimeField(auto_now=True, null=True)),
26 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
27 | ('background_image', models.ImageField(blank=True, null=True, upload_to=netbox_floorplan.utils.file_upload)),
28 | ('scale', models.DecimalField(blank=True, decimal_places=50, max_digits=100, null=True)),
29 | ('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.location')),
30 | ('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='dcim.site')),
31 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
32 | ],
33 | options={
34 | 'ordering': ('site', 'location', 'background_image', 'scale'),
35 | },
36 | ),
37 | migrations.CreateModel(
38 | name='FloorplanObject',
39 | fields=[
40 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
41 | ('created', models.DateTimeField(auto_now_add=True, null=True)),
42 | ('last_updated', models.DateTimeField(auto_now=True, null=True)),
43 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
44 | ('x_coordinate', models.DecimalField(decimal_places=50, max_digits=100)),
45 | ('y_coordinate', models.DecimalField(decimal_places=50, max_digits=100)),
46 | ('rotation', models.IntegerField(default=0)),
47 | ('floorplan', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='netbox_floorplan.floorplan')),
48 | ('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.location')),
49 | ('rack', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.rack')),
50 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
51 | ],
52 | options={
53 | 'ordering': ('floorplan', 'rack', 'location', 'x_coordinate', 'y_coordinate', 'rotation'),
54 | },
55 | ),
56 | ]
57 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/0002_floorplan_measurement_unit_alter_floorplan_scale.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.9 on 2023-06-10 23:14
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('netbox_floorplan', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='floorplan',
15 | name='measurement_unit',
16 | field=models.CharField(default='m', max_length=2),
17 | ),
18 | migrations.AlterField(
19 | model_name='floorplan',
20 | name='scale',
21 | field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/0003_floorplan_canvas.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.9 on 2023-06-11 16:36
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('netbox_floorplan', '0002_floorplan_measurement_unit_alter_floorplan_scale'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='floorplan',
15 | name='canvas',
16 | field=models.JSONField(default=dict),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/0004_alter_floorplan_options.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.9 on 2023-07-08 09:42
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('netbox_floorplan', '0003_floorplan_canvas'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='floorplan',
15 | options={'ordering': ('site', 'location', 'background_image', 'scale', 'measurement_unit')},
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/0005_alter_floorplan_site.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.9 on 2023-07-18 10:58
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('dcim', '0172_larger_power_draw_values'),
11 | ('netbox_floorplan', '0004_alter_floorplan_options'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='floorplan',
17 | name='site',
18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.site'),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/0006_delete_floorplanobject.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.9 on 2023-07-18 11:44
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('netbox_floorplan', '0005_alter_floorplan_site'),
10 | ]
11 |
12 | operations = [
13 | migrations.DeleteModel(
14 | name='FloorplanObject',
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/0007_alter_floorplan_options_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.1.9 on 2023-07-18 11:49
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('netbox_floorplan', '0006_delete_floorplanobject'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='floorplan',
15 | options={'ordering': ('site', 'location', 'background_image', 'width', 'height', 'measurement_unit')},
16 | ),
17 | migrations.RenameField(
18 | model_name='floorplan',
19 | old_name='scale',
20 | new_name='height',
21 | ),
22 | migrations.AddField(
23 | model_name='floorplan',
24 | name='width',
25 | field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/0008_floorplanimage.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.0.7 on 2024-07-19 23:49
2 |
3 | import taggit.managers
4 | import utilities.json
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('extras', '0115_convert_dashboard_widgets'),
12 | ('netbox_floorplan', '0007_alter_floorplan_options_and_more'),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='FloorplanImage',
18 | fields=[
19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
20 | ('created', models.DateTimeField(auto_now_add=True, null=True)),
21 | ('last_updated', models.DateTimeField(auto_now=True, null=True)),
22 | ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
23 | ('name', models.CharField(max_length=128)),
24 | ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
25 | ],
26 | options={
27 | 'ordering': ('name',),
28 | },
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/0009_floorplanimage_file.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.0.7 on 2024-07-20 19:02
2 |
3 | import netbox_floorplan.utils
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('netbox_floorplan', '0008_floorplanimage'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='floorplanimage',
16 | name='file',
17 | field=models.FileField(blank=True, upload_to=netbox_floorplan.utils.file_upload),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/0010_floorplanimage_external_url.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.0.7 on 2024-07-20 19:57
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('netbox_floorplan', '0009_floorplanimage_file'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='floorplanimage',
15 | name='external_url',
16 | field=models.URLField(blank=True, max_length=255),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/0011_floorplanimage_comments.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.0.7 on 2024-07-20 20:34
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('netbox_floorplan', '0010_floorplanimage_external_url'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='floorplanimage',
15 | name='comments',
16 | field=models.TextField(blank=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/0012_alter_floorplan_options_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.0.7 on 2024-07-20 20:53
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('netbox_floorplan', '0011_floorplanimage_comments'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterModelOptions(
15 | name='floorplan',
16 | options={'ordering': ('site', 'location', 'assigned_image', 'width', 'height', 'measurement_unit')},
17 | ),
18 | migrations.RemoveField(
19 | model_name='floorplan',
20 | name='background_image',
21 | ),
22 | migrations.AddField(
23 | model_name='floorplan',
24 | name='assigned_image',
25 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='netbox_floorplan.floorplanimage'),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/netbox_floorplan/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netbox-community/netbox-floorplan-plugin/68cef7fd35350a4fad427670285c62000dffc1e9/netbox_floorplan/migrations/__init__.py
--------------------------------------------------------------------------------
/netbox_floorplan/models.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ValidationError
2 | from django.db import models
3 | from django.urls import reverse
4 | from netbox.models import NetBoxModel
5 | from dcim.models import Rack, Device
6 | from .utils import file_upload
7 |
8 |
9 | class FloorplanImage(NetBoxModel):
10 | """
11 | A Floorplan Image is effectively a background image
12 | """
13 | name = models.CharField(
14 | help_text='Can be used to quickly identify a particular image',
15 | max_length=128,
16 | blank=False,
17 | null=False
18 | )
19 |
20 | file = models.FileField(
21 | upload_to=file_upload,
22 | blank=True
23 | )
24 |
25 | external_url = models.URLField(
26 | blank=True,
27 | max_length=255
28 | )
29 |
30 | comments = models.TextField(
31 | blank=True
32 | )
33 |
34 | def get_absolute_url(self):
35 | return reverse('plugins:netbox_floorplan:floorplanimage', args=[self.pk])
36 |
37 | def __str__(self):
38 | return f'{self.name}'
39 |
40 | class Meta:
41 | ordering = ('name',)
42 |
43 | @property
44 | def size(self):
45 | """
46 | Wrapper around `document.size` to suppress an OSError in case the file is inaccessible. Also opportunistically
47 | catch other exceptions that we know other storage back-ends to throw.
48 | """
49 | expected_exceptions = [OSError]
50 |
51 | try:
52 | from botocore.exceptions import ClientError
53 | expected_exceptions.append(ClientError)
54 | except ImportError:
55 | pass
56 |
57 | try:
58 | return self.file.size
59 | except NameError:
60 | return None
61 |
62 | @property
63 | def filename(self):
64 | filename = self.file.name.rsplit('/', 1)[-1]
65 | return filename
66 |
67 | def clean(self):
68 | super().clean()
69 |
70 | # Must have an uploaded document or an external URL. cannot have both
71 | if not self.file and self.external_url == '':
72 | raise ValidationError("A document must contain an uploaded file or an external URL.")
73 | if self.file and self.external_url:
74 | raise ValidationError("A document cannot contain both an uploaded file and an external URL.")
75 |
76 | def delete(self, *args, **kwargs):
77 |
78 | # Check if its a document or a URL
79 | if self.external_url == '':
80 |
81 | _name = self.file.name
82 |
83 | # Delete file from disk
84 | super().delete(*args, **kwargs)
85 | self.file.delete(save=False)
86 |
87 | # Restore the name of the document as it's re-used in the notifications later
88 | self.file.name = _name
89 | else:
90 | # Straight delete of external URL
91 | super().delete(*args, **kwargs)
92 |
93 |
94 | class Floorplan(NetBoxModel):
95 |
96 | site = models.ForeignKey(
97 | to='dcim.Site',
98 | blank=True,
99 | null=True,
100 | on_delete=models.PROTECT
101 | )
102 | location = models.ForeignKey(
103 | to='dcim.Location',
104 | blank=True,
105 | null=True,
106 | on_delete=models.PROTECT
107 | )
108 |
109 | assigned_image = models.ForeignKey(
110 | to='FloorplanImage',
111 | blank=True,
112 | null=True,
113 | on_delete=models.SET_NULL
114 | )
115 |
116 | width = models.DecimalField(
117 | max_digits=10,
118 | decimal_places=2,
119 | blank=True,
120 | null=True
121 | )
122 |
123 | height = models.DecimalField(
124 | max_digits=10,
125 | decimal_places=2,
126 | blank=True,
127 | null=True
128 | )
129 | measurement_choices = [
130 | ('ft', 'Feet'),
131 | ('m', 'Meters')
132 | ]
133 | measurement_unit = models.CharField(
134 | max_length=2,
135 | choices=measurement_choices,
136 | default='m'
137 | )
138 |
139 | canvas = models.JSONField(default=dict)
140 |
141 | class Meta:
142 | ordering = ('site', 'location', 'assigned_image',
143 | 'width', 'height', 'measurement_unit')
144 |
145 | def __str__(self):
146 | if self.site:
147 | return f'{self.site.name} Floorplan'
148 | else:
149 | return f'{self.location.name} Floorplan'
150 |
151 | def get_absolute_url(self):
152 | return reverse('plugins:netbox_floorplan:floorplan_edit', args=[self.pk])
153 |
154 | @property
155 | def record_type(self):
156 | if self.site:
157 | return "site"
158 | else:
159 | return "location"
160 |
161 | @property
162 | def mapped_racks(self):
163 | drawn_racks = []
164 | if self.canvas:
165 | if self.canvas.get("objects"):
166 | for obj in self.canvas["objects"]:
167 | if obj.get("objects"):
168 | for subobj in obj["objects"]:
169 | if subobj.get("custom_meta"):
170 | if subobj["custom_meta"].get("object_type") == "rack":
171 | drawn_racks.append(
172 | int(subobj["custom_meta"]["object_id"]))
173 | return drawn_racks
174 |
175 | @property
176 | def mapped_devices(self):
177 | drawn_devices = []
178 | if self.canvas:
179 | if self.canvas.get("objects"):
180 | for obj in self.canvas["objects"]:
181 | if obj.get("objects"):
182 | for subobj in obj["objects"]:
183 | if subobj.get("custom_meta"):
184 | if subobj["custom_meta"].get("object_type") == "device":
185 | drawn_devices.append(
186 | int(subobj["custom_meta"]["object_id"]))
187 | return drawn_devices
188 |
189 | def resync_canvas(self):
190 | changed = False
191 | if self.canvas:
192 | if self.canvas.get("objects"):
193 | for index, obj in enumerate(self.canvas["objects"]):
194 | if obj.get("custom_meta"):
195 | if obj["custom_meta"].get("object_type") == "rack":
196 | rack_id = int(obj["custom_meta"]["object_id"])
197 | # if rack is not in the database, remove it from the canvas
198 | rack_qs = Rack.objects.filter(pk=rack_id)
199 | if not rack_qs.exists():
200 | self.canvas["objects"].remove(obj)
201 | changed = True
202 | else:
203 | rack = rack_qs.first()
204 | self.canvas["objects"][index]["custom_meta"]["object_name"] = rack.name
205 | if obj.get("objects"):
206 | for subcounter, subobj in enumerate(obj["objects"]):
207 | if subobj.get("type") == "i-text":
208 | if subobj.get("custom_meta", {}).get("text_type") == "name":
209 | if subobj["text"] != f"{rack.name}":
210 | self.canvas["objects"][index]["objects"][
211 | subcounter]["text"] = f"{rack.name}"
212 | changed = True
213 | if subobj.get("custom_meta", {}).get("text_type") == "status":
214 | if subobj["text"] != f"{rack.status}":
215 | self.canvas["objects"][index]["objects"][
216 | subcounter]["text"] = f"{rack.status}"
217 | changed = True
218 | if obj["custom_meta"].get("object_type") == "device":
219 | device_id = int(obj["custom_meta"]["object_id"])
220 | # if device is not in the database, remove it from the canvas
221 | device_qs = Device.objects.filter(pk=device_id)
222 | if not device_qs.exists():
223 | self.canvas["objects"].remove(obj)
224 | changed = True
225 | else:
226 | device = device_qs.first()
227 | self.canvas["objects"][index]["custom_meta"]["object_name"] = device.name
228 | if obj.get("objects"):
229 | for subcounter, subobj in enumerate(obj["objects"]):
230 | if subobj.get("type") == "i-text":
231 | if subobj.get("custom_meta", {}).get("text_type") == "name":
232 | if subobj["text"] != f"{device.name}":
233 | self.canvas["objects"][index]["objects"][
234 | subcounter]["text"] = f"{device.name}"
235 | changed = True
236 | if subobj.get("custom_meta", {}).get("text_type") == "status":
237 | if subobj["text"] != f"{device.status}":
238 | self.canvas["objects"][index]["objects"][
239 | subcounter]["text"] = f"{device.status}"
240 | changed = True
241 | if changed:
242 | self.save()
243 |
244 | def save(self, *args, **kwargs):
245 | if self.site and self.location:
246 | raise ValueError(
247 | "Only one of site or location can be set for a floorplan")
248 | # ensure that the site or location is set
249 | if not self.site and not self.location:
250 | raise ValueError(
251 | "Either site or location must be set for a floorplan")
252 | super().save(*args, **kwargs)
253 |
--------------------------------------------------------------------------------
/netbox_floorplan/navigation.py:
--------------------------------------------------------------------------------
1 | """
2 | Define the plugin menu buttons & the plugin navigation bar enteries.
3 | """
4 |
5 | from netbox.plugins import PluginMenuItem, PluginMenuButton
6 |
7 |
8 | #
9 | # Define plugin menu buttons
10 | #
11 | menu_buttons = (
12 | PluginMenuItem(
13 | link="plugins:netbox_floorplan:floorplanimage_list",
14 | link_text="Floorplan Images",
15 | buttons=(
16 | PluginMenuButton(
17 | link='plugins:netbox_floorplan:floorplanimage_add',
18 | title='Add',
19 | icon_class='mdi mdi-plus-thick',
20 | ),
21 | ),
22 | ),
23 |
24 | )
25 |
26 |
27 | menu_items = menu_buttons
28 |
--------------------------------------------------------------------------------
/netbox_floorplan/static/netbox_floorplan/floorplan/edit.js:
--------------------------------------------------------------------------------
1 | // start initial ----------------------------------------------------------------------------- !
2 |
3 |
4 | import {
5 | resize_canvas,
6 | export_svg,
7 | enable_button_selection,
8 | disable_button_selection,
9 | prevent_leaving_canvas,
10 | wheel_zoom,
11 | stop_pan,
12 | start_pan,
13 | move_pan,
14 | reset_zoom,
15 | init_floor_plan
16 | } from "/static/netbox_floorplan/floorplan/utils.js";
17 |
18 |
19 | var csrf = document.getElementById('csrf').value;
20 | var obj_pk = document.getElementById('obj_pk').value;
21 | var obj_name = document.getElementById('obj_name').value;
22 | var record_type = document.getElementById('record_type').value;
23 | var site_id = document.getElementById('site_id').value;
24 | var location_id = document.getElementById('location_id').value;
25 |
26 | htmx.ajax('GET', `/plugins/floorplan/floorplans/racks/?floorplan_id=${obj_pk}`, { source: '#rack-card', target: '#rack-card', swap: 'innerHTML', trigger: 'load' })
27 | htmx.ajax('GET', `/plugins/floorplan/floorplans/devices/?floorplan_id=${obj_pk}`, { source: '#unrack-card', target: '#unrack-card', swap: 'innerHTML', trigger: 'load' })
28 |
29 |
30 | fabric.Object.prototype.set({
31 | snapThreshold: 45,
32 | snapAngle: 45
33 | });
34 |
35 | var current_zoom = 1;
36 |
37 | var canvas = new fabric.Canvas('canvas'),
38 | canvasWidth = document.getElementById('canvas').width,
39 | canvasHeight = document.getElementById('canvas').height;
40 |
41 | // end initial ----------------------------------------------------------------------------- !
42 |
43 |
44 | // start motion events ----------------------------------------------------------------------------- !
45 |
46 | canvas.on({
47 | "selection:updated": enable_button_selection,
48 | "selection:created": enable_button_selection,
49 | "selection:cleared": disable_button_selection,
50 | });
51 |
52 | canvas.on('object:moving', function (opt) {
53 | prevent_leaving_canvas(opt, canvas);
54 | });
55 |
56 | // end motion events ----------------------------------------------------------------------------- !
57 |
58 | // start grid ----------------------------------------------------------------------------- !
59 | var grid = 8;
60 |
61 | canvas.on('object:moving', function (options) {
62 | options.target.set({
63 | left: Math.round(options.target.left / grid) * grid,
64 | top: Math.round(options.target.top / grid) * grid
65 | });
66 | });
67 |
68 | // end grid ----------------------------------------------------------------------------- !
69 |
70 | // start zoom, pan control & resizing ----------------------------------------------------------------------------- !
71 |
72 | $(window).resize(resize_canvas(canvas, window));
73 |
74 | canvas.on('mouse:wheel', function (opt) {
75 | wheel_zoom(opt, canvas);
76 | });
77 |
78 | canvas.on('mouse:down', function (opt) {
79 | start_pan(opt, canvas);
80 | });
81 |
82 | canvas.on('mouse:move', function (opt) {
83 | move_pan(opt, canvas);
84 | });
85 | canvas.on('mouse:up', function (opt) {
86 | stop_pan(canvas);
87 | });
88 |
89 | // start zoom, pan control & resizing ----------------------------------------------------------------------------- !
90 |
91 | // start buttons ----------------------------------------------------------------------------- !
92 |
93 | document.getElementById('reset_zoom').addEventListener('click', () => {
94 | reset_zoom(canvas);
95 | });
96 |
97 | document.getElementById('export_svg').addEventListener('click', () => {
98 | export_svg(canvas);
99 | });
100 |
101 | function add_wall() {
102 | var wall = new fabric.Rect({
103 | top: 0,
104 | left: 0,
105 | width: 10,
106 | height: 500,
107 | //fill: '#6ea8fe',
108 | fill: 'red',
109 | opacity: 0.8,
110 | lockRotation: false,
111 | originX: "center",
112 | originY: "center",
113 | cornerSize: 15,
114 | hasRotatingPoint: true,
115 | perPixelTargetFind: true,
116 | minScaleLimit: 1,
117 | maxWidth: canvasWidth,
118 | maxHeight: canvasHeight,
119 | centeredRotation: true,
120 | angle: 90,
121 | custom_meta: {
122 | "object_type": "wall",
123 | },
124 | });
125 | var group = new fabric.Group([wall]);
126 |
127 | group.setControlsVisibility({
128 | mt: true,
129 | mb: true,
130 | ml: true,
131 | mr: true,
132 | bl: false,
133 | br: false,
134 | tl: false,
135 | tr: false,
136 | })
137 |
138 | canvas.add(group);
139 | canvas.centerObject(group);
140 | }
141 | window.add_wall = add_wall;
142 |
143 | function add_area() {
144 | var wall = new fabric.Rect({
145 | top: 0,
146 | left: 0,
147 | width: 300,
148 | height: 300,
149 | fill: '#6ea8fe',
150 | opacity: 0.5,
151 | lockRotation: false,
152 | originX: "center",
153 | originY: "center",
154 | cornerSize: 15,
155 | hasRotatingPoint: true,
156 | perPixelTargetFind: true,
157 | minScaleLimit: 1,
158 | maxWidth: canvasWidth,
159 | maxHeight: canvasHeight,
160 | centeredRotation: true,
161 | angle: 90,
162 | custom_meta: {
163 | "object_type": "area",
164 | },
165 | });
166 | var group = new fabric.Group([wall]);
167 |
168 | group.setControlsVisibility({
169 | mt: true,
170 | mb: true,
171 | ml: true,
172 | mr: true,
173 | bl: false,
174 | br: false,
175 | tl: false,
176 | tr: false,
177 | })
178 | canvas.add(group);
179 | canvas.centerObject(group);
180 | canvas.requestRenderAll();
181 | }
182 | window.add_area = add_area;
183 |
184 | /*
185 | * lock_floorplan_object: Toggle function to enable/disable movement and resize of objects
186 | * Uses object.custom_meta.object_type to determine which controls to enable/disable
187 | * for walls/area, mtr, mt, mb, ml, mr and movement/rotation are all enabled/disabled.
188 | * for racks, only mtr and movement/roatation are enabled/disabled.
189 | */
190 | function lock_floorplan_object() {
191 | var object = canvas.getActiveObject();
192 | if (object) {
193 | if (object.lockMovementX) {
194 | object.set({
195 | 'lockMovementX': false,
196 | 'lockMovementY': false,
197 | 'lockRotation': false
198 | });
199 | object.setControlsVisibility({
200 | mtr: true,
201 | });
202 | if ( object._objects[0].custom_meta.object_type === "wall" ||
203 | object._objects[0].custom_meta.object_type === "area" ) {
204 | object.setControlsVisibility({
205 | mt: true,
206 | mb: true,
207 | ml: true,
208 | mr: true,
209 | });
210 | };
211 | } else {
212 | object.set({
213 | 'lockMovementX': true,
214 | 'lockMovementY': true,
215 | 'lockRotation': true
216 | });
217 | object.setControlsVisibility({
218 | mtr: false,
219 | });
220 | if ( object._objects[0].custom_meta.object_type === "wall" ||
221 | object._objects[0].custom_meta.object_type === "area" ) {
222 | object.setControlsVisibility({
223 | mt: false,
224 | mb: false,
225 | ml: false,
226 | mr: false,
227 | });
228 | };
229 | };
230 | };
231 | canvas.renderAll();
232 | return;
233 | }
234 | window.lock_floorplan_object = lock_floorplan_object;
235 |
236 | function bring_forward() {
237 | var object = canvas.getActiveObject();
238 | if (object) {
239 | object.bringForward();
240 | canvas.renderAll();
241 | }
242 | }
243 | window.bring_forward = bring_forward;
244 |
245 | function send_back() {
246 | var object = canvas.getActiveObject();
247 | if (object) {
248 | object.sendBackwards();
249 | canvas.renderAll();
250 | }
251 | }
252 | window.send_back = send_back;
253 |
254 | function set_dimensions() {
255 | $('#control_unit_modal').modal('show');
256 | }
257 | function set_background() {
258 | $('#background_unit_modal').modal('show');
259 | }
260 |
261 | window.set_background = set_background;
262 | window.set_dimensions = set_dimensions;
263 |
264 | function add_text() {
265 | var object = new fabric.IText("Label", {
266 | fontFamily: "Courier New",
267 | left: 150,
268 | top: 100,
269 | fontSize: 12,
270 | textAlign: "left",
271 | fill: "#fff"
272 | });
273 | canvas.add(object);
274 | canvas.centerObject(object);
275 | }
276 | window.add_text = add_text;
277 |
278 | function add_floorplan_object(top, left, width, height, unit, fill, rotation, object_id, object_name, object_type, status, image) {
279 | var object_width;
280 | var object_height;
281 | if ( !width || !height || !unit ){
282 | object_width = 60;
283 | object_height = 91;
284 | } else {
285 | var conversion_scale = 100;
286 | console.log("width: " + width)
287 | console.log("unit: " + unit)
288 | console.log("height: " + height)
289 | if (unit == "in") {
290 | var new_width = (width * 0.0254) * conversion_scale;
291 | var new_height = (height * 0.0254) * conversion_scale;
292 | } else {
293 | var new_width = (width / 1000) * conversion_scale;
294 | var new_height = (height / 1000) * conversion_scale;
295 | }
296 |
297 | object_width = parseFloat(new_width.toFixed(2));
298 | console.log(object_width)
299 | object_height = parseFloat(new_height.toFixed(2));
300 | console.log(object_height)
301 | }
302 | document.getElementById(`object_${object_type}_${object_id}`).remove();
303 | /* if we have an image, we display the text below, otherwise we display the text within */
304 | var rect, text_offset = 0;
305 | if (!image) {
306 | rect = new fabric.Rect({
307 | top: top,
308 | name: "rectangle",
309 | left: left,
310 | width: object_width,
311 | height: object_height,
312 | fill: fill,
313 | opacity: 0.8,
314 | lockRotation: false,
315 | originX: "center",
316 | originY: "center",
317 | cornerSize: 15,
318 | hasRotatingPoint: true,
319 | perPixelTargetFind: true,
320 | minScaleLimit: 1,
321 | maxWidth: canvasWidth,
322 | maxHeight: canvasHeight,
323 | centeredRotation: true,
324 | custom_meta: {
325 | "object_type": object_type,
326 | "object_id": object_id,
327 | "object_name": object_name,
328 | "object_url": "/dcim/" + object_type + "s/" + object_id + "/",
329 | },
330 | });
331 | } else {
332 | object_height = object_width;
333 | text_offset = object_height/2 + 4;
334 | rect = new fabric.Image(null, {
335 | top: top,
336 | name: "rectangle",
337 | left: left,
338 | width: object_width,
339 | height: object_height,
340 | opacity: 1,
341 | lockRotation: false,
342 | originX: "center",
343 | originY: "center",
344 | cornerSize: 15,
345 | hasRotatingPoint: true,
346 | perPixelTargetFind: true,
347 | minScaleLimit: 1,
348 | maxWidth: canvasWidth,
349 | maxHeight: canvasHeight,
350 | centeredRotation: true,
351 | shadow: new fabric.Shadow({
352 | color: "red",
353 | blur: 15,
354 | }),
355 | custom_meta: {
356 | "object_type": object_type,
357 | "object_id": object_id,
358 | "object_name": object_name,
359 | "object_url": "/dcim/" + object_type + "s/" + object_id + "/",
360 | },
361 | });
362 | rect.setSrc("/media/" + image, function(img){
363 | img.scaleX = object_width / img.width;
364 | img.scaleY = object_height / img.height;
365 | canvas.renderAll();
366 | });
367 | }
368 |
369 | var text = new fabric.Textbox(object_name, {
370 | fontFamily: "Courier New",
371 | fontSize: 16,
372 | splitByGrapheme: text_offset? null : true,
373 | fill: "#FFFF",
374 | width: object_width,
375 | textAlign: "center",
376 | originX: "center",
377 | originY: "center",
378 | left: left,
379 | top: top + text_offset,
380 | excludeFromExport: false,
381 | includeDefaultValues: true,
382 | centeredRotation: true,
383 | stroke: "#000",
384 | strokeWidth: 2,
385 | paintFirst: 'stroke',
386 | custom_meta: {
387 | "text_type": "name",
388 | }
389 | });
390 |
391 | var button = new fabric.IText(status, {
392 | fontFamily: "Courier New",
393 | fontSize: 13,
394 | fill: "#6ea8fe",
395 | borderColor: "6ea8fe",
396 | textAlign: "center",
397 | originX: "center",
398 | originY: "center",
399 | left: left,
400 | top: top + text_offset + 16,
401 | excludeFromExport: false,
402 | includeDefaultValues: true,
403 | centeredRotation: true,
404 | shadow: text_offset? new fabric.Shadow({
405 | color: '#FFF',
406 | blur: 1
407 | }) : null,
408 | custom_meta: {
409 | "text_type": "status",
410 | }
411 | });
412 |
413 | var group = new fabric.Group([rect, text, button]);
414 | group.custom_meta = {
415 | "object_type": object_type,
416 | "object_id": object_id,
417 | "object_name": object_name,
418 | "object_url": "/dcim/" + object_type + "s/" + object_id + "/",
419 | }
420 | group.setControlsVisibility({
421 | mt: false,
422 | mb: false,
423 | ml: false,
424 | mr: false,
425 | bl: false,
426 | br: false,
427 | tl: false,
428 | tr: false,
429 | })
430 |
431 | if (object_id) {
432 | group.set('id', object_id);
433 | }
434 |
435 | canvas.add(group);
436 | canvas.centerObject(group);
437 | //canvas.bringToFront(group);
438 | }
439 | window.add_floorplan_object = add_floorplan_object;
440 |
441 | function delete_floorplan_object() {
442 | var object = canvas.getActiveObject();
443 | if (object) {
444 | canvas.remove(object);
445 | canvas.renderAll();
446 | }
447 | save_floorplan();
448 | setTimeout(() => {
449 | htmx.ajax('GET', `/plugins/floorplan/floorplans/racks/?floorplan_id=${obj_pk}`, { target: '#rack-card', swap: 'innerHTML' });
450 | htmx.ajax('GET', `/plugins/floorplan/floorplans/devices/?floorplan_id=${obj_pk}`, { target: '#unrack-card', swap: 'innerHTML' });
451 | }, 1500);
452 | };
453 | window.delete_floorplan_object = delete_floorplan_object;
454 |
455 | function set_color(color) {
456 | var object = canvas.getActiveObject();
457 | if (object) {
458 | if (object.type == "i-text") {
459 | object.set('fill', color);
460 | canvas.renderAll();
461 | return;
462 | }
463 | object._objects[0].set('fill', color);
464 | canvas.renderAll();
465 | return;
466 | }
467 | }
468 | window.set_color = set_color;
469 |
470 | function set_zoom(new_current_zoom) {
471 | current_zoom = new_current_zoom;
472 | canvas.setZoom(current_zoom);
473 | canvas.requestRenderAll()
474 | document.getElementById("zoom").value = current_zoom;
475 | }
476 | window.set_zoom = set_zoom;
477 |
478 | function center_pan_on_slected_object() {
479 | let pan_x = 0
480 | let pan_y = 0
481 | let object = canvas.getActiveObject()
482 | let obj_wdth = object.getScaledWidth()
483 | let obj_hgt = object.getScaledHeight()
484 | let rect_cooords = object.getBoundingRect();
485 | let zoom_level = Math.min(canvas.width / rect_cooords.width, canvas.height / rect_cooords.height);
486 |
487 | canvas.setZoom(zoom_level * 0.7);
488 | let zoom = canvas.getZoom()
489 | pan_x = ((canvas.getWidth() / zoom / 2) - (object.aCoords.tl.x) - (obj_wdth / 2)) * zoom
490 | pan_y = ((canvas.getHeight() / zoom / 2) - (object.aCoords.tl.y) - (obj_hgt / 2)) * zoom
491 | pan_x = (canvas.getVpCenter().x - object.getCenterPoint().x) * zoom
492 | pan_y = ((canvas.getVpCenter().y - object.getCenterPoint().y) * zoom)
493 | canvas.relativePan({ x: pan_x, y: pan_y })
494 | canvas.requestRenderAll()
495 |
496 | }
497 | window.center_pan_on_slected_object = center_pan_on_slected_object;
498 |
499 | // end buttons ----------------------------------------------------------------------------- !
500 |
501 | // start set scale ----------------------------------------------------------------------------- !
502 |
503 | function update_background() {
504 | var assigned_image = document.getElementById("id_assigned_image").value;
505 | if (assigned_image == "") {
506 | assigned_image = null;
507 | canvas.setBackgroundImage(null, canvas.renderAll.bind(canvas));
508 | }
509 | var floor_json = canvas.toJSON(["id", "text", "_controlsVisibility", "custom_meta", "lockMovementY", "lockMovementX", "evented", "selectable"]);
510 |
511 |
512 |
513 |
514 |
515 | $.ajax({
516 | type: "PATCH",
517 | url: `/api/plugins/floorplan/floorplans/${obj_pk}/`,
518 | dataType: "json",
519 | headers: {
520 | "X-CSRFToken": csrf,
521 | "Content-Type": "application/json"
522 | },
523 | data: JSON.stringify({
524 | "assigned_image": assigned_image,
525 | "canvas": floor_json
526 | }),
527 | error: function (err) {
528 | console.log(`Error: ${err}`);
529 | }
530 | }).done(function (floorplan) {
531 | if (floorplan.assigned_image != null) {
532 | var img_url = "";
533 | if (floorplan.assigned_image.external_url != "") {
534 | img_url = floorplan.assigned_image.external_url;
535 | } else {
536 | img_url = floorplan.assigned_image.file;
537 | }
538 |
539 | var img = fabric.Image.fromURL(img_url, function(img) {
540 |
541 |
542 | var left = 0;
543 | var top = 0;
544 | var width = 0;
545 | var height = 0;
546 | canvas.getObjects().forEach(function (object) {
547 | if (object.custom_meta) {
548 | if (object.custom_meta.object_type == "floorplan_boundry") {
549 | left = object.left;
550 | top = object.top;
551 | width = object.width;
552 | height = object.height;
553 | }
554 | }
555 | });
556 | // if we have a floorplan boundary, position the image in there
557 | if (height != 0 && width != 0) {
558 | let scaleRatioX = Math.max(width / img.width)
559 | let scaleRatioY = Math.max(height / img.height);
560 | canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {
561 | scaleX: scaleRatioX,
562 | scaleY: scaleRatioY,
563 | left: left,
564 | top: top
565 | });
566 | }
567 | else
568 | {
569 | let scaleRatio = Math.max(canvas.width / img.width, canvas.height / img.height);
570 | canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {
571 | scaleX: scaleRatio,
572 | scaleY: scaleRatio,
573 | left: canvas.width / 2,
574 | top: canvas.height / 2,
575 | originX: 'middle',
576 | originY: 'middle'
577 | });
578 | }
579 | });
580 |
581 | } else {
582 | canvas.setBackgroundImage().renderAll();
583 | }
584 | canvas.renderAll();
585 | $('#background_unit_modal').modal('hide');
586 | });
587 | }
588 |
589 | window.update_background = update_background;
590 |
591 | function update_dimensions() {
592 |
593 | var width = document.getElementById("width_value").value;
594 | var height = document.getElementById("height_value").value;
595 |
596 | var measurement_unit = document.getElementById("measurement_unit").value;
597 |
598 | var conversion_scale = 100;
599 | if (measurement_unit == "ft") {
600 | var new_width = (width / 3.28) * conversion_scale;
601 | var new_height = (height / 3.28) * conversion_scale;
602 | } else {
603 | var new_width = width * conversion_scale;
604 | var new_height = height * conversion_scale;
605 | }
606 |
607 | var rounded_width = parseFloat(new_width.toFixed(2));
608 | var rounded_height = parseFloat(new_height.toFixed(2));
609 |
610 | var floor_json = canvas.toJSON(["id", "text", "_controlsVisibility", "custom_meta", "lockMovementY", "lockMovementX", "evented", "selectable"]);
611 | $.ajax({
612 | type: "PATCH",
613 | url: `/api/plugins/floorplan/floorplans/${obj_pk}/`,
614 | dataType: "json",
615 | headers: {
616 | "X-CSRFToken": csrf,
617 | "Content-Type": "application/json"
618 | },
619 | data: JSON.stringify({
620 | "width": rounded_width,
621 | "height": rounded_height,
622 | "measurement_unit": measurement_unit,
623 | "canvas": floor_json,
624 | }),
625 | error: function (err) {
626 | console.log(`Error: ${err}`);
627 | }
628 | }).done(function () {
629 |
630 | // set the boundry variables for zoom controls
631 | var center_x = rounded_width / 2;
632 | var center_y = rounded_height / 2;
633 |
634 | var rect_left = center_x;
635 | var rect_top = center_y;
636 | var rect_bottom = rounded_height;
637 |
638 | var rect = new fabric.Rect({
639 | top: rect_top,
640 | name: "rectangle",
641 | left: rect_left,
642 | width: rounded_width,
643 | height: rounded_height,
644 | fill: null,
645 | opacity: 1,
646 | stroke: "#6ea8fe",
647 | strokeWidth: 2,
648 | lockRotation: false,
649 | originX: "center",
650 | originY: "center",
651 | cornerSize: 15,
652 | hasRotatingPoint: true,
653 | perPixelTargetFind: true,
654 | minScaleLimit: 1,
655 | maxWidth: canvasWidth,
656 | maxHeight: canvasHeight,
657 | centeredRotation: true,
658 | });
659 |
660 |
661 | var text = new fabric.IText(`${obj_name}`, {
662 | fontFamily: "Courier New",
663 | fontSize: 16,
664 | fill: "#FFFF",
665 | textAlign: "center",
666 | originX: "center",
667 | originY: "center",
668 | left: rect_left,
669 | top: rect_bottom - 40,
670 | excludeFromExport: false,
671 | includeDefaultValues: true,
672 | centeredRotation: true,
673 | });
674 |
675 | var dimensions = new fabric.IText(`${width} ${measurement_unit} (width) x ${height} ${measurement_unit} (height)`, {
676 | fontFamily: "Courier New",
677 | fontSize: 8,
678 | fill: "#FFFF",
679 | textAlign: "center",
680 | originX: "center",
681 | originY: "center",
682 | left: rect_left,
683 | top: rect_bottom - 20,
684 | excludeFromExport: false,
685 | includeDefaultValues: true,
686 | centeredRotation: true,
687 | });
688 |
689 | // check if the canvas already has a floorplan boundry
690 | var current_angle = 0;
691 | canvas.getObjects().forEach(function (object) {
692 | if (object.custom_meta) {
693 | if (object.custom_meta.object_type == "floorplan_boundry") {
694 | current_angle = object.angle;
695 | canvas.remove(object);
696 | }
697 | }
698 | });
699 |
700 | var group = new fabric.Group([rect, text, dimensions]);
701 | group.angle = current_angle;
702 | group.lockMovementY = true;
703 | group.lockMovementX = true;
704 | group.selectable = false;
705 | group.evented = false;
706 | group.setControlsVisibility({
707 | mt: false,
708 | mb: false,
709 | ml: false,
710 | mr: false,
711 | bl: false,
712 | br: false,
713 | tl: false,
714 | tr: false,
715 | })
716 | group.set('custom_meta', {
717 | "object_type": "floorplan_boundry",
718 | });
719 | canvas.add(group);
720 | //canvas.setDimensions({ width: rounded_width, height: rounded_height }, { cssOnly: true });
721 | canvas.renderAll();
722 | save_floorplan();
723 | set_zoom(1);
724 | $('#control_unit_modal').modal('hide');
725 | });
726 | };
727 | window.update_dimensions = update_dimensions;
728 |
729 | // end set scale ----------------------------------------------------------------------------- !
730 |
731 | // start keyboard/mouse controls ----------------------------------------------------------------------------- !
732 |
733 | function move_active_object(x, y) {
734 | var object = canvas.getActiveObject();
735 | if (object) {
736 | object.set({
737 | left: object.left + x,
738 | top: object.top + y
739 | });
740 | canvas.renderAll();
741 | }
742 | }
743 |
744 | function rotate_active_object(angle) {
745 | var object = canvas.getActiveObject();
746 | if (object) {
747 | object.rotate(object.angle + angle);
748 | canvas.renderAll();
749 | }
750 | }
751 |
752 | // key down events for object control
753 | document.addEventListener('keydown', function (e) {
754 | // delete key
755 | if (e.keyCode == 46) {
756 | delete_floorplan_object();
757 | }
758 | // events for arrows to move active object
759 | if (e.keyCode == 37) {
760 | move_active_object(-5, 0);
761 | } else if (e.keyCode == 38) {
762 | move_active_object(0, -5);
763 | } else if (e.keyCode == 39) {
764 | move_active_object(5, 0);
765 | } else if (e.keyCode == 40) {
766 | move_active_object(0, 5);
767 | }
768 | // when shift and arrow is pressed, rotate active object
769 | if (e.shiftKey && e.keyCode == 37) {
770 | rotate_active_object(-45);
771 | } else if (e.shiftKey && e.keyCode == 39) {
772 | rotate_active_object(45);
773 | }
774 | });
775 |
776 |
777 | // end keyboard/mouse controls ----------------------------------------------------------------------------- !
778 |
779 | // start save floorplan ----------------------------------------------------------------------------- !
780 |
781 | function save_floorplan() {
782 | var floor_json = canvas.toJSON(["id", "text", "_controlsVisibility", "custom_meta", "lockMovementY", "lockMovementX", "evented", "selectable"]);
783 | $.ajax({
784 | type: "PATCH",
785 | url: `/api/plugins/floorplan/floorplans/${obj_pk}/`,
786 | dataType: "json",
787 | headers: {
788 | "X-CSRFToken": csrf,
789 | "Content-Type": "application/json"
790 | },
791 | data: JSON.stringify({
792 | "canvas": floor_json,
793 | }),
794 | error: function (err) {
795 | console.log(`Error: ${err}`);
796 | }
797 | });
798 | }
799 |
800 | function save_and_redirect() {
801 | var floor_json = canvas.toJSON(["id", "text", "_controlsVisibility", "custom_meta", "lockMovementY", "lockMovementX", "evented", "selectable"]);
802 | $.ajax({
803 | type: "PATCH",
804 | url: `/api/plugins/floorplan/floorplans/${obj_pk}/`,
805 | dataType: "json",
806 | headers: {
807 | "X-CSRFToken": csrf,
808 | "Content-Type": "application/json"
809 | },
810 | data: JSON.stringify({
811 | "canvas": floor_json,
812 | }),
813 | error: function (err) {
814 | console.log(`Error: ${err}`);
815 | }
816 | }).done(function () {
817 | if (record_type == "site") {
818 | window.location.href = `/dcim/sites/${site_id}/floorplans/`;
819 | } else {
820 | window.location.href = `/dcim/locations/${location_id}/floorplans/`;
821 | }
822 | });
823 | }
824 |
825 |
826 |
827 | window.save_and_redirect = save_and_redirect;
828 | // end save floorplan ----------------------------------------------------------------------------- !
829 |
830 | // start initialize load ----------------------------------------------------------------------------- !
831 | document.addEventListener("DOMContentLoaded", init_floor_plan(obj_pk, canvas, "edit"));
832 | // end initialize load ----------------------------------------------------------------------------- !
833 |
--------------------------------------------------------------------------------
/netbox_floorplan/static/netbox_floorplan/floorplan/utils.js:
--------------------------------------------------------------------------------
1 | export {
2 | resize_canvas,
3 | export_svg,
4 | enable_button_selection,
5 | disable_button_selection,
6 | prevent_leaving_canvas,
7 | wheel_zoom,
8 | reset_zoom,
9 | stop_pan,
10 | start_pan,
11 | move_pan,
12 | init_floor_plan
13 | };
14 |
15 |
16 | function resize_canvas(canvas, window) {
17 | var bob_width = $("#content-container").width();
18 | var window_width = $(window).width();
19 | window_width = Math.min(window_width, bob_width);
20 | var window_height = $(window).height();
21 | var canvas_width = window_width;
22 | var canvas_height = window_height - 100;
23 | canvas.setWidth(canvas_width);
24 | canvas.setHeight(canvas_height);
25 | // canvas.backgroundImage.scaleToWidth(canvas_width);
26 | // canvas.backgroundImage.scaleToHeight(canvas_height);
27 | canvas.renderAll();
28 | }
29 |
30 | function reset_zoom(canvas) {
31 |
32 | var objs = canvas.getObjects();
33 | for (var i = 0; i < objs.length; i++) {
34 | if (objs[i].custom_meta) {
35 | if (objs[i].custom_meta.object_type == "floorplan_boundry") {
36 | canvas.setActiveObject(objs[i]);
37 | let pan_x = 0
38 | let pan_y = 0
39 | let object = canvas.getActiveObject()
40 | let obj_wdth = object.getScaledWidth()
41 | let obj_hgt = object.getScaledHeight()
42 | let rect_cooords = object.getBoundingRect();
43 | let zoom_level = Math.min(canvas.width / rect_cooords.width, canvas.height / rect_cooords.height);
44 |
45 | canvas.setZoom(zoom_level * 0.7);
46 | let zoom = canvas.getZoom()
47 | pan_x = ((canvas.getWidth() / zoom / 2) - (object.aCoords.tl.x) - (obj_wdth / 2)) * zoom
48 | pan_y = ((canvas.getHeight() / zoom / 2) - (object.aCoords.tl.y) - (obj_hgt / 2)) * zoom
49 | pan_x = (canvas.getVpCenter().x - object.getCenterPoint().x) * zoom
50 | pan_y = ((canvas.getVpCenter().y - object.getCenterPoint().y) * zoom)
51 | canvas.relativePan({ x: pan_x, y: pan_y })
52 | canvas.requestRenderAll()
53 | canvas.discardActiveObject();
54 | }
55 | }
56 | }
57 | }
58 |
59 | function export_svg(canvas) {
60 | var filedata = canvas.toSVG();
61 | var locfile = new Blob([filedata], { type: "image/svg+xml;charset=utf-8" });
62 | var locfilesrc = URL.createObjectURL(locfile);
63 | var link = document.createElement('a');
64 | link.style.display = 'none';
65 | link.href = locfilesrc;
66 | link.download = "floorplan.svg";
67 | link.click();
68 | }
69 |
70 | function enable_button_selection() {
71 | document.getElementById("selected_color").value = "#000000";
72 | $(".tools").removeClass("disabled");
73 | }
74 |
75 | function disable_button_selection() {
76 | // set color to default
77 | document.getElementById("selected_color").value = "#000000";
78 | $(".tools").addClass("disabled");
79 | }
80 |
81 | function prevent_leaving_canvas(e, canvas) {
82 | var obj = e.target;
83 | obj.setCoords();
84 | var current_zoom = obj.canvas.getZoom();
85 | if (obj.getScaledHeight() > obj.canvas.height || obj.getScaledWidth() > obj.canvas.width) {
86 | return;
87 | }
88 | if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) {
89 | obj.top = Math.max(obj.top * current_zoom, obj.top * current_zoom - obj.getBoundingRect().top) / current_zoom;
90 | obj.left = Math.max(obj.left * current_zoom, obj.left * current_zoom - obj.getBoundingRect().left) / current_zoom;
91 | }
92 | if (obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height || obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width) {
93 | obj.top = Math.min(obj.top * current_zoom, obj.canvas.height - obj.getBoundingRect().height + obj.top * current_zoom - obj.getBoundingRect().top) / current_zoom;
94 | obj.left = Math.min(obj.left * current_zoom, obj.canvas.width - obj.getBoundingRect().width + obj.left * current_zoom - obj.getBoundingRect().left) / current_zoom;
95 | }
96 | };
97 |
98 |
99 | function wheel_zoom(opt, canvas) {
100 | var delta = opt.e.deltaY;
101 | var zoom = canvas.getZoom();
102 | zoom *= 0.999 ** delta;
103 | if (zoom > 20) zoom = 20;
104 | if (zoom < 0.01) zoom = 0.01;
105 | canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
106 | opt.e.preventDefault();
107 | opt.e.stopPropagation();
108 | }
109 |
110 | function stop_pan(canvas) {
111 | canvas.setViewportTransform(canvas.viewportTransform);
112 | canvas.isDragging = false;
113 | canvas.selection = true;
114 | }
115 |
116 | function start_pan(opt, canvas) {
117 | var evt = opt.e;
118 | if (evt.altKey === true) {
119 | canvas.isDragging = true;
120 | canvas.selection = false;
121 | canvas.lastPosX = evt.clientX;
122 | canvas.lastPosY = evt.clientY;
123 | }
124 | }
125 |
126 | function move_pan(opt, canvas) {
127 | if (canvas.isDragging) {
128 | var e = opt.e;
129 | var vpt = canvas.viewportTransform;
130 | vpt[4] += e.clientX - canvas.lastPosX;
131 | vpt[5] += e.clientY - canvas.lastPosY;
132 | canvas.requestRenderAll();
133 | canvas.lastPosX = e.clientX;
134 | canvas.lastPosY = e.clientY;
135 | }
136 | }
137 |
138 |
139 |
140 |
141 | function init_floor_plan(floorplan_id, canvas, mode) {
142 |
143 | if (floorplan_id === undefined || floorplan_id === null || floorplan_id === "") {
144 | return;
145 | }
146 |
147 | var target_image = 0;
148 | const floorplan_call = $.get(`/api/plugins/floorplan/floorplans/?id=${floorplan_id}`);
149 | floorplan_call.done(function (floorplan) {
150 | floorplan.results.forEach((floorplan) => {
151 | target_image = floorplan.assigned_image
152 | canvas.loadFromJSON(JSON.stringify(floorplan.canvas), canvas.renderAll.bind(canvas), function (o, object) {
153 | if (mode == "readonly") {
154 | object.set('selectable', false);
155 | }
156 | if (floorplan.assigned_image != null) {
157 | var img_url = "";
158 | if (floorplan.assigned_image.external_url != "") {
159 | img_url = floorplan.assigned_image.external_url;
160 | } else {
161 | img_url = floorplan.assigned_image.file;
162 | }
163 |
164 |
165 | var img = fabric.Image.fromURL(img_url, function(img) {
166 | var left = 0;
167 | var top = 0;
168 | var width = 0;
169 | var height = 0;
170 | canvas.getObjects().forEach(function (object) {
171 | if (object.custom_meta) {
172 | if (object.custom_meta.object_type == "floorplan_boundry") {
173 | left = object.left;
174 | top = object.top;
175 | width = object.width;
176 | height = object.height;
177 | }
178 | }
179 | });
180 | // if we have a floorplan boundary, position the image in there
181 | if (height != 0 && width != 0) {
182 | let scaleRatioX = Math.max(width / img.width)
183 | let scaleRatioY = Math.max(height / img.height);
184 | canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {
185 | scaleX: scaleRatioX,
186 | scaleY: scaleRatioY,
187 | left: left,
188 | top: top
189 | });
190 | }
191 | else
192 | {
193 | let scaleRatio = Math.max(canvas.width / img.width, canvas.height / img.height);
194 | canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {
195 | scaleX: scaleRatio,
196 | scaleY: scaleRatio,
197 | left: canvas.width / 2,
198 | top: canvas.height / 2,
199 | originX: 'middle',
200 | originY: 'middle'
201 | });
202 | }
203 | });
204 |
205 |
206 | } else {
207 | canvas.setBackgroundImage().renderAll();
208 | }
209 | canvas.renderAll();
210 | });
211 | });
212 | reset_zoom(canvas);
213 | resize_canvas(canvas, window);
214 | }).fail(function (jq_xhr, text_status, error_thrown) {
215 | console.log(`error: ${error_thrown} - ${text_status}`);
216 | });
217 | };
--------------------------------------------------------------------------------
/netbox_floorplan/static/netbox_floorplan/floorplan/view.js:
--------------------------------------------------------------------------------
1 | import {
2 | resize_canvas,
3 | export_svg,
4 | wheel_zoom,
5 | stop_pan,
6 | move_pan,
7 | start_pan,
8 | reset_zoom,
9 | init_floor_plan
10 | } from "/static/netbox_floorplan/floorplan/utils.js";
11 |
12 | var canvas = new fabric.Canvas('canvas');
13 |
14 | canvas.on('mouse:over', function (options) {
15 | if (options.target) {
16 | if (options.target.hasOwnProperty("custom_meta")) {
17 | options.target.hoverCursor = "pointer";
18 | }
19 |
20 | }
21 | });
22 |
23 | canvas.on('mouse:down', function (options) {
24 | if (options.target) {
25 | if (options.target.hasOwnProperty("custom_meta")) {
26 | window.location.href = options.target.custom_meta.object_url;
27 | }
28 | }
29 | });
30 |
31 | // start zoom, pan control & resizing ----------------------------------------------------------------------------- !
32 |
33 | $(window).resize(resize_canvas(canvas, window));
34 |
35 | canvas.on('mouse:wheel', function (opt) {
36 | wheel_zoom(opt, canvas);
37 | });
38 |
39 | canvas.on('mouse:down', function (opt) {
40 | start_pan(opt, canvas);
41 | });
42 |
43 | canvas.on('mouse:move', function (opt) {
44 | move_pan(opt, canvas);
45 | });
46 | canvas.on('mouse:up', function (opt) {
47 | stop_pan(canvas);
48 | });
49 |
50 | // end start zoom, pan control & resizing ----------------------------------------------------------------------------- !
51 |
52 |
53 | document.getElementById('export_svg').addEventListener('click', () => {
54 | export_svg(canvas);
55 | });
56 |
57 |
58 | let floorplan_id = document.getElementById('floorplan_id').value;
59 | document.addEventListener("DOMContentLoaded", init_floor_plan(floorplan_id, canvas, "readonly"));
60 |
--------------------------------------------------------------------------------
/netbox_floorplan/static/netbox_floorplan/vendors/htmx.min.js:
--------------------------------------------------------------------------------
1 | (function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:F,process:zt,on:de,off:ge,trigger:ce,ajax:Nr,find:C,findAll:f,closest:v,values:function(e,t){var r=dr(e,t||"post");return r.values},remove:_,addClass:z,removeClass:n,toggleClass:$,takeClass:W,defineExtension:Ur,removeExtension:Br,logAll:V,logNone:j,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null},parseInterval:d,_:t,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.12"};var r={addTriggerHandler:Lt,bodyContains:se,canAccessLocalStorage:U,findThisElement:xe,filterValues:yr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Hr,getHeaders:xr,getInputValues:dr,getInternalData:ae,getSwapSpecification:wr,getTriggerSpecs:it,getTarget:ye,makeFragment:l,mergeObjects:le,makeSettleInfo:T,oobSwap:Ee,querySelectorExt:ue,selectAndSwap:je,settleImmediately:nr,shouldCancel:ut,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:R};var w=["get","post","put","delete","patch"];var i=w.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");var S=e("head"),q=e("title"),H=e("svg",true);function e(e,t){return new RegExp("<"+e+"(\\s[^>]*>|>)([\\s\\S]*?)<\\/"+e+">",!!t?"gim":"im")}function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function L(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=L(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function A(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function s(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function N(e){return/
Host | Free Ports |
---|---|
{{ host }} | 28 |{{ obj }} | 29 |
Name | 17 |{{ object.name }} | 18 |
---|
id | 15 |{{ object.id }} | 16 |
---|---|
Name | 19 |{{ object.name|placeholder }} | 20 |
External URL | 24 |{{ object.external_url }} | 25 |
Filename | 29 |{{ object.filename }} | 30 |
Size | 33 |{{ object.size|filesizeformat }} | 34 |
3 | 8 |
9 |Action | 16 |Description | 17 |
---|---|
Pan 22 | ALT + LEFT MOUSE CLICK + DRAG 23 | |
24 | Pans window | 25 |
Zoom in/out 28 | MOUSE WHEEL IN/OUT 29 | |
30 | Zooms in/out | 31 |
Move Object (Down/Up/Left/Right) 34 | (Down/Up/Left/Right) Arrow 35 | |
36 | Moves Selected in the given direction | 37 |
Rotate Object (Down/Up/Left/Right) 40 | SHIFT + (Down/Up/Left/Right) Arrow 41 | |
42 | Rotates Selected in the given direction | 43 |
Delete Object 46 | DEL 47 | |
48 | Deletes Selected Object | 49 |