mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Introduce Dashboard model to store user dashboard layout & config
This commit is contained in:
parent
e9e0738d5d
commit
892fce1518
@ -34,6 +34,7 @@ __all__ = (
|
|||||||
'ContentTypeSerializer',
|
'ContentTypeSerializer',
|
||||||
'CustomFieldSerializer',
|
'CustomFieldSerializer',
|
||||||
'CustomLinkSerializer',
|
'CustomLinkSerializer',
|
||||||
|
'DashboardSerializer',
|
||||||
'ExportTemplateSerializer',
|
'ExportTemplateSerializer',
|
||||||
'ImageAttachmentSerializer',
|
'ImageAttachmentSerializer',
|
||||||
'JobResultSerializer',
|
'JobResultSerializer',
|
||||||
@ -563,3 +564,13 @@ class ContentTypeSerializer(BaseModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ContentType
|
model = ContentType
|
||||||
fields = ['id', 'url', 'display', 'app_label', 'model']
|
fields = ['id', 'url', 'display', 'app_label', 'model']
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# User dashboard
|
||||||
|
#
|
||||||
|
|
||||||
|
class DashboardSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Dashboard
|
||||||
|
fields = ('layout', 'config')
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
from netbox.api.routers import NetBoxRouter
|
from netbox.api.routers import NetBoxRouter
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
@ -22,4 +24,7 @@ router.register('job-results', views.JobResultViewSet)
|
|||||||
router.register('content-types', views.ContentTypeViewSet)
|
router.register('content-types', views.ContentTypeViewSet)
|
||||||
|
|
||||||
app_name = 'extras-api'
|
app_name = 'extras-api'
|
||||||
urlpatterns = router.urls
|
urlpatterns = [
|
||||||
|
path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
|
||||||
|
path('', include(router.urls)),
|
||||||
|
]
|
||||||
|
@ -4,6 +4,7 @@ from django_rq.queues import get_connection
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
from rest_framework.generics import RetrieveUpdateDestroyAPIView
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -423,3 +424,15 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
|
|||||||
queryset = ContentType.objects.order_by('app_label', 'model')
|
queryset = ContentType.objects.order_by('app_label', 'model')
|
||||||
serializer_class = serializers.ContentTypeSerializer
|
serializer_class = serializers.ContentTypeSerializer
|
||||||
filterset_class = filtersets.ContentTypeFilterSet
|
filterset_class = filtersets.ContentTypeFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# User dashboard
|
||||||
|
#
|
||||||
|
|
||||||
|
class DashboardView(RetrieveUpdateDestroyAPIView):
|
||||||
|
queryset = Dashboard.objects.all()
|
||||||
|
serializer_class = serializers.DashboardSerializer
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return Dashboard.objects.filter(user=self.request.user).first()
|
||||||
|
@ -2,6 +2,7 @@ import uuid
|
|||||||
|
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from extras.constants import DEFAULT_DASHBOARD
|
from extras.constants import DEFAULT_DASHBOARD
|
||||||
|
from extras.models import Dashboard
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'get_dashboard',
|
'get_dashboard',
|
||||||
@ -22,8 +23,8 @@ def register_widget(cls):
|
|||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
def get_widget_class_and_config(user, id):
|
def get_widget_class_and_config(dashboard, id):
|
||||||
config = dict(user.config.get(f'dashboard.widgets.{id}')) # Copy to avoid mutating userconfig data
|
config = dict(dashboard.config[id]) # Copy to avoid mutating userconfig data
|
||||||
widget_class = registry['widgets'].get(config.pop('class'))
|
widget_class = registry['widgets'].get(config.pop('class'))
|
||||||
return widget_class, config
|
return widget_class, config
|
||||||
|
|
||||||
@ -32,16 +33,14 @@ def get_dashboard(user):
|
|||||||
"""
|
"""
|
||||||
Return the dashboard layout for a given User.
|
Return the dashboard layout for a given User.
|
||||||
"""
|
"""
|
||||||
if not user.is_anonymous and user.config.get('dashboard'):
|
if not user.is_anonymous and hasattr(user, 'dashboard'):
|
||||||
config = user.config.get('dashboard')
|
dashboard = user.dashboard
|
||||||
else:
|
else:
|
||||||
config = get_default_dashboard_config()
|
dashboard = get_default_dashboard_config()
|
||||||
if not user.is_anonymous:
|
|
||||||
user.config.set('dashboard', config, commit=True)
|
|
||||||
|
|
||||||
widgets = []
|
widgets = []
|
||||||
for grid_item in config['layout']:
|
for grid_item in dashboard.layout:
|
||||||
widget_class, widget_config = get_widget_class_and_config(user, grid_item['id'])
|
widget_class, widget_config = get_widget_class_and_config(dashboard, grid_item['id'])
|
||||||
widget = widget_class(id=grid_item['id'], **widget_config)
|
widget = widget_class(id=grid_item['id'], **widget_config)
|
||||||
widget.set_layout(grid_item)
|
widget.set_layout(grid_item)
|
||||||
widgets.append(widget)
|
widgets.append(widget)
|
||||||
@ -50,23 +49,23 @@ def get_dashboard(user):
|
|||||||
|
|
||||||
|
|
||||||
def get_default_dashboard_config():
|
def get_default_dashboard_config():
|
||||||
config = {
|
dashboard = Dashboard(
|
||||||
'layout': [],
|
layout=[],
|
||||||
'widgets': {},
|
config={}
|
||||||
}
|
)
|
||||||
for widget in DEFAULT_DASHBOARD:
|
for widget in DEFAULT_DASHBOARD:
|
||||||
id = str(uuid.uuid4())
|
id = str(uuid.uuid4())
|
||||||
config['layout'].append({
|
dashboard.layout.append({
|
||||||
'id': id,
|
'id': id,
|
||||||
'w': widget['width'],
|
'w': widget['width'],
|
||||||
'h': widget['height'],
|
'h': widget['height'],
|
||||||
'x': widget.get('x'),
|
'x': widget.get('x'),
|
||||||
'y': widget.get('y'),
|
'y': widget.get('y'),
|
||||||
})
|
})
|
||||||
config['widgets'][id] = {
|
dashboard.config[id] = {
|
||||||
'class': widget['widget'],
|
'class': widget['widget'],
|
||||||
'title': widget.get('title'),
|
'title': widget.get('title'),
|
||||||
'config': widget.get('config', {}),
|
'config': widget.get('config', {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return dashboard
|
||||||
|
@ -35,7 +35,7 @@ class DashboardWidget:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self, id=None, title=None, color=None, config=None, width=None, height=None, x=None, y=None):
|
def __init__(self, id=None, title=None, color=None, config=None, width=None, height=None, x=None, y=None):
|
||||||
self.id = id or uuid.uuid4()
|
self.id = id or str(uuid.uuid4())
|
||||||
self.config = config or {}
|
self.config = config or {}
|
||||||
self.title = title or self.default_title
|
self.title = title or self.default_title
|
||||||
self.color = color
|
self.color = color
|
||||||
@ -59,7 +59,7 @@ class DashboardWidget:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return f'{self.__class__.__module__}.{self.__class__.__name__}'
|
return f'{self.__class__.__module__.split(".")[0]}.{self.__class__.__name__}'
|
||||||
|
|
||||||
|
|
||||||
@register_widget
|
@register_widget
|
||||||
|
25
netbox/extras/migrations/0087_dashboard.py
Normal file
25
netbox/extras/migrations/0087_dashboard.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-02-24 00:56
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('extras', '0086_configtemplate'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Dashboard',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('layout', models.JSONField()),
|
||||||
|
('config', models.JSONField()),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='dashboard', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -1,6 +1,7 @@
|
|||||||
from .change_logging import ObjectChange
|
from .change_logging import ObjectChange
|
||||||
from .configs import *
|
from .configs import *
|
||||||
from .customfields import CustomField
|
from .customfields import CustomField
|
||||||
|
from .dashboard import *
|
||||||
from .models import *
|
from .models import *
|
||||||
from .search import *
|
from .search import *
|
||||||
from .staging import *
|
from .staging import *
|
||||||
@ -15,6 +16,7 @@ __all__ = (
|
|||||||
'ConfigTemplate',
|
'ConfigTemplate',
|
||||||
'CustomField',
|
'CustomField',
|
||||||
'CustomLink',
|
'CustomLink',
|
||||||
|
'Dashboard',
|
||||||
'ExportTemplate',
|
'ExportTemplate',
|
||||||
'ImageAttachment',
|
'ImageAttachment',
|
||||||
'JobResult',
|
'JobResult',
|
||||||
|
41
netbox/extras/models/dashboard.py
Normal file
41
netbox/extras/models/dashboard.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'Dashboard',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Dashboard(models.Model):
|
||||||
|
user = models.OneToOneField(
|
||||||
|
to=get_user_model(),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='dashboard'
|
||||||
|
)
|
||||||
|
layout = models.JSONField()
|
||||||
|
config = models.JSONField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add_widget(self, widget, x=None, y=None):
|
||||||
|
id = str(widget.id)
|
||||||
|
self.config[id] = {
|
||||||
|
'class': widget.name,
|
||||||
|
'title': widget.title,
|
||||||
|
'color': widget.color,
|
||||||
|
'config': widget.config,
|
||||||
|
}
|
||||||
|
self.layout.append({
|
||||||
|
'id': id,
|
||||||
|
'h': widget.height,
|
||||||
|
'w': widget.width,
|
||||||
|
'x': x,
|
||||||
|
'y': y,
|
||||||
|
})
|
||||||
|
|
||||||
|
def delete_widget(self, id):
|
||||||
|
del self.config[id]
|
||||||
|
self.layout = [
|
||||||
|
item for item in self.layout if item['id'] != id
|
||||||
|
]
|
@ -707,13 +707,8 @@ class DashboardWidgetAddView(LoginRequiredMixin, View):
|
|||||||
data['config'] = config_form.cleaned_data
|
data['config'] = config_form.cleaned_data
|
||||||
widget = widget_class(**data)
|
widget = widget_class(**data)
|
||||||
data['class'] = class_name
|
data['class'] = class_name
|
||||||
request.user.config.set(f'dashboard.widgets.{widget.id}', data)
|
request.user.dashboard.add_widget(widget)
|
||||||
request.user.config.get(f'dashboard.layout').append({
|
request.user.dashboard.save()
|
||||||
'h': widget.height,
|
|
||||||
'w': widget.width,
|
|
||||||
'id': str(widget.id),
|
|
||||||
})
|
|
||||||
request.user.config.save()
|
|
||||||
|
|
||||||
response = HttpResponse()
|
response = HttpResponse()
|
||||||
response['HX-Redirect'] = reverse('home')
|
response['HX-Redirect'] = reverse('home')
|
||||||
@ -730,7 +725,8 @@ class DashboardWidgetConfigView(LoginRequiredMixin, View):
|
|||||||
template_name = 'extras/dashboard/widget_config.html'
|
template_name = 'extras/dashboard/widget_config.html'
|
||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
widget_class, config = get_widget_class_and_config(request.user, id)
|
id = str(id)
|
||||||
|
widget_class, config = get_widget_class_and_config(request.user.dashboard, id)
|
||||||
widget_form = DashboardWidgetForm(initial=config)
|
widget_form = DashboardWidgetForm(initial=config)
|
||||||
config_form = widget_class.ConfigForm(initial=config.get('config'), prefix='config')
|
config_form = widget_class.ConfigForm(initial=config.get('config'), prefix='config')
|
||||||
|
|
||||||
@ -744,14 +740,16 @@ class DashboardWidgetConfigView(LoginRequiredMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request, id):
|
def post(self, request, id):
|
||||||
widget_class, config = get_widget_class_and_config(request.user, id)
|
id = str(id)
|
||||||
|
widget_class, config = get_widget_class_and_config(request.user.dashboard, id)
|
||||||
widget_form = DashboardWidgetForm(request.POST)
|
widget_form = DashboardWidgetForm(request.POST)
|
||||||
config_form = widget_class.ConfigForm(request.POST, prefix='config')
|
config_form = widget_class.ConfigForm(request.POST, prefix='config')
|
||||||
|
|
||||||
if widget_form.is_valid() and config_form.is_valid():
|
if widget_form.is_valid() and config_form.is_valid():
|
||||||
data = widget_form.cleaned_data
|
data = widget_form.cleaned_data
|
||||||
data['config'] = config_form.cleaned_data
|
data['config'] = config_form.cleaned_data
|
||||||
request.user.config.set(f'dashboard.widgets.{id}', data, commit=True)
|
request.user.dashboard.config[id].update(data)
|
||||||
|
request.user.dashboard.save()
|
||||||
|
|
||||||
response = HttpResponse()
|
response = HttpResponse()
|
||||||
response['HX-Redirect'] = reverse('home')
|
response['HX-Redirect'] = reverse('home')
|
||||||
@ -768,7 +766,8 @@ class DashboardWidgetDeleteView(LoginRequiredMixin, View):
|
|||||||
template_name = 'generic/object_delete.html'
|
template_name = 'generic/object_delete.html'
|
||||||
|
|
||||||
def get(self, request, id):
|
def get(self, request, id):
|
||||||
widget_class, config = get_widget_class_and_config(request.user, id)
|
id = str(id)
|
||||||
|
widget_class, config = get_widget_class_and_config(request.user.dashboard, id)
|
||||||
widget = widget_class(**config)
|
widget = widget_class(**config)
|
||||||
form = ConfirmationForm(initial=request.GET)
|
form = ConfirmationForm(initial=request.GET)
|
||||||
|
|
||||||
@ -786,15 +785,12 @@ class DashboardWidgetDeleteView(LoginRequiredMixin, View):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request, id):
|
def post(self, request, id):
|
||||||
|
id = str(id)
|
||||||
form = ConfirmationForm(request.POST)
|
form = ConfirmationForm(request.POST)
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
config = request.user.config
|
request.user.dashboard.delete_widget(id)
|
||||||
config.clear(f'dashboard.widgets.{id}')
|
request.user.dashboard.save()
|
||||||
config.set('dashboard.layout', [
|
|
||||||
item for item in config.get('dashboard.layout') if item['id'] != str(id)
|
|
||||||
])
|
|
||||||
config.save()
|
|
||||||
messages.success(request, f'Deleted widget {id}')
|
messages.success(request, f'Deleted widget {id}')
|
||||||
else:
|
else:
|
||||||
messages.error(request, f'Error deleting widget: {form.errors[0]}')
|
messages.error(request, f'Error deleting widget: {form.errors[0]}')
|
||||||
|
@ -11,7 +11,6 @@ from packaging import version
|
|||||||
|
|
||||||
from extras.dashboard.utils import get_dashboard
|
from extras.dashboard.utils import get_dashboard
|
||||||
from netbox.forms import SearchForm
|
from netbox.forms import SearchForm
|
||||||
from netbox.registry import registry
|
|
||||||
from netbox.search import LookupTypes
|
from netbox.search import LookupTypes
|
||||||
from netbox.search.backends import search_backend
|
from netbox.search.backends import search_backend
|
||||||
from netbox.tables import SearchTable
|
from netbox.tables import SearchTable
|
||||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
@ -7,9 +7,7 @@ async function saveDashboardLayout(
|
|||||||
gridData: GridStackWidget[] | GridStackOptions,
|
gridData: GridStackWidget[] | GridStackOptions,
|
||||||
): Promise<APIResponse<APIUserConfig>> {
|
): Promise<APIResponse<APIUserConfig>> {
|
||||||
let data = {
|
let data = {
|
||||||
dashboard: {
|
layout: gridData
|
||||||
layout: gridData
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
return await apiPatch<APIUserConfig>(url, data);
|
return await apiPatch<APIUserConfig>(url, data);
|
||||||
}
|
}
|
||||||
@ -24,7 +22,7 @@ export function initDashboard(): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
gridSaveButton.addEventListener('click', () => {
|
gridSaveButton.addEventListener('click', () => {
|
||||||
const url = '/api/users/config/';
|
const url = '/api/extras/dashboard/';
|
||||||
let gridData = grid.save(false);
|
let gridData = grid.save(false);
|
||||||
saveDashboardLayout(url, gridData).then(res => {
|
saveDashboardLayout(url, gridData).then(res => {
|
||||||
if (hasError(res)) {
|
if (hasError(res)) {
|
||||||
|
Loading…
Reference in New Issue
Block a user