Initial work on dashboard widgets

This commit is contained in:
jeremystretch 2023-02-20 13:31:08 -05:00
parent b25c349a27
commit fc362979ad
7 changed files with 145 additions and 154 deletions

View File

@ -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, {})

View File

@ -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)

View File

@ -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)),
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(),
)
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)
# 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,
})

View File

@ -0,0 +1,18 @@
{% load dashboard %}
<div class="grid-stack-item" gs-w="{{ widget.width }}" gs-h="{{ widget.height }}">
<div class="card grid-stack-item-content">
{% if widget.title %}
<div class="card-header text-center text-light bg-secondary p-1">
<div class="float-end pe-1"><i class="mdi mdi-close"></i></div>
<strong>{{ widget.title }}</strong>
</div>
{% endif %}
<div class="card-body p-2">
{% if not widget.title %}
<div class="float-end pe-1"><i class="mdi mdi-close"></i></div>
{% endif %}
{% render_widget widget %}
</div>
</div>
</div>

View File

@ -0,0 +1,4 @@
<div class="htmx-container"
hx-get="{% url 'extras:objectchange_list' %}?sort=-time"
hx-trigger="load"
></div>

View File

@ -0,0 +1,12 @@
{% if counts %}
<div class="list-group list-group-flush">
{% for model, count in counts %}
<a href="#" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between align-items-center">
{{ model|meta:"verbose_name_plural"|bettertitle }}
<h6 class="mb-1">{{ count }}</h6>
</div>
</a>
{% endfor %}
</div>
{% endif %}

View File

@ -23,60 +23,14 @@
{% block title %}Home{% endblock %}
{% block content-wrapper %}
<div class="px-3">
{# General stats #}
<div class="row masonry">
{% for section, items, icon in stats %}
<div class="col col-sm-12 col-lg-6 col-xl-4 my-2 masonry-item">
<div class="card">
<h6 class="card-header text-center">
<i class="mdi mdi-{{ icon }}"></i>
<span class="ms-1">{{ section }}</span>
</h6>
<div class="card-body">
<div class="list-group list-group-flush">
{% for item in items %}
{% if item.permission in perms %}
<a href="{% url item.viewname %}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between align-items-center">
{{ item.label }}
<h4 class="mb-1">{{ item.count }}</h4>
</div>
</a>
{% else %}
<li class="list-group-item list-group-item-action disabled">
<div class="d-flex w-100 justify-content-between align-items-center">
{{ item.label }}
<h4 class="mb-1">
<i title="No permission" class="mdi mdi-lock"></i>
</h4>
</div>
</li>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
<div class="px-2 py-0">
{# Render the user's customized dashboard #}
<div class="grid-stack">
{% for widget in widgets %}
{% include 'extras/dashboard/widget.html' %}
{% endfor %}
</div>
{# Changelog #}
{% if perms.extras.view_objectchange %}
<div class="row my-4 flex-grow-1 changelog-container">
<div class="col">
<div class="card">
<h6 class="card-header text-center">
<i class="mdi mdi-clipboard-clock"></i>
<span class="ms-1">Change Log</span>
</h6>
<div class="card-body htmx-container table-responsive"
hx-get="{% url 'extras:objectchange_list' %}?sort=-time"
hx-trigger="load"
></div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock content-wrapper %}