1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #9416: Dashboard widgets (#11823)

* Replace masonry with gridstack

* Initial work on dashboard widgets

* Implement function to save dashboard layout

* Define a default dashboard

* Clean up widgets

* Implement widget configuration views & forms

* Permit merging dict value with existing dict in user config

* Add widget deletion view

* Enable HTMX for widget configuration

* Implement view to add dashboard widgets

* ObjectCountsWidget: Identify models by app_label & name

* Add color customization to dashboard widgets

* Introduce Dashboard model to store user dashboard layout & config

* Clean up utility functions

* Remove hard-coded API URL

* Use fixed grid cell height

* Add modal close button

* Clean up dashboard views

* Rebuild JS
This commit is contained in:
Jeremy Stretch
2023-02-24 16:04:00 -05:00
committed by GitHub
parent 36771e821c
commit 084a2cc52c
40 changed files with 788 additions and 310 deletions

View File

@@ -0,0 +1,2 @@
from .utils import *
from .widgets import *

View File

@@ -0,0 +1,38 @@
from django import forms
from django.urls import reverse_lazy
from netbox.registry import registry
from utilities.forms import BootstrapMixin, add_blank_choice
from utilities.choices import ButtonColorChoices
__all__ = (
'DashboardWidgetAddForm',
'DashboardWidgetForm',
)
def get_widget_choices():
return registry['widgets'].items()
class DashboardWidgetForm(BootstrapMixin, forms.Form):
title = forms.CharField(
required=False
)
color = forms.ChoiceField(
choices=add_blank_choice(ButtonColorChoices),
required=False,
)
class DashboardWidgetAddForm(DashboardWidgetForm):
widget_class = forms.ChoiceField(
choices=get_widget_choices,
widget=forms.Select(
attrs={
'hx-get': reverse_lazy('extras:dashboardwidget_add'),
'hx-target': '#widget_add_form',
}
)
)
field_order = ('widget_class', 'title', 'color')

View File

@@ -0,0 +1,76 @@
import uuid
from django.core.exceptions import ObjectDoesNotExist
from netbox.registry import registry
from extras.constants import DEFAULT_DASHBOARD
__all__ = (
'get_dashboard',
'get_default_dashboard',
'get_widget_class',
'register_widget',
)
def register_widget(cls):
"""
Decorator for registering a DashboardWidget class.
"""
app_label = cls.__module__.split('.', maxsplit=1)[0]
label = f'{app_label}.{cls.__name__}'
registry['widgets'][label] = cls
return cls
def get_widget_class(name):
"""
Return a registered DashboardWidget class identified by its name.
"""
try:
return registry['widgets'][name]
except KeyError:
raise ValueError(f"Unregistered widget class: {name}")
def get_dashboard(user):
"""
Return the Dashboard for a given User if one exists, or generate a default dashboard.
"""
if user.is_anonymous:
dashboard = get_default_dashboard()
else:
try:
dashboard = user.dashboard
except ObjectDoesNotExist:
# Create a dashboard for this user
dashboard = get_default_dashboard()
dashboard.user = user
dashboard.save()
return dashboard
def get_default_dashboard():
from extras.models import Dashboard
dashboard = Dashboard(
layout=[],
config={}
)
for widget in DEFAULT_DASHBOARD:
id = str(uuid.uuid4())
dashboard.layout.append({
'id': id,
'w': widget['width'],
'h': widget['height'],
'x': widget.get('x'),
'y': widget.get('y'),
})
dashboard.config[id] = {
'class': widget['widget'],
'title': widget.get('title'),
'config': widget.get('config', {}),
}
return dashboard

View File

@@ -0,0 +1,119 @@
import uuid
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from utilities.forms import BootstrapMixin
from utilities.templatetags.builtins.filters import render_markdown
from utilities.utils import content_type_identifier, content_type_name
from .utils import register_widget
__all__ = (
'ChangeLogWidget',
'DashboardWidget',
'NoteWidget',
'ObjectCountsWidget',
)
def get_content_type_labels():
return [
(content_type_identifier(ct), content_type_name(ct))
for ct in ContentType.objects.order_by('app_label', 'model')
]
class DashboardWidget:
default_title = None
description = None
width = 4
height = 3
class ConfigForm(forms.Form):
pass
def __init__(self, id=None, title=None, color=None, config=None, width=None, height=None, x=None, y=None):
self.id = id or str(uuid.uuid4())
self.config = config or {}
self.title = title or self.default_title
self.color = color
if width:
self.width = width
if height:
self.height = height
self.x, self.y = x, y
def __str__(self):
return self.title or self.__class__.__name__
def set_layout(self, grid_item):
self.width = grid_item['w']
self.height = grid_item['h']
self.x = grid_item.get('x')
self.y = grid_item.get('y')
def render(self, request):
raise NotImplementedError(f"{self.__class__} must define a render() method.")
@property
def name(self):
return f'{self.__class__.__module__.split(".")[0]}.{self.__class__.__name__}'
@property
def form_data(self):
return {
'title': self.title,
'color': self.color,
'config': self.config,
}
@register_widget
class NoteWidget(DashboardWidget):
description = _('Display some arbitrary custom content. Markdown is supported.')
class ConfigForm(BootstrapMixin, forms.Form):
content = forms.CharField(
widget=forms.Textarea()
)
def render(self, request):
return render_markdown(self.config.get('content'))
@register_widget
class ObjectCountsWidget(DashboardWidget):
default_title = _('Objects')
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):
models = forms.MultipleChoiceField(
choices=get_content_type_labels
)
def render(self, request):
counts = []
for content_type_id in self.config['models']:
app_label, model_name = content_type_id.split('.')
model = ContentType.objects.get_by_natural_key(app_label, model_name).model_class()
object_count = model.objects.restrict(request.user, 'view').count
counts.append((model, object_count))
return render_to_string(self.template_name, {
'counts': counts,
})
@register_widget
class ChangeLogWidget(DashboardWidget):
default_title = _('Change Log')
description = _('Display the most recent records from the global change log.')
template_name = 'extras/dashboard/widgets/changelog.html'
width = 12
height = 4
def render(self, request):
return render_to_string(self.template_name, {})