diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c05588a0f..5e00a6553 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -18,9 +18,9 @@ from extras.views import ObjectConfigContextView, ObjectRenderConfigView from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable from netbox.object_actions import * -from netbox.ui import layout +from netbox.ui import actions, layout from netbox.ui.panels import ( - CommentsPanel, CustomFieldsPanel, EmbeddedTablePanel, ImageAttachmentsPanel, PluginContentPanel, + CommentsPanel, CustomFieldsPanel, ImageAttachmentsPanel, ObjectsTablePanel, PluginContentPanel, RelatedObjectsPanel, TagsPanel, ) from netbox.views import generic @@ -485,19 +485,24 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView): ), layout.Row( layout.Column( - EmbeddedTablePanel( - 'dcim:location_list', - url_params={'site_id': lambda x: x.pk}, - title=_('Locations') + ObjectsTablePanel( + model='dcim.Location', + filters={'site_id': lambda obj: obj.pk}, + actions=[ + actions.AddObject('dcim.Location', url_params={'site': lambda obj: obj.pk}), + ], ), - EmbeddedTablePanel( - 'dcim:device_list', - url_params={ - 'site_id': lambda x: x.pk, + ObjectsTablePanel( + model='dcim.Device', + title=_('Non-Racked Devices'), + filters={ + 'site_id': lambda obj: obj.pk, 'rack_id': settings.FILTERS_NULL_CHOICE_VALUE, 'parent_bay_id': settings.FILTERS_NULL_CHOICE_VALUE, }, - title=_('Non-Racked Devices') + actions=[ + actions.AddObject('dcim.Device', url_params={'site': lambda obj: obj.pk}), + ], ), PluginContentPanel('full_width_page'), ), diff --git a/netbox/netbox/ui/actions.py b/netbox/netbox/ui/actions.py index 0b21b9071..79fd9a5d6 100644 --- a/netbox/netbox/ui/actions.py +++ b/netbox/netbox/ui/actions.py @@ -32,6 +32,8 @@ class PanelAction: url_params = { k: v(obj) if callable(v) else v for k, v in self.url_params.items() } + if 'return_url' not in url_params: + url_params['return_url'] = obj.get_absolute_url() url = f'{url}?{urlencode(url_params)}' return url @@ -49,8 +51,11 @@ class AddObject(PanelAction): button_icon = 'plus-thick' def __init__(self, model, label=None, url_params=None): + # Resolve the model class from its app.name label app_label, model_name = model.split('.') model = apps.get_model(app_label, model_name) view_name = get_viewname(model, 'add') super().__init__(view_name=view_name, label=label, url_params=url_params) + + # Require "add" permission on the model by default self.permissions = [get_permission_for_model(model, 'add')] diff --git a/netbox/netbox/ui/panels.py b/netbox/netbox/ui/panels.py index 195dcfd3c..2ff495c42 100644 --- a/netbox/netbox/ui/panels.py +++ b/netbox/netbox/ui/panels.py @@ -1,5 +1,6 @@ from abc import ABC, ABCMeta +from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.template.loader import render_to_string from django.utils.translation import gettext_lazy as _ @@ -9,14 +10,15 @@ from netbox.ui.attrs import Attr from utilities.querydict import dict_to_querydict from utilities.string import title from utilities.templatetags.plugins import _get_registered_content +from utilities.views import get_viewname __all__ = ( 'CommentsPanel', 'CustomFieldsPanel', - 'EmbeddedTablePanel', 'ImageAttachmentsPanel', 'NestedGroupObjectPanel', 'ObjectPanel', + 'ObjectsTablePanel', 'RelatedObjectsPanel', 'Panel', 'PluginContentPanel', @@ -130,9 +132,33 @@ class RelatedObjectsPanel(Panel): }) -class ImageAttachmentsPanel(Panel): - template_name = 'ui/panels/image_attachments.html' - title = _('Image Attachments') +class ObjectsTablePanel(Panel): + template_name = 'ui/panels/objects_table.html' + title = None + + def __init__(self, model, filters=None, **kwargs): + super().__init__(**kwargs) + + # Resolve the model class from its app.name label + app_label, model_name = model.split('.') + self.model = apps.get_model(app_label, model_name) + self.filters = filters or {} + if self.title is None: + self.title = title(self.model._meta.verbose_name_plural) + + def get_context(self, obj): + url_params = { + k: v(obj) if callable(v) else v for k, v in self.filters.items() + } + if 'return_url' not in url_params: + url_params['return_url'] = obj.get_absolute_url() + return { + 'viewname': get_viewname(self.model, 'list'), + 'url_params': dict_to_querydict(url_params), + } + + +class ImageAttachmentsPanel(ObjectsTablePanel): actions = [ actions.AddObject( 'extras.imageattachment', @@ -145,25 +171,8 @@ class ImageAttachmentsPanel(Panel): ), ] - -class EmbeddedTablePanel(Panel): - template_name = 'ui/panels/embedded_table.html' - title = None - - def __init__(self, view_name, url_params=None, **kwargs): - super().__init__(**kwargs) - self.view_name = view_name - self.url_params = url_params or {} - - def get_context(self, obj): - url_params = { - k: v(obj) if callable(v) else v for k, v in self.url_params.items() - } - # url_params['return_url'] = return_url or context['request'].path - return { - 'viewname': self.view_name, - 'url_params': dict_to_querydict(url_params), - } + def __init__(self, **kwargs): + super().__init__('extras.imageattachment', **kwargs) class PluginContentPanel(Panel): diff --git a/netbox/templates/ui/panels/image_attachments.html b/netbox/templates/ui/panels/image_attachments.html deleted file mode 100644 index 0b6ecdf80..000000000 --- a/netbox/templates/ui/panels/image_attachments.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "ui/panels/_base.html" %} -{% load i18n %} - -{# TODO: Add "attach an image" button in panel header #} -{% block panel_content %} - {% htmx_table 'extras:imageattachment_list' object_type_id=object|content_type_id object_id=object.pk %} -{% endblock panel_content %} diff --git a/netbox/templates/ui/panels/objects_table.html b/netbox/templates/ui/panels/objects_table.html new file mode 100644 index 000000000..64579705f --- /dev/null +++ b/netbox/templates/ui/panels/objects_table.html @@ -0,0 +1,5 @@ +{% extends "ui/panels/_base.html" %} + +{% block panel_content %} + {% include 'builtins/htmx_table.html' %} +{% endblock panel_content %}