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',
|
||||
'CustomFieldSerializer',
|
||||
'CustomLinkSerializer',
|
||||
'DashboardSerializer',
|
||||
'ExportTemplateSerializer',
|
||||
'ImageAttachmentSerializer',
|
||||
'JobResultSerializer',
|
||||
@ -563,3 +564,13 @@ class ContentTypeSerializer(BaseModelSerializer):
|
||||
class Meta:
|
||||
model = ContentType
|
||||
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 . import views
|
||||
|
||||
@ -22,4 +24,7 @@ router.register('job-results', views.JobResultViewSet)
|
||||
router.register('content-types', views.ContentTypeViewSet)
|
||||
|
||||
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.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.generics import RetrieveUpdateDestroyAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.response import Response
|
||||
@ -423,3 +424,15 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
|
||||
queryset = ContentType.objects.order_by('app_label', 'model')
|
||||
serializer_class = serializers.ContentTypeSerializer
|
||||
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 extras.constants import DEFAULT_DASHBOARD
|
||||
from extras.models import Dashboard
|
||||
|
||||
__all__ = (
|
||||
'get_dashboard',
|
||||
@ -22,8 +23,8 @@ def register_widget(cls):
|
||||
return cls
|
||||
|
||||
|
||||
def get_widget_class_and_config(user, id):
|
||||
config = dict(user.config.get(f'dashboard.widgets.{id}')) # Copy to avoid mutating userconfig data
|
||||
def get_widget_class_and_config(dashboard, id):
|
||||
config = dict(dashboard.config[id]) # Copy to avoid mutating userconfig data
|
||||
widget_class = registry['widgets'].get(config.pop('class'))
|
||||
return widget_class, config
|
||||
|
||||
@ -32,16 +33,14 @@ def get_dashboard(user):
|
||||
"""
|
||||
Return the dashboard layout for a given User.
|
||||
"""
|
||||
if not user.is_anonymous and user.config.get('dashboard'):
|
||||
config = user.config.get('dashboard')
|
||||
if not user.is_anonymous and hasattr(user, 'dashboard'):
|
||||
dashboard = user.dashboard
|
||||
else:
|
||||
config = get_default_dashboard_config()
|
||||
if not user.is_anonymous:
|
||||
user.config.set('dashboard', config, commit=True)
|
||||
dashboard = get_default_dashboard_config()
|
||||
|
||||
widgets = []
|
||||
for grid_item in config['layout']:
|
||||
widget_class, widget_config = get_widget_class_and_config(user, grid_item['id'])
|
||||
for grid_item in dashboard.layout:
|
||||
widget_class, widget_config = get_widget_class_and_config(dashboard, grid_item['id'])
|
||||
widget = widget_class(id=grid_item['id'], **widget_config)
|
||||
widget.set_layout(grid_item)
|
||||
widgets.append(widget)
|
||||
@ -50,23 +49,23 @@ def get_dashboard(user):
|
||||
|
||||
|
||||
def get_default_dashboard_config():
|
||||
config = {
|
||||
'layout': [],
|
||||
'widgets': {},
|
||||
}
|
||||
dashboard = Dashboard(
|
||||
layout=[],
|
||||
config={}
|
||||
)
|
||||
for widget in DEFAULT_DASHBOARD:
|
||||
id = str(uuid.uuid4())
|
||||
config['layout'].append({
|
||||
dashboard.layout.append({
|
||||
'id': id,
|
||||
'w': widget['width'],
|
||||
'h': widget['height'],
|
||||
'x': widget.get('x'),
|
||||
'y': widget.get('y'),
|
||||
})
|
||||
config['widgets'][id] = {
|
||||
dashboard.config[id] = {
|
||||
'class': widget['widget'],
|
||||
'title': widget.get('title'),
|
||||
'config': widget.get('config', {}),
|
||||
}
|
||||
|
||||
return config
|
||||
return dashboard
|
||||
|
@ -35,7 +35,7 @@ class DashboardWidget:
|
||||
pass
|
||||
|
||||
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.title = title or self.default_title
|
||||
self.color = color
|
||||
@ -59,7 +59,7 @@ class DashboardWidget:
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return f'{self.__class__.__module__}.{self.__class__.__name__}'
|
||||
return f'{self.__class__.__module__.split(".")[0]}.{self.__class__.__name__}'
|
||||
|
||||
|
||||
@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 .configs import *
|
||||
from .customfields import CustomField
|
||||
from .dashboard import *
|
||||
from .models import *
|
||||
from .search import *
|
||||
from .staging import *
|
||||
@ -15,6 +16,7 @@ __all__ = (
|
||||
'ConfigTemplate',
|
||||
'CustomField',
|
||||
'CustomLink',
|
||||
'Dashboard',
|
||||
'ExportTemplate',
|
||||
'ImageAttachment',
|
||||
'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
|
||||
widget = widget_class(**data)
|
||||
data['class'] = class_name
|
||||
request.user.config.set(f'dashboard.widgets.{widget.id}', data)
|
||||
request.user.config.get(f'dashboard.layout').append({
|
||||
'h': widget.height,
|
||||
'w': widget.width,
|
||||
'id': str(widget.id),
|
||||
})
|
||||
request.user.config.save()
|
||||
request.user.dashboard.add_widget(widget)
|
||||
request.user.dashboard.save()
|
||||
|
||||
response = HttpResponse()
|
||||
response['HX-Redirect'] = reverse('home')
|
||||
@ -730,7 +725,8 @@ class DashboardWidgetConfigView(LoginRequiredMixin, View):
|
||||
template_name = 'extras/dashboard/widget_config.html'
|
||||
|
||||
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)
|
||||
config_form = widget_class.ConfigForm(initial=config.get('config'), prefix='config')
|
||||
|
||||
@ -744,14 +740,16 @@ class DashboardWidgetConfigView(LoginRequiredMixin, View):
|
||||
})
|
||||
|
||||
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)
|
||||
config_form = widget_class.ConfigForm(request.POST, prefix='config')
|
||||
|
||||
if widget_form.is_valid() and config_form.is_valid():
|
||||
data = widget_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['HX-Redirect'] = reverse('home')
|
||||
@ -768,7 +766,8 @@ class DashboardWidgetDeleteView(LoginRequiredMixin, View):
|
||||
template_name = 'generic/object_delete.html'
|
||||
|
||||
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)
|
||||
form = ConfirmationForm(initial=request.GET)
|
||||
|
||||
@ -786,15 +785,12 @@ class DashboardWidgetDeleteView(LoginRequiredMixin, View):
|
||||
})
|
||||
|
||||
def post(self, request, id):
|
||||
id = str(id)
|
||||
form = ConfirmationForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
config = request.user.config
|
||||
config.clear(f'dashboard.widgets.{id}')
|
||||
config.set('dashboard.layout', [
|
||||
item for item in config.get('dashboard.layout') if item['id'] != str(id)
|
||||
])
|
||||
config.save()
|
||||
request.user.dashboard.delete_widget(id)
|
||||
request.user.dashboard.save()
|
||||
messages.success(request, f'Deleted widget {id}')
|
||||
else:
|
||||
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 netbox.forms import SearchForm
|
||||
from netbox.registry import registry
|
||||
from netbox.search import LookupTypes
|
||||
from netbox.search.backends import search_backend
|
||||
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,
|
||||
): Promise<APIResponse<APIUserConfig>> {
|
||||
let data = {
|
||||
dashboard: {
|
||||
layout: gridData
|
||||
},
|
||||
}
|
||||
return await apiPatch<APIUserConfig>(url, data);
|
||||
}
|
||||
@ -24,7 +22,7 @@ export function initDashboard(): void {
|
||||
return;
|
||||
}
|
||||
gridSaveButton.addEventListener('click', () => {
|
||||
const url = '/api/users/config/';
|
||||
const url = '/api/extras/dashboard/';
|
||||
let gridData = grid.save(false);
|
||||
saveDashboardLayout(url, gridData).then(res => {
|
||||
if (hasError(res)) {
|
||||
|
Loading…
Reference in New Issue
Block a user