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 %} + +