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.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

View File

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

View File

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

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 { 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,

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 { 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,

View File

@ -1,6 +1,13 @@
{% 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">
{% if widget.title %}
<div class="card-header text-center text-light bg-secondary p-1">

View File

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