Implement function to save dashboard layout

This commit is contained in:
jeremystretch 2023-02-20 16:15:59 -05:00
parent fc362979ad
commit 029d22e495
15 changed files with 99 additions and 33 deletions

View File

@ -1,21 +1,37 @@
import uuid
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from netbox.registry import registry
__all__ = ( __all__ = (
'ChangeLogWidget', 'ChangeLogWidget',
'DashboardWidget', 'DashboardWidget',
'ObjectCountsWidget', 'ObjectCountsWidget',
'StaticContentWidget', '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: class DashboardWidget:
width = 4 width = 4
height = 3 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 {} self.config = config or {}
if title: if title:
self.title = title self.title = title
@ -28,14 +44,21 @@ class DashboardWidget:
def render(self, request): def render(self, request):
raise NotImplementedError("DashboardWidget subclasses must define a render() method.") 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): class StaticContentWidget(DashboardWidget):
def render(self, request): def render(self, request):
return self.config.get('content', 'Empty!') return self.config.get('content', 'Empty!')
@register_widget
class ObjectCountsWidget(DashboardWidget): class ObjectCountsWidget(DashboardWidget):
title = _('Objects')
template_name = 'extras/dashboard/widgets/objectcounts.html' template_name = 'extras/dashboard/widgets/objectcounts.html'
def render(self, request): def render(self, request):
@ -51,6 +74,7 @@ class ObjectCountsWidget(DashboardWidget):
}) })
@register_widget
class ChangeLogWidget(DashboardWidget): class ChangeLogWidget(DashboardWidget):
width = 12 width = 12
height = 4 height = 4

View File

@ -27,4 +27,5 @@ registry = Registry({
'plugins': dict(), 'plugins': dict(),
'search': dict(), 'search': dict(),
'views': collections.defaultdict(dict), 'views': collections.defaultdict(dict),
'widgets': dict(),
}) })

View File

@ -11,6 +11,7 @@ from packaging import version
from extras import dashboard from extras import dashboard
from netbox.forms import SearchForm from netbox.forms import SearchForm
from netbox.registry import registry
from netbox.search import LookupTypes from netbox.search import LookupTypes
from netbox.search.backends import search_backend from netbox.search.backends import search_backend
from netbox.tables import SearchTable from netbox.tables import SearchTable
@ -32,22 +33,20 @@ class HomeView(View):
if settings.LOGIN_REQUIRED and not request.user.is_authenticated: if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
return redirect('login') return redirect('login')
widgets = ( # Build custom dashboard from user's config
dashboard.StaticContentWidget({ widgets = []
'content': 'First widget!', for grid_item in request.user.config.get('dashboard.layout'):
}), config = request.user.config.get(f"dashboard.widgets.{grid_item['id']}")
dashboard.StaticContentWidget({ widget_class = registry['widgets'].get(config.pop('class'))
'content': 'First widget!', widget = widget_class(
}, title='Testing'), id=grid_item.get('id'),
dashboard.ObjectCountsWidget({ width=grid_item['w'],
'models': [ height=grid_item['h'],
'dcim.Site', x=grid_item['x'],
'ipam.Prefix', y=grid_item['y'],
'tenancy.Tenant', **config
], )
}, title='Stuff'), widgets.append(widget)
dashboard.ChangeLogWidget(),
)
# Check whether a new release is available. (Only for staff/superusers.) # Check whether a new release is available. (Only for staff/superusers.)
new_release = None new_release = None

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,4 @@
import { Collapse, Modal, Popover, Tab, Toast, Tooltip } from 'bootstrap'; import { Collapse, Modal, Popover, Tab, Toast, Tooltip } from 'bootstrap';
import { GridStack } from 'gridstack';
import { createElement, getElements } from './util'; import { createElement, getElements } from './util';
type ToastLevel = 'danger' | 'warning' | 'success' | 'info'; type ToastLevel = 'danger' | 'warning' | 'success' | 'info';
@ -12,10 +11,6 @@ window.Popover = Popover;
window.Toast = Toast; window.Toast = Toast;
window.Tooltip = Tooltip; window.Tooltip = Tooltip;
function initGridStack(): void {
GridStack.init();
}
function initTooltips() { function initTooltips() {
for (const tooltip of getElements('[data-bs-toggle="tooltip"]')) { for (const tooltip of getElements('[data-bs-toggle="tooltip"]')) {
new Tooltip(tooltip, { container: 'body' }); new Tooltip(tooltip, { container: 'body' });
@ -186,7 +181,6 @@ export function initBootstrap(): void {
for (const func of [ for (const func of [
initTooltips, initTooltips,
initModals, initModals,
initGridStack,
initTabs, initTabs,
initImagePreview, initImagePreview,
initSidebarAccordions, initSidebarAccordions,

View File

@ -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<APIResponse<APIUserConfig>> {
let data = {
dashboard: {
layout: gridData
},
}
return await apiPatch<APIUserConfig>(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();
}
});
});
}

View File

@ -10,6 +10,7 @@ import { initDateSelector } from './dateSelector';
import { initTableConfig } from './tableConfig'; import { initTableConfig } from './tableConfig';
import { initInterfaceTable } from './tables'; import { initInterfaceTable } from './tables';
import { initSideNav } from './sidenav'; import { initSideNav } from './sidenav';
import { initDashboard } from './dashboard';
import { initRackElevation } from './racks'; import { initRackElevation } from './racks';
import { initLinks } from './links'; import { initLinks } from './links';
import { initHtmx } from './htmx'; import { initHtmx } from './htmx';
@ -28,6 +29,7 @@ function initDocument(): void {
initTableConfig, initTableConfig,
initInterfaceTable, initInterfaceTable,
initSideNav, initSideNav,
initDashboard,
initRackElevation, initRackElevation,
initLinks, initLinks,
initHtmx, initHtmx,

View File

@ -1,6 +1,13 @@
{% load dashboard %} {% load dashboard %}
<div class="grid-stack-item" gs-w="{{ widget.width }}" gs-h="{{ widget.height }}"> <div
class="grid-stack-item"
gs-w="{{ widget.width }}"
gs-h="{{ widget.height }}"
gs-x="{{ widget.x }}"
gs-y="{{ widget.y }}"
gs-id="{{ widget.id }}"
>
<div class="card grid-stack-item-content"> <div class="card grid-stack-item-content">
{% if widget.title %} {% if widget.title %}
<div class="card-header text-center text-light bg-secondary p-1"> <div class="card-header text-center text-light bg-secondary p-1">

View File

@ -23,14 +23,15 @@
{% block title %}Home{% endblock %} {% block title %}Home{% endblock %}
{% block content-wrapper %} {% block content-wrapper %}
<div class="px-2 py-0"> {# Render the user's customized dashboard #}
<div class="grid-stack">
{# Render the user's customized dashboard #} {% for widget in widgets %}
<div class="grid-stack"> {% include 'extras/dashboard/widget.html' %}
{% for widget in widgets %} {% endfor %}
{% include 'extras/dashboard/widget.html' %} </div>
{% endfor %} <div class="text-end px-2">
</div> <button id="save_dashboard" class="btn btn-primary btn-sm">
<i class="mdi mdi-content-save-outline"></i> Save
</button>
</div> </div>
{% endblock content-wrapper %} {% endblock content-wrapper %}