diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py
index e1ce7b713..0474582ee 100644
--- a/netbox/extras/constants.py
+++ b/netbox/extras/constants.py
@@ -1,3 +1,5 @@
+from django.contrib.contenttypes.models import ContentType
+
# Webhook content types
HTTP_CONTENT_TYPE_JSON = 'application/json'
@@ -10,10 +12,9 @@ DEFAULT_DASHBOARD = [
'title': 'IPAM',
'config': {
'models': [
- 'ipam.Aggregate',
- 'ipam.Prefix',
- 'ipam.IPRange',
- 'ipam.IPAddress',
+ ContentType.objects.get_by_natural_key('ipam', 'aggregate').pk,
+ ContentType.objects.get_by_natural_key('ipam', 'prefix').pk,
+ ContentType.objects.get_by_natural_key('ipam', 'ipaddress').pk,
]
}
},
@@ -24,15 +25,14 @@ DEFAULT_DASHBOARD = [
'title': 'DCIM',
'config': {
'models': [
- 'dcim.Site',
- 'dcim.Rack',
- 'dcim.Device',
- 'dcim.Cable',
+ ContentType.objects.get_by_natural_key('dcim', 'site').pk,
+ ContentType.objects.get_by_natural_key('dcim', 'rack').pk,
+ ContentType.objects.get_by_natural_key('dcim', 'device').pk,
]
}
},
{
- 'widget': 'extras.StaticContentWidget',
+ 'widget': 'extras.NoteWidget',
'width': 4,
'height': 3,
'config': {
diff --git a/netbox/extras/dashboard/forms.py b/netbox/extras/dashboard/forms.py
new file mode 100644
index 000000000..04896cf80
--- /dev/null
+++ b/netbox/extras/dashboard/forms.py
@@ -0,0 +1,13 @@
+from django import forms
+
+from utilities.forms import BootstrapMixin
+
+__all__ = (
+ 'DashboardWidgetForm',
+)
+
+
+class DashboardWidgetForm(BootstrapMixin, forms.Form):
+ title = forms.CharField(
+ required=False
+ )
diff --git a/netbox/extras/dashboard/utils.py b/netbox/extras/dashboard/utils.py
index 345b00ff6..cc07ca4e0 100644
--- a/netbox/extras/dashboard/utils.py
+++ b/netbox/extras/dashboard/utils.py
@@ -5,6 +5,8 @@ from extras.constants import DEFAULT_DASHBOARD
__all__ = (
'get_dashboard',
+ 'get_default_dashboard_config',
+ 'get_widget_class_and_config',
'register_widget',
)
@@ -20,6 +22,12 @@ 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
+ widget_class = registry['widgets'].get(config.pop('class'))
+ return widget_class, config
+
+
def get_dashboard(user):
"""
Return the dashboard layout for a given User.
@@ -33,10 +41,8 @@ def get_dashboard(user):
widgets = []
for grid_item in config['layout']:
- widget_id = grid_item['id']
- widget_config = config['widgets'][widget_id]
- widget_class = registry['widgets'].get(widget_config.pop('class'))
- widget = widget_class(id=widget_id, **widget_config)
+ widget_class, widget_config = get_widget_class_and_config(user, grid_item['id'])
+ widget = widget_class(id=grid_item['id'], **widget_config)
widget.set_layout(grid_item)
widgets.append(widget)
diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py
index 5107b276d..4a5078a93 100644
--- a/netbox/extras/dashboard/widgets.py
+++ b/netbox/extras/dashboard/widgets.py
@@ -1,18 +1,21 @@
import uuid
+from django import forms
from django.contrib.contenttypes.models import ContentType
from django.template.loader import render_to_string
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
+from utilities.forms import BootstrapMixin
+from utilities.forms.fields import ContentTypeMultipleChoiceField
from utilities.templatetags.builtins.filters import render_markdown
from .utils import register_widget
__all__ = (
'ChangeLogWidget',
'DashboardWidget',
+ 'NoteWidget',
'ObjectCountsWidget',
- 'StaticContentWidget',
)
@@ -22,7 +25,10 @@ class DashboardWidget:
width = 4
height = 3
- def __init__(self, id=None, config=None, title=None, width=None, height=None, x=None, y=None):
+ class ConfigForm(forms.Form):
+ pass
+
+ def __init__(self, id=None, title=None, config=None, width=None, height=None, x=None, y=None):
self.id = id or uuid.uuid4()
self.config = config or {}
if title:
@@ -48,7 +54,7 @@ class DashboardWidget:
@register_widget
-class StaticContentWidget(DashboardWidget):
+class NoteWidget(DashboardWidget):
description = _('Display some arbitrary custom content. Markdown is supported.')
default_content = """
@@ -56,6 +62,11 @@ class StaticContentWidget(DashboardWidget):
"""
+ class ConfigForm(BootstrapMixin, forms.Form):
+ content = forms.CharField(
+ widget=forms.Textarea()
+ )
+
def render(self, request):
if content := self.config.get('content'):
return render_markdown(content)
@@ -68,11 +79,19 @@ class ObjectCountsWidget(DashboardWidget):
description = _('Display a set of NetBox models and the number of objects created for each type.')
template_name = 'extras/dashboard/widgets/objectcounts.html'
+ class ConfigForm(BootstrapMixin, forms.Form):
+ # TODO: Track models by app label & name rather than ContentType ID
+ models = ContentTypeMultipleChoiceField(
+ queryset=ContentType.objects.all()
+ )
+
+ def clean_models(self):
+ return [obj.pk for obj in self.cleaned_data['models']]
+
def render(self, request):
counts = []
- for model_name in self.config['models']:
- app_label, name = model_name.lower().split('.')
- model = ContentType.objects.get_by_natural_key(app_label, name).model_class()
+ for content_type_id in self.config['models']:
+ model = ContentType.objects.get(pk=content_type_id).model_class()
object_count = model.objects.restrict(request.user, 'view').count
counts.append((model, object_count))
diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py
index dfbaa1bc6..78483d419 100644
--- a/netbox/extras/urls.py
+++ b/netbox/extras/urls.py
@@ -87,6 +87,9 @@ urlpatterns = [
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
path('changelog//', include(get_model_urls('extras', 'objectchange'))),
+ # User dashboard
+ path('dashboard/widgets/', views.DashboardWidgetConfigView.as_view(), name='dashboardwidget_edit'),
+
# Reports
path('reports/', views.ReportListView.as_view(), name='report_list'),
path('reports/results//', views.ReportResultView.as_view(), name='report_result'),
diff --git a/netbox/extras/views.py b/netbox/extras/views.py
index 3edb70cf1..8a9d62709 100644
--- a/netbox/extras/views.py
+++ b/netbox/extras/views.py
@@ -1,4 +1,5 @@
from django.contrib import messages
+from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count, Q
from django.http import Http404, HttpResponseForbidden
@@ -8,6 +9,8 @@ from django.views.generic import View
from django_rq.queues import get_connection
from rq import Worker
+from extras.dashboard.forms import DashboardWidgetForm
+from extras.dashboard.utils import get_widget_class_and_config
from netbox.views import generic
from utilities.htmx import is_htmx
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
@@ -664,6 +667,42 @@ class JournalEntryBulkDeleteView(generic.BulkDeleteView):
table = tables.JournalEntryTable
+#
+# Dashboard widgets
+#
+
+class DashboardWidgetConfigView(LoginRequiredMixin, View):
+ template_name = 'extras/dashboardwidget_edit.html'
+
+ def get(self, request):
+ widget_class, config = get_widget_class_and_config(request.user, request.GET['id'])
+ widget_form = DashboardWidgetForm(initial=config)
+ config_form = widget_class.ConfigForm(initial=config.get('config'), prefix='config')
+
+ return render(request, self.template_name, {
+ 'widget_form': widget_form,
+ 'config_form': config_form,
+ })
+
+ def post(self, request):
+ id = request.GET['id']
+ widget_class, config = get_widget_class_and_config(request.user, 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)
+
+ return redirect('home')
+
+ return render(request, self.template_name, {
+ 'widget_form': widget_form,
+ 'config_form': config_form,
+ })
+
+
#
# Reports
#
diff --git a/netbox/templates/extras/dashboard/widget.html b/netbox/templates/extras/dashboard/widget.html
index cd95564ed..eefa7f0df 100644
--- a/netbox/templates/extras/dashboard/widget.html
+++ b/netbox/templates/extras/dashboard/widget.html
@@ -9,16 +9,18 @@
gs-id="{{ widget.id }}"
>
- {% if widget.title %}
-