mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Implement function to save dashboard layout
This commit is contained in:
parent
fc362979ad
commit
029d22e495
@ -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
|
||||
|
@ -27,4 +27,5 @@ registry = Registry({
|
||||
'plugins': dict(),
|
||||
'search': dict(),
|
||||
'views': collections.defaultdict(dict),
|
||||
'widgets': dict(),
|
||||
})
|
||||
|
@ -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
|
||||
|
BIN
netbox/project-static/dist/config.js
vendored
BIN
netbox/project-static/dist/config.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/config.js.map
vendored
BIN
netbox/project-static/dist/config.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js
vendored
BIN
netbox/project-static/dist/lldp.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js.map
vendored
BIN
netbox/project-static/dist/lldp.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js
vendored
BIN
netbox/project-static/dist/status.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js.map
vendored
BIN
netbox/project-static/dist/status.js.map
vendored
Binary file not shown.
@ -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,
|
||||
|
38
netbox/project-static/src/dashboard.ts
Normal file
38
netbox/project-static/src/dashboard.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
@ -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,
|
||||
|
@ -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">
|
||||
|
@ -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 %}
|
||||
|
Loading…
Reference in New Issue
Block a user