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
|
# Webhook content types
|
||||||
HTTP_CONTENT_TYPE_JSON = 'application/json'
|
HTTP_CONTENT_TYPE_JSON = 'application/json'
|
||||||
|
|
||||||
@ -10,10 +12,9 @@ DEFAULT_DASHBOARD = [
|
|||||||
'title': 'IPAM',
|
'title': 'IPAM',
|
||||||
'config': {
|
'config': {
|
||||||
'models': [
|
'models': [
|
||||||
'ipam.Aggregate',
|
ContentType.objects.get_by_natural_key('ipam', 'aggregate').pk,
|
||||||
'ipam.Prefix',
|
ContentType.objects.get_by_natural_key('ipam', 'prefix').pk,
|
||||||
'ipam.IPRange',
|
ContentType.objects.get_by_natural_key('ipam', 'ipaddress').pk,
|
||||||
'ipam.IPAddress',
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -24,15 +25,14 @@ DEFAULT_DASHBOARD = [
|
|||||||
'title': 'DCIM',
|
'title': 'DCIM',
|
||||||
'config': {
|
'config': {
|
||||||
'models': [
|
'models': [
|
||||||
'dcim.Site',
|
ContentType.objects.get_by_natural_key('dcim', 'site').pk,
|
||||||
'dcim.Rack',
|
ContentType.objects.get_by_natural_key('dcim', 'rack').pk,
|
||||||
'dcim.Device',
|
ContentType.objects.get_by_natural_key('dcim', 'device').pk,
|
||||||
'dcim.Cable',
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'widget': 'extras.StaticContentWidget',
|
'widget': 'extras.NoteWidget',
|
||||||
'width': 4,
|
'width': 4,
|
||||||
'height': 3,
|
'height': 3,
|
||||||
'config': {
|
'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__ = (
|
__all__ = (
|
||||||
'get_dashboard',
|
'get_dashboard',
|
||||||
|
'get_default_dashboard_config',
|
||||||
|
'get_widget_class_and_config',
|
||||||
'register_widget',
|
'register_widget',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,6 +22,12 @@ def register_widget(cls):
|
|||||||
return 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):
|
def get_dashboard(user):
|
||||||
"""
|
"""
|
||||||
Return the dashboard layout for a given User.
|
Return the dashboard layout for a given User.
|
||||||
@ -33,10 +41,8 @@ def get_dashboard(user):
|
|||||||
|
|
||||||
widgets = []
|
widgets = []
|
||||||
for grid_item in config['layout']:
|
for grid_item in config['layout']:
|
||||||
widget_id = grid_item['id']
|
widget_class, widget_config = get_widget_class_and_config(user, grid_item['id'])
|
||||||
widget_config = config['widgets'][widget_id]
|
widget = widget_class(id=grid_item['id'], **widget_config)
|
||||||
widget_class = registry['widgets'].get(widget_config.pop('class'))
|
|
||||||
widget = widget_class(id=widget_id, **widget_config)
|
|
||||||
widget.set_layout(grid_item)
|
widget.set_layout(grid_item)
|
||||||
widgets.append(widget)
|
widgets.append(widget)
|
||||||
|
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext as _
|
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 utilities.templatetags.builtins.filters import render_markdown
|
||||||
from .utils import register_widget
|
from .utils import register_widget
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ChangeLogWidget',
|
'ChangeLogWidget',
|
||||||
'DashboardWidget',
|
'DashboardWidget',
|
||||||
|
'NoteWidget',
|
||||||
'ObjectCountsWidget',
|
'ObjectCountsWidget',
|
||||||
'StaticContentWidget',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +25,10 @@ class DashboardWidget:
|
|||||||
width = 4
|
width = 4
|
||||||
height = 3
|
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.id = id or uuid.uuid4()
|
||||||
self.config = config or {}
|
self.config = config or {}
|
||||||
if title:
|
if title:
|
||||||
@ -48,7 +54,7 @@ class DashboardWidget:
|
|||||||
|
|
||||||
|
|
||||||
@register_widget
|
@register_widget
|
||||||
class StaticContentWidget(DashboardWidget):
|
class NoteWidget(DashboardWidget):
|
||||||
description = _('Display some arbitrary custom content. Markdown is supported.')
|
description = _('Display some arbitrary custom content. Markdown is supported.')
|
||||||
default_content = """
|
default_content = """
|
||||||
<div class="d-flex justify-content-center align-items-center" style="height: 100%">
|
<div class="d-flex justify-content-center align-items-center" style="height: 100%">
|
||||||
@ -56,6 +62,11 @@ class StaticContentWidget(DashboardWidget):
|
|||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class ConfigForm(BootstrapMixin, forms.Form):
|
||||||
|
content = forms.CharField(
|
||||||
|
widget=forms.Textarea()
|
||||||
|
)
|
||||||
|
|
||||||
def render(self, request):
|
def render(self, request):
|
||||||
if content := self.config.get('content'):
|
if content := self.config.get('content'):
|
||||||
return render_markdown(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.')
|
description = _('Display a set of NetBox models and the number of objects created for each type.')
|
||||||
template_name = 'extras/dashboard/widgets/objectcounts.html'
|
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):
|
def render(self, request):
|
||||||
counts = []
|
counts = []
|
||||||
for model_name in self.config['models']:
|
for content_type_id in self.config['models']:
|
||||||
app_label, name = model_name.lower().split('.')
|
model = ContentType.objects.get(pk=content_type_id).model_class()
|
||||||
model = ContentType.objects.get_by_natural_key(app_label, name).model_class()
|
|
||||||
object_count = model.objects.restrict(request.user, 'view').count
|
object_count = model.objects.restrict(request.user, 'view').count
|
||||||
counts.append((model, object_count))
|
counts.append((model, object_count))
|
||||||
|
|
||||||
|
@ -87,6 +87,9 @@ urlpatterns = [
|
|||||||
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
path('changelog/', views.ObjectChangeListView.as_view(), name='objectchange_list'),
|
||||||
path('changelog/<int:pk>/', include(get_model_urls('extras', 'objectchange'))),
|
path('changelog/<int:pk>/', include(get_model_urls('extras', 'objectchange'))),
|
||||||
|
|
||||||
|
# User dashboard
|
||||||
|
path('dashboard/widgets/', views.DashboardWidgetConfigView.as_view(), name='dashboardwidget_edit'),
|
||||||
|
|
||||||
# Reports
|
# Reports
|
||||||
path('reports/', views.ReportListView.as_view(), name='report_list'),
|
path('reports/', views.ReportListView.as_view(), name='report_list'),
|
||||||
path('reports/results/<int:job_result_pk>/', views.ReportResultView.as_view(), name='report_result'),
|
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 import messages
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q
|
||||||
from django.http import Http404, HttpResponseForbidden
|
from django.http import Http404, HttpResponseForbidden
|
||||||
@ -8,6 +9,8 @@ from django.views.generic import View
|
|||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
from rq import Worker
|
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 netbox.views import generic
|
||||||
from utilities.htmx import is_htmx
|
from utilities.htmx import is_htmx
|
||||||
from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict
|
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
|
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
|
# Reports
|
||||||
#
|
#
|
||||||
|
@ -9,16 +9,18 @@
|
|||||||
gs-id="{{ widget.id }}"
|
gs-id="{{ widget.id }}"
|
||||||
>
|
>
|
||||||
<div class="card grid-stack-item-content">
|
<div class="card grid-stack-item-content">
|
||||||
{% if widget.title %}
|
<div class="card-header text-center text-light bg-secondary p-1">
|
||||||
<div class="card-header text-center text-light bg-secondary p-1">
|
<div class="float-start ps-1">
|
||||||
<div class="float-end pe-1"><i class="mdi mdi-close"></i></div>
|
<a href="{% url 'extras:dashboardwidget_edit' %}?id={{ widget.id }}"><i class="mdi mdi-cog text-gray"></i></a>
|
||||||
<strong>{{ widget.title }}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="float-end pe-1">
|
||||||
<div class="card-body p-2">
|
<a href="#"><i class="mdi mdi-close text-gray"></i></a>
|
||||||
{% if not widget.title %}
|
</div>
|
||||||
<div class="float-end pe-1"><i class="mdi mdi-close"></i></div>
|
{% if widget.title %}
|
||||||
|
<strong>{{ widget.title }}</strong>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
{% render_widget widget %}
|
{% render_widget widget %}
|
||||||
</div>
|
</div>
|
||||||
</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