Skip to content

Commit ed228aa

Browse files
authored
Save and Load Topology View (#46)
A Topology Viewer page (plugins/nextbox-ui/topology/) now allows you to save a current layout to the database. A saved topology is a "snapshot" of the topology state on the screen with all associated attributes. A saved topology can then be loaded from the Topology Viewer page. Overall device layout and their relative coordinates will be preserved. General plugin Hide/Display controls are also preserved on the view save/load. Device and link changes taken between save and load actions are not currently considered. Load Saved View shows a topology state in the past as is. Tracking changes is a matter of further development. * Save Topology View feature initial commit Add general workflow for current layout saving. Loading from the created snapshot is not yet implemented. * Implement Load Saved View It is possible to load a saved topology from on a Topology Viewer page.
1 parent 21693e4 commit ed228aa

File tree

12 files changed

+265
-6
lines changed

12 files changed

+265
-6
lines changed

nextbox_ui_plugin/admin.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from django.contrib import admin
2+
from .models import SavedTopology
3+
4+
5+
@admin.register(SavedTopology)
6+
class SavedTopologyAdmin(admin.ModelAdmin):
7+
list_display = ("name", "created_by", "timestamp", "topology",)

nextbox_ui_plugin/api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""REST API for NextBox-UI Plugin"""
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from rest_framework import serializers
2+
from nextbox_ui_plugin.models import SavedTopology
3+
import datetime
4+
import json
5+
6+
7+
class SavedTopologySerializer(serializers.ModelSerializer):
8+
9+
created_by = serializers.CharField(read_only=True)
10+
timestamp = serializers.DateTimeField(read_only=True)
11+
12+
def to_internal_value(self, data):
13+
validated = {
14+
'name': str(data.get('name').strip() or f"{self.context['request'].user} - {datetime.datetime.now()}"),
15+
'topology': json.loads(data.get('topology')),
16+
'layout_context': json.loads(data.get('layout_context')),
17+
'created_by': self.context['request'].user,
18+
'timestamp': str(datetime.datetime.now())
19+
}
20+
return validated
21+
22+
class Meta:
23+
model = SavedTopology
24+
fields = [
25+
"id", "name", "topology", "layout_context", "created_by", "timestamp",
26+
]

nextbox_ui_plugin/api/urls.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from rest_framework.routers import DefaultRouter
2+
from . import views
3+
4+
5+
router = DefaultRouter()
6+
router.APIRootView = views.NextBoxUIPluginRootView
7+
8+
router.register(r'savedtopologies', views.SavedTopologyViewSet)
9+
10+
app_name = "nextbox_ui_plugin-api"
11+
urlpatterns = router.urls

nextbox_ui_plugin/api/views.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from rest_framework.routers import APIRootView
2+
from rest_framework.viewsets import ModelViewSet
3+
from nextbox_ui_plugin.models import SavedTopology
4+
from . import serializers
5+
6+
7+
class NextBoxUIPluginRootView(APIRootView):
8+
"""
9+
NextBoxUI_plugin API root view
10+
"""
11+
def get_view_name(self):
12+
return 'NextBoxUI'
13+
14+
15+
class SavedTopologyViewSet(ModelViewSet):
16+
queryset = SavedTopology.objects.all()
17+
serializer_class = serializers.SavedTopologySerializer

nextbox_ui_plugin/forms.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from utilities.forms import (
33
BootstrapMixin, DynamicModelMultipleChoiceField,
44
)
5+
from .models import SavedTopology
56
from dcim.models import Device, Site, Region
67

78

@@ -27,3 +28,19 @@ class TopologyFilterForm(BootstrapMixin, forms.Form):
2728
to_field_name='id',
2829
null_option='None',
2930
)
31+
32+
33+
class LoadSavedTopologyFilterForm(BootstrapMixin, forms.Form):
34+
35+
def __init__(self, *args, **kwargs):
36+
user = kwargs.pop('user', None)
37+
super(LoadSavedTopologyFilterForm, self).__init__(*args, **kwargs)
38+
self.fields['saved_topology_id'].queryset = SavedTopology.objects.filter(created_by=user)
39+
40+
model = SavedTopology
41+
42+
saved_topology_id = forms.ModelChoiceField(
43+
queryset=None,
44+
to_field_name='id',
45+
required=True
46+
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from django.db import migrations, models
2+
import django.db.models.deletion
3+
4+
5+
class Migration(migrations.Migration):
6+
7+
initial = True
8+
9+
dependencies = [
10+
('users', '0010_update_jsonfield'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='SavedTopology',
16+
fields=[
17+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
18+
('name', models.CharField(blank=True, max_length=100)),
19+
('topology', models.JSONField()),
20+
('layout_context', models.JSONField(blank=True, null=True)),
21+
('timestamp', models.DateTimeField()),
22+
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.adminuser')),
23+
],
24+
),
25+
]

nextbox_ui_plugin/migrations/__init__.py

Whitespace-only changes.

nextbox_ui_plugin/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from django.db import models
2+
from utilities.querysets import RestrictedQuerySet
3+
4+
5+
class SavedTopology(models.Model):
6+
7+
name = models.CharField(max_length=100, blank=True)
8+
topology = models.JSONField()
9+
layout_context = models.JSONField(null=True, blank=True)
10+
created_by = models.ForeignKey(
11+
to="users.AdminUser",
12+
on_delete=models.CASCADE,
13+
blank=False,
14+
null=False,
15+
)
16+
timestamp = models.DateTimeField()
17+
18+
objects = RestrictedQuerySet.as_manager()
19+
20+
def __str__(self):
21+
return str(self.name)

nextbox_ui_plugin/static/nextbox_ui_plugin/next_app.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,41 @@
347347
displayPassiveDevices = !displayPassiveDevices
348348
};
349349

350+
saveView = function (topoSaveURI, CSRFToken) {
351+
var topoSaveName = document.getElementById('topoSaveName').value.trim();
352+
var saveButton = document.getElementById('saveTopologyView');
353+
var saveResultLabel = document.getElementById('saveResult');
354+
saveButton.setAttribute('disabled', true);
355+
saveResultLabel.setAttribute('innerHTML', 'Processing');
356+
$.ajax({
357+
type: 'POST',
358+
url: topoSaveURI,
359+
data: {
360+
'name': topoSaveName,
361+
'topology': JSON.stringify(topo.data()),
362+
'layout_context': JSON.stringify({
363+
'initialLayout': initialLayout,
364+
'displayUnconnected': !displayUnconnected,
365+
'undisplayedRoles': undisplayedRoles,
366+
'undisplayedDeviceTags': undisplayedDeviceTags,
367+
'displayPassiveDevices': !displayPassiveDevices,
368+
'displayLogicalMultiCableLinks': displayLogicalMultiCableLinks,
369+
'requestGET': requestGET,
370+
})
371+
},
372+
headers: {'X-CSRFToken': CSRFToken},
373+
success: function (response) {
374+
saveResultLabel.innerHTML = 'Success';
375+
saveButton.removeAttribute('disabled');
376+
console.log(response);
377+
},
378+
error: function (response) {
379+
saveResultLabel.innerHTML = 'Failed';
380+
console.log(response);
381+
}
382+
})
383+
};
384+
350385
topo.on('topologyGenerated', function(){
351386
showHideUndonnected();
352387
showHidePassiveDevices();
@@ -360,6 +395,9 @@
360395
undisplayedDeviceTags.forEach(function(deviceTag){
361396
showHideDevicesByTag(deviceTag, false);
362397
});
398+
topologySavedData['nodes'].forEach(function(node){
399+
topo.graph().getVertex(node['id']).position({'x': node.x, 'y': node.y});
400+
});
363401
});
364402

365403
// Create an application instance

0 commit comments

Comments
 (0)