diff --git a/netbox/extras/dashboard.py b/netbox/extras/dashboard.py index aa1889e90..557452af1 100644 --- a/netbox/extras/dashboard.py +++ b/netbox/extras/dashboard.py @@ -1,21 +1,37 @@ +import uuid + from django.contrib.contenttypes.models import ContentType from django.template.loader import render_to_string from django.utils.translation import gettext as _ +from netbox.registry import registry + __all__ = ( 'ChangeLogWidget', 'DashboardWidget', 'ObjectCountsWidget', 'StaticContentWidget', + 'register_widget', ) +def register_widget(cls): + """ + Decorator for registering a DashboardWidget class. + """ + label = f'{cls.__module__}.{cls.__name__}' + registry['widgets'][label] = cls + + return cls + + class DashboardWidget: width = 4 height = 3 - def __init__(self, config=None, title=None, width=None, height=None, x=None, y=None): + def __init__(self, id=None, config=None, title=None, width=None, height=None, x=None, y=None): + self.id = id or uuid.uuid4() self.config = config or {} if title: self.title = title @@ -28,14 +44,21 @@ class DashboardWidget: def render(self, request): raise NotImplementedError("DashboardWidget subclasses must define a render() method.") + @property + def name(self): + return f'{self.__class__.__module__}.{self.__class__.__name__}' + +@register_widget class StaticContentWidget(DashboardWidget): def render(self, request): return self.config.get('content', 'Empty!') +@register_widget class ObjectCountsWidget(DashboardWidget): + title = _('Objects') template_name = 'extras/dashboard/widgets/objectcounts.html' def render(self, request): @@ -51,6 +74,7 @@ class ObjectCountsWidget(DashboardWidget): }) +@register_widget class ChangeLogWidget(DashboardWidget): width = 12 height = 4 diff --git a/netbox/netbox/registry.py b/netbox/netbox/registry.py index e37ee0d0c..23b9ad4cb 100644 --- a/netbox/netbox/registry.py +++ b/netbox/netbox/registry.py @@ -27,4 +27,5 @@ registry = Registry({ 'plugins': dict(), 'search': dict(), 'views': collections.defaultdict(dict), + 'widgets': dict(), }) diff --git a/netbox/netbox/views/misc.py b/netbox/netbox/views/misc.py index 738c048d9..337862f78 100644 --- a/netbox/netbox/views/misc.py +++ b/netbox/netbox/views/misc.py @@ -11,6 +11,7 @@ from packaging import version from extras import dashboard from netbox.forms import SearchForm +from netbox.registry import registry from netbox.search import LookupTypes from netbox.search.backends import search_backend from netbox.tables import SearchTable @@ -32,22 +33,20 @@ class HomeView(View): if settings.LOGIN_REQUIRED and not request.user.is_authenticated: return redirect('login') - 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(), - ) + # Build custom dashboard from user's config + widgets = [] + for grid_item in request.user.config.get('dashboard.layout'): + config = request.user.config.get(f"dashboard.widgets.{grid_item['id']}") + widget_class = registry['widgets'].get(config.pop('class')) + widget = widget_class( + id=grid_item.get('id'), + width=grid_item['w'], + height=grid_item['h'], + x=grid_item['x'], + y=grid_item['y'], + **config + ) + widgets.append(widget) # Check whether a new release is available. (Only for staff/superusers.) new_release = None diff --git a/netbox/project-static/dist/config.js b/netbox/project-static/dist/config.js index 73f9e83cd..02e2c5518 100644 Binary files a/netbox/project-static/dist/config.js and b/netbox/project-static/dist/config.js differ diff --git a/netbox/project-static/dist/config.js.map b/netbox/project-static/dist/config.js.map index cdf87c20f..65dcddcf2 100644 Binary files a/netbox/project-static/dist/config.js.map and b/netbox/project-static/dist/config.js.map differ diff --git a/netbox/project-static/dist/lldp.js b/netbox/project-static/dist/lldp.js index 1e84d7a84..77430ea57 100644 Binary files a/netbox/project-static/dist/lldp.js and b/netbox/project-static/dist/lldp.js differ diff --git a/netbox/project-static/dist/lldp.js.map b/netbox/project-static/dist/lldp.js.map index 566b47027..d7a46d320 100644 Binary files a/netbox/project-static/dist/lldp.js.map and b/netbox/project-static/dist/lldp.js.map differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 303f424cf..864245150 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/status.js b/netbox/project-static/dist/status.js index 3e8dd27da..cf9cd63ab 100644 Binary files a/netbox/project-static/dist/status.js and b/netbox/project-static/dist/status.js differ diff --git a/netbox/project-static/dist/status.js.map b/netbox/project-static/dist/status.js.map index 4a9e03570..6073a6bd7 100644 Binary files a/netbox/project-static/dist/status.js.map and b/netbox/project-static/dist/status.js.map differ diff --git a/netbox/project-static/src/bs.ts b/netbox/project-static/src/bs.ts index b36a0cf83..ecc99ba1a 100644 --- a/netbox/project-static/src/bs.ts +++ b/netbox/project-static/src/bs.ts @@ -1,5 +1,4 @@ import { Collapse, Modal, Popover, Tab, Toast, Tooltip } from 'bootstrap'; -import { GridStack } from 'gridstack'; import { createElement, getElements } from './util'; type ToastLevel = 'danger' | 'warning' | 'success' | 'info'; @@ -12,10 +11,6 @@ window.Popover = Popover; window.Toast = Toast; window.Tooltip = Tooltip; -function initGridStack(): void { - GridStack.init(); -} - function initTooltips() { for (const tooltip of getElements('[data-bs-toggle="tooltip"]')) { new Tooltip(tooltip, { container: 'body' }); @@ -186,7 +181,6 @@ export function initBootstrap(): void { for (const func of [ initTooltips, initModals, - initGridStack, initTabs, initImagePreview, initSidebarAccordions, diff --git a/netbox/project-static/src/dashboard.ts b/netbox/project-static/src/dashboard.ts new file mode 100644 index 000000000..8498dfa4f --- /dev/null +++ b/netbox/project-static/src/dashboard.ts @@ -0,0 +1,38 @@ +import { GridStack, GridStackOptions, GridStackWidget } from 'gridstack'; +import { createToast } from './bs'; +import { apiPatch, hasError } from './util'; + +async function saveDashboardLayout( + url: string, + gridData: GridStackWidget[] | GridStackOptions, +): Promise> { + let data = { + dashboard: { + layout: gridData + }, + } + return await apiPatch(url, data); +} + +export function initDashboard(): void { + // Initialize the grid + let grid = GridStack.init(); + + // Create a listener for the dashboard save button + const gridSaveButton = document.getElementById('save_dashboard') as HTMLButtonElement; + if (gridSaveButton === null) { + return; + } + gridSaveButton.addEventListener('click', () => { + const url = '/api/users/config/'; + let gridData = grid.save(false); + saveDashboardLayout(url, gridData).then(res => { + if (hasError(res)) { + const toast = createToast('danger', 'Error Saving Dashboard Config', res.error); + toast.show(); + } else { + location.reload(); + } + }); + }); +} diff --git a/netbox/project-static/src/netbox.ts b/netbox/project-static/src/netbox.ts index f19b879fe..ed294e655 100644 --- a/netbox/project-static/src/netbox.ts +++ b/netbox/project-static/src/netbox.ts @@ -10,6 +10,7 @@ import { initDateSelector } from './dateSelector'; import { initTableConfig } from './tableConfig'; import { initInterfaceTable } from './tables'; import { initSideNav } from './sidenav'; +import { initDashboard } from './dashboard'; import { initRackElevation } from './racks'; import { initLinks } from './links'; import { initHtmx } from './htmx'; @@ -28,6 +29,7 @@ function initDocument(): void { initTableConfig, initInterfaceTable, initSideNav, + initDashboard, initRackElevation, initLinks, initHtmx, diff --git a/netbox/templates/extras/dashboard/widget.html b/netbox/templates/extras/dashboard/widget.html index 41c610179..cd95564ed 100644 --- a/netbox/templates/extras/dashboard/widget.html +++ b/netbox/templates/extras/dashboard/widget.html @@ -1,6 +1,13 @@ {% load dashboard %} -
+
{% if widget.title %}
diff --git a/netbox/templates/home.html b/netbox/templates/home.html index f6cb96d14..bf20c1f2c 100644 --- a/netbox/templates/home.html +++ b/netbox/templates/home.html @@ -23,14 +23,15 @@ {% block title %}Home{% endblock %} {% block content-wrapper %} -
- - {# Render the user's customized dashboard #} -
- {% for widget in widgets %} - {% include 'extras/dashboard/widget.html' %} - {% endfor %} -
- + {# Render the user's customized dashboard #} +
+ {% for widget in widgets %} + {% include 'extras/dashboard/widget.html' %} + {% endfor %} +
+
+
{% endblock content-wrapper %}