mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Implement widget configuration views & forms
This commit is contained in:
parent
f876e9ed04
commit
7e1f4ef07e
@ -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': {
|
||||
|
13
netbox/extras/dashboard/forms.py
Normal file
13
netbox/extras/dashboard/forms.py
Normal file
@ -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
|
||||
)
|
@ -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)
|
||||
|
||||
|
@ -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 = """
|
||||
<div class="d-flex justify-content-center align-items-center" style="height: 100%">
|
||||
@ -56,6 +62,11 @@ class StaticContentWidget(DashboardWidget):
|
||||
</div>
|
||||
"""
|
||||
|
||||
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))
|
||||
|
||||
|
@ -87,6 +87,9 @@ urlpatterns = [
|
||||
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||
path('changelog/<int:pk>/', 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/<int:job_result_pk>/', views.ReportResultView.as_view(), name='report_result'),
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -9,16 +9,18 @@
|
||||
gs-id="{{ widget.id }}"
|
||||
>
|
||||
<div class="card grid-stack-item-content">
|
||||
{% if widget.title %}
|
||||
<div class="card-header text-center text-light bg-secondary p-1">
|
||||
<div class="float-end pe-1"><i class="mdi mdi-close"></i></div>
|
||||
<strong>{{ widget.title }}</strong>
|
||||
<div class="card-header text-center text-light bg-secondary p-1">
|
||||
<div class="float-start ps-1">
|
||||
<a href="{% url 'extras:dashboardwidget_edit' %}?id={{ widget.id }}"><i class="mdi mdi-cog text-gray"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body p-2">
|
||||
{% if not widget.title %}
|
||||
<div class="float-end pe-1"><i class="mdi mdi-close"></i></div>
|
||||
<div class="float-end pe-1">
|
||||
<a href="#"><i class="mdi mdi-close text-gray"></i></a>
|
||||
</div>
|
||||
{% if widget.title %}
|
||||
<strong>{{ widget.title }}</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
{% render_widget widget %}
|
||||
</div>
|
||||
</div>
|
||||
|
33
netbox/templates/extras/dashboardwidget_edit.html
Normal file
33
netbox/templates/extras/dashboardwidget_edit.html
Normal file
@ -0,0 +1,33 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
Editing {{ widget }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="object-list-tab">
|
||||
|
||||
<form action="" method="post" enctype="multipart/form-data" class="form-object-edit mt-5">
|
||||
{% csrf_token %}
|
||||
|
||||
<div id="form_fields">
|
||||
{% block form %}
|
||||
{% render_form widget_form %}
|
||||
{% render_form config_form %}
|
||||
{% endblock form %}
|
||||
</div>
|
||||
|
||||
<div class="text-end my-3">
|
||||
{% block buttons %}
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||
<a class="btn btn-outline-danger" href="{% url 'home' %}">Cancel</a>
|
||||
{% endblock buttons %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content-wrapper %}
|
Loading…
Reference in New Issue
Block a user