-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* 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
- Loading branch information
1 parent
36771e8
commit 084a2cc
Showing
40 changed files
with
788 additions
and
310 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,47 @@ | ||
from django.contrib.contenttypes.models import ContentType | ||
|
||
# Webhook content types | ||
HTTP_CONTENT_TYPE_JSON = 'application/json' | ||
|
||
# Dashboard | ||
DEFAULT_DASHBOARD = [ | ||
{ | ||
'widget': 'extras.ObjectCountsWidget', | ||
'width': 4, | ||
'height': 3, | ||
'title': 'IPAM', | ||
'config': { | ||
'models': [ | ||
'ipam.aggregate', | ||
'ipam.prefix', | ||
'ipam.ipaddress', | ||
] | ||
} | ||
}, | ||
{ | ||
'widget': 'extras.ObjectCountsWidget', | ||
'width': 4, | ||
'height': 3, | ||
'title': 'DCIM', | ||
'config': { | ||
'models': [ | ||
'dcim.site', | ||
'dcim.rack', | ||
'dcim.device', | ||
] | ||
} | ||
}, | ||
{ | ||
'widget': 'extras.NoteWidget', | ||
'width': 4, | ||
'height': 3, | ||
'config': { | ||
'content': 'Welcome to **NetBox**!' | ||
} | ||
}, | ||
{ | ||
'widget': 'extras.ChangeLogWidget', | ||
'width': 12, | ||
'height': 6, | ||
}, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from .utils import * | ||
from .widgets import * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, {}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)), | ||
], | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.