From fc362979ad8626784e1d7bd6e5f7896ab55c6b07 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 20 Feb 2023 13:31:08 -0500 Subject: [PATCH] Initial work on dashboard widgets --- netbox/extras/dashboard.py | 61 ++++++++++ netbox/extras/templatetags/dashboard.py | 11 ++ netbox/netbox/views/misc.py | 105 +++--------------- netbox/templates/extras/dashboard/widget.html | 18 +++ .../extras/dashboard/widgets/changelog.html | 4 + .../dashboard/widgets/objectcounts.html | 12 ++ netbox/templates/home.html | 88 ++++----------- 7 files changed, 145 insertions(+), 154 deletions(-) create mode 100644 netbox/extras/dashboard.py create mode 100644 netbox/extras/templatetags/dashboard.py create mode 100644 netbox/templates/extras/dashboard/widget.html create mode 100644 netbox/templates/extras/dashboard/widgets/changelog.html create mode 100644 netbox/templates/extras/dashboard/widgets/objectcounts.html diff --git a/netbox/extras/dashboard.py b/netbox/extras/dashboard.py new file mode 100644 index 000000000..aa1889e90 --- /dev/null +++ b/netbox/extras/dashboard.py @@ -0,0 +1,61 @@ +from django.contrib.contenttypes.models import ContentType +from django.template.loader import render_to_string +from django.utils.translation import gettext as _ + + +__all__ = ( + 'ChangeLogWidget', + 'DashboardWidget', + 'ObjectCountsWidget', + 'StaticContentWidget', +) + + +class DashboardWidget: + width = 4 + height = 3 + + def __init__(self, config=None, title=None, width=None, height=None, x=None, y=None): + self.config = config or {} + if title: + self.title = title + if width: + self.width = width + if height: + self.height = height + self.x, self.y = x, y + + def render(self, request): + raise NotImplementedError("DashboardWidget subclasses must define a render() method.") + + +class StaticContentWidget(DashboardWidget): + + def render(self, request): + return self.config.get('content', 'Empty!') + + +class ObjectCountsWidget(DashboardWidget): + template_name = 'extras/dashboard/widgets/objectcounts.html' + + 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() + object_count = model.objects.restrict(request.user, 'view').count + counts.append((model, object_count)) + + return render_to_string(self.template_name, { + 'counts': counts, + }) + + +class ChangeLogWidget(DashboardWidget): + width = 12 + height = 4 + title = _('Change log') + template_name = 'extras/dashboard/widgets/changelog.html' + + def render(self, request): + return render_to_string(self.template_name, {}) diff --git a/netbox/extras/templatetags/dashboard.py b/netbox/extras/templatetags/dashboard.py new file mode 100644 index 000000000..4ac31abcf --- /dev/null +++ b/netbox/extras/templatetags/dashboard.py @@ -0,0 +1,11 @@ +from django import template + + +register = template.Library() + + +@register.simple_tag(takes_context=True) +def render_widget(context, widget): + request = context['request'] + + return widget.render(request) diff --git a/netbox/netbox/views/misc.py b/netbox/netbox/views/misc.py index 3c8c93f84..738c048d9 100644 --- a/netbox/netbox/views/misc.py +++ b/netbox/netbox/views/misc.py @@ -5,27 +5,17 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.cache import cache from django.shortcuts import redirect, render -from django.utils.translation import gettext as _ from django.views.generic import View from django_tables2 import RequestConfig from packaging import version -from circuits.models import Circuit, Provider -from dcim.models import ( - Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, Site, -) -from extras.models import ObjectChange -from extras.tables import ObjectChangeTable -from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF +from extras import dashboard from netbox.forms import SearchForm from netbox.search import LookupTypes from netbox.search.backends import search_backend from netbox.tables import SearchTable -from tenancy.models import Contact, Tenant from utilities.htmx import is_htmx from utilities.paginator import EnhancedPaginator, get_paginate_count -from virtualization.models import Cluster, VirtualMachine -from wireless.models import WirelessLAN, WirelessLink __all__ = ( 'HomeView', @@ -42,79 +32,22 @@ class HomeView(View): if settings.LOGIN_REQUIRED and not request.user.is_authenticated: return redirect('login') - console_connections = ConsolePort.objects.restrict(request.user, 'view')\ - .prefetch_related('_path').filter(_path__is_complete=True).count - power_connections = PowerPort.objects.restrict(request.user, 'view')\ - .prefetch_related('_path').filter(_path__is_complete=True).count - interface_connections = Interface.objects.restrict(request.user, 'view')\ - .prefetch_related('_path').filter(_path__is_complete=True).count - - def get_count_queryset(model): - return model.objects.restrict(request.user, 'view').count - - def build_stats(): - org = ( - Link(_('Sites'), 'dcim:site_list', 'dcim.view_site', get_count_queryset(Site)), - Link(_('Tenants'), 'tenancy:tenant_list', 'tenancy.view_tenant', get_count_queryset(Tenant)), - Link(_('Contacts'), 'tenancy:contact_list', 'tenancy.view_contact', get_count_queryset(Contact)), - ) - dcim = ( - Link(_('Racks'), 'dcim:rack_list', 'dcim.view_rack', get_count_queryset(Rack)), - Link(_('Device Types'), 'dcim:devicetype_list', 'dcim.view_devicetype', get_count_queryset(DeviceType)), - Link(_('Devices'), 'dcim:device_list', 'dcim.view_device', get_count_queryset(Device)), - ) - ipam = ( - Link(_('VRFs'), 'ipam:vrf_list', 'ipam.view_vrf', get_count_queryset(VRF)), - Link(_('Aggregates'), 'ipam:aggregate_list', 'ipam.view_aggregate', get_count_queryset(Aggregate)), - Link(_('Prefixes'), 'ipam:prefix_list', 'ipam.view_prefix', get_count_queryset(Prefix)), - Link(_('IP Ranges'), 'ipam:iprange_list', 'ipam.view_iprange', get_count_queryset(IPRange)), - Link(_('IP Addresses'), 'ipam:ipaddress_list', 'ipam.view_ipaddress', get_count_queryset(IPAddress)), - Link(_('VLANs'), 'ipam:vlan_list', 'ipam.view_vlan', get_count_queryset(VLAN)), - ) - circuits = ( - Link(_('Providers'), 'circuits:provider_list', 'circuits.view_provider', get_count_queryset(Provider)), - Link(_('Circuits'), 'circuits:circuit_list', 'circuits.view_circuit', get_count_queryset(Circuit)) - ) - virtualization = ( - Link(_('Clusters'), 'virtualization:cluster_list', 'virtualization.view_cluster', - get_count_queryset(Cluster)), - Link(_('Virtual Machines'), 'virtualization:virtualmachine_list', 'virtualization.view_virtualmachine', - get_count_queryset(VirtualMachine)), - ) - connections = ( - Link(_('Cables'), 'dcim:cable_list', 'dcim.view_cable', get_count_queryset(Cable)), - Link(_('Interfaces'), 'dcim:interface_connections_list', 'dcim.view_interface', interface_connections), - Link(_('Console'), 'dcim:console_connections_list', 'dcim.view_consoleport', console_connections), - Link(_('Power'), 'dcim:power_connections_list', 'dcim.view_powerport', power_connections), - ) - power = ( - Link(_('Power Panels'), 'dcim:powerpanel_list', 'dcim.view_powerpanel', get_count_queryset(PowerPanel)), - Link(_('Power Feeds'), 'dcim:powerfeed_list', 'dcim.view_powerfeed', get_count_queryset(PowerFeed)), - ) - wireless = ( - Link(_('Wireless LANs'), 'wireless:wirelesslan_list', 'wireless.view_wirelesslan', - get_count_queryset(WirelessLAN)), - Link(_('Wireless Links'), 'wireless:wirelesslink_list', 'wireless.view_wirelesslink', - get_count_queryset(WirelessLink)), - ) - stats = ( - (_('Organization'), org, 'domain'), - (_('IPAM'), ipam, 'counter'), - (_('Virtualization'), virtualization, 'monitor'), - (_('Inventory'), dcim, 'server'), - (_('Circuits'), circuits, 'transit-connection-variant'), - (_('Connections'), connections, 'cable-data'), - (_('Power'), power, 'flash'), - (_('Wireless'), wireless, 'wifi'), - ) - - return stats - - # Compile changelog table - changelog = ObjectChange.objects.restrict(request.user, 'view').prefetch_related( - 'user', 'changed_object_type' - )[:10] - changelog_table = ObjectChangeTable(changelog, user=request.user) + widgets = ( + dashboard.StaticContentWidget({ + 'content': 'First widget!', + }), + dashboard.StaticContentWidget({ + 'content': 'First widget!', + }, title='Testing'), + dashboard.ObjectCountsWidget({ + 'models': [ + 'dcim.Site', + 'ipam.Prefix', + 'tenancy.Tenant', + ], + }, title='Stuff'), + dashboard.ChangeLogWidget(), + ) # Check whether a new release is available. (Only for staff/superusers.) new_release = None @@ -129,9 +62,7 @@ class HomeView(View): } return render(request, self.template_name, { - 'search_form': SearchForm(), - 'stats': build_stats(), - 'changelog_table': changelog_table, + 'widgets': widgets, 'new_release': new_release, }) diff --git a/netbox/templates/extras/dashboard/widget.html b/netbox/templates/extras/dashboard/widget.html new file mode 100644 index 000000000..41c610179 --- /dev/null +++ b/netbox/templates/extras/dashboard/widget.html @@ -0,0 +1,18 @@ +{% load dashboard %} + +
+
+ {% if widget.title %} +
+
+ {{ widget.title }} +
+ {% endif %} +
+ {% if not widget.title %} +
+ {% endif %} + {% render_widget widget %} +
+
+
diff --git a/netbox/templates/extras/dashboard/widgets/changelog.html b/netbox/templates/extras/dashboard/widgets/changelog.html new file mode 100644 index 000000000..dfa4dba3f --- /dev/null +++ b/netbox/templates/extras/dashboard/widgets/changelog.html @@ -0,0 +1,4 @@ +
diff --git a/netbox/templates/extras/dashboard/widgets/objectcounts.html b/netbox/templates/extras/dashboard/widgets/objectcounts.html new file mode 100644 index 000000000..85fa4d389 --- /dev/null +++ b/netbox/templates/extras/dashboard/widgets/objectcounts.html @@ -0,0 +1,12 @@ +{% if counts %} +
+ {% for model, count in counts %} + +
+ {{ model|meta:"verbose_name_plural"|bettertitle }} +
{{ count }}
+
+
+ {% endfor %} +
+{% endif %} diff --git a/netbox/templates/home.html b/netbox/templates/home.html index cef797f40..f6cb96d14 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -3,80 +3,34 @@ {% load render_table from django_tables2 %} {% block header %} - {% if new_release %} - {# new_release is set only if the current user is a superuser or staff member #} -
- -
- {% endif %} + {% if new_release %} + {# new_release is set only if the current user is a superuser or staff member #} +
+ +
+ {% endif %} {% endblock %} {% block title %}Home{% endblock %} {% block content-wrapper %} -
- {# General stats #} -
- {% for section, items, icon in stats %} -
-
-
- - {{ section }} -
-
-
- {% for item in items %} - {% if item.permission in perms %} - -
- {{ item.label }} -

{{ item.count }}

-
-
- {% else %} -
  • -
    - {{ item.label }} -

    - -

    -
    -
  • - {% endif %} - {% endfor %} -
    -
    -
    -
    +
    + + {# Render the user's customized dashboard #} +
    + {% for widget in widgets %} + {% include 'extras/dashboard/widget.html' %} {% endfor %}
    - {# Changelog #} - {% if perms.extras.view_objectchange %} -
    -
    -
    -
    - - Change Log -
    -
    -
    -
    -
    - {% endif %}
    {% endblock content-wrapper %}