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.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
|
||||||
|
@ -27,4 +27,5 @@ registry = Registry({
|
|||||||
'plugins': dict(),
|
'plugins': dict(),
|
||||||
'search': dict(),
|
'search': dict(),
|
||||||
'views': collections.defaultdict(dict),
|
'views': collections.defaultdict(dict),
|
||||||
|
'widgets': dict(),
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
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 { 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,
|
||||||
|
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 { 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,
|
||||||
|
@ -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">
|
||||||
|
@ -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 %}
|
||||||
|
Loading…
Reference in New Issue
Block a user