mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Initial work on dashboard widgets
This commit is contained in:
parent
b25c349a27
commit
fc362979ad
61
netbox/extras/dashboard.py
Normal file
61
netbox/extras/dashboard.py
Normal 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, {})
|
11
netbox/extras/templatetags/dashboard.py
Normal file
11
netbox/extras/templatetags/dashboard.py
Normal 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)
|
@ -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,
|
||||
})
|
||||
|
||||
|
18
netbox/templates/extras/dashboard/widget.html
Normal file
18
netbox/templates/extras/dashboard/widget.html
Normal 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>
|
4
netbox/templates/extras/dashboard/widgets/changelog.html
Normal file
4
netbox/templates/extras/dashboard/widgets/changelog.html
Normal file
@ -0,0 +1,4 @@
|
||||
<div class="htmx-container"
|
||||
hx-get="{% url 'extras:objectchange_list' %}?sort=-time"
|
||||
hx-trigger="load"
|
||||
></div>
|
12
netbox/templates/extras/dashboard/widgets/objectcounts.html
Normal file
12
netbox/templates/extras/dashboard/widgets/objectcounts.html
Normal 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 %}
|
@ -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 %}
|
||||
|
Loading…
Reference in New Issue
Block a user