mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-02 15:37:18 +02:00
Adapt custom actions panel for declarative layout system
Convert the ObjectPermission detail view to use the new panel-based layout from #21568. Add ObjectPermissionCustomActionsPanel that cross-references assigned object types with the model_actions registry to display which models each custom action applies to. Also fix dark-mode visibility of disabled action checkboxes in the permission form by overriding Bootstrap's disabled opacity.
This commit is contained in:
8
netbox/project-static/dist/netbox.js
vendored
8
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
8
netbox/project-static/dist/netbox.js.map
vendored
8
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -30,13 +30,20 @@ export function initRegisteredActions(): void {
|
||||
const enabled = modelKey !== null && selectedModels.has(modelKey);
|
||||
const el = group as HTMLElement;
|
||||
|
||||
el.style.opacity = enabled ? '1' : '0.4';
|
||||
|
||||
// Toggle disabled on checkboxes within the group
|
||||
// Toggle disabled on checkboxes, overriding Bootstrap's disabled opacity
|
||||
// to keep them visible in dark mode
|
||||
for (const checkbox of Array.from(
|
||||
el.querySelectorAll<HTMLInputElement>('input[type="checkbox"]'),
|
||||
)) {
|
||||
checkbox.disabled = !enabled;
|
||||
checkbox.style.opacity = enabled ? '' : '0.75';
|
||||
}
|
||||
|
||||
// Fade text for disabled groups
|
||||
for (const label of Array.from(
|
||||
el.querySelectorAll<HTMLElement>('small, .form-check-label'),
|
||||
)) {
|
||||
label.style.opacity = enabled ? '' : '0.5';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,101 +1,5 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load i18n %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}{% trans "Permission" %} {{ object.name }}{% endblock %}
|
||||
|
||||
{% block subtitle %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Permission" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Enabled" %}</th>
|
||||
<td>{% checkmark object.enabled %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Actions" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "View" %}</th>
|
||||
<td>{% checkmark object.can_view %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Add" %}</th>
|
||||
<td>{% checkmark object.can_add %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Change" %}</th>
|
||||
<td>{% checkmark object.can_change %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Delete" %}</th>
|
||||
<td>{% checkmark object.can_delete %}</td>
|
||||
</tr>
|
||||
{% for action in object.actions %}
|
||||
{% if action not in reserved_actions %}
|
||||
<tr>
|
||||
<th scope="row">{{ action }}</th>
|
||||
<td>{% checkmark True %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Constraints" %}</h2>
|
||||
<div class="card-body">
|
||||
{% if object.constraints %}
|
||||
<pre>{{ object.constraints|json }}</pre>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Object Types" %}</h2>
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for user in object.object_types.all %}
|
||||
<li class="list-group-item">{{ user }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Assigned Users" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for user in object.users.all %}
|
||||
<a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Assigned Groups" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for group in object.groups.all %}
|
||||
<a href="{% url 'users:group' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
20
netbox/templates/users/panels/custom_actions.html
Normal file
20
netbox/templates/users/panels/custom_actions.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block panel_content %}
|
||||
<table class="table table-hover attr-table">
|
||||
{% for action, models in custom_actions %}
|
||||
<tr>
|
||||
<th scope="row">{{ action }}</th>
|
||||
<td>
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
{% checkmark True %}
|
||||
{% if models %}
|
||||
<small class="text-muted">{{ models }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock panel_content %}
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.registry import registry
|
||||
from netbox.ui import actions, attrs, panels
|
||||
from users.constants import RESERVED_ACTIONS
|
||||
|
||||
|
||||
class TokenPanel(panels.ObjectAttributesPanel):
|
||||
@@ -54,6 +56,46 @@ class ObjectPermissionActionsPanel(panels.ObjectAttributesPanel):
|
||||
can_delete = attrs.BooleanAttr('can_delete', label=_('Delete'))
|
||||
|
||||
|
||||
class ObjectPermissionCustomActionsPanel(panels.ObjectPanel):
|
||||
"""
|
||||
A panel which displays non-CRUD (custom) actions assigned to an ObjectPermission.
|
||||
"""
|
||||
template_name = 'users/panels/custom_actions.html'
|
||||
title = _('Custom Actions')
|
||||
|
||||
def get_context(self, context):
|
||||
obj = context['object']
|
||||
custom_actions = [a for a in obj.actions if a not in RESERVED_ACTIONS]
|
||||
|
||||
# Build a list of (action_name, model_labels) tuples from the registry,
|
||||
# scoped to the object types assigned to this permission.
|
||||
assigned_types = {
|
||||
f'{ot.app_label}.{ot.model}' for ot in obj.object_types.all()
|
||||
}
|
||||
action_models = {}
|
||||
for model_key, model_actions in registry['model_actions'].items():
|
||||
if model_key in assigned_types:
|
||||
for action in model_actions:
|
||||
if action.name in custom_actions:
|
||||
action_models.setdefault(action.name, []).append(model_key)
|
||||
|
||||
custom_actions_display = [
|
||||
(action, ', '.join(action_models.get(action, [])))
|
||||
for action in custom_actions
|
||||
]
|
||||
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'custom_actions': custom_actions_display,
|
||||
}
|
||||
|
||||
def render(self, context):
|
||||
ctx = self.get_context(context)
|
||||
if not ctx['custom_actions']:
|
||||
return ''
|
||||
return super().render(context)
|
||||
|
||||
|
||||
class OwnerPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
group = attrs.RelatedObjectAttr('group', linkify=True)
|
||||
|
||||
@@ -19,7 +19,6 @@ from utilities.query import count_related
|
||||
from utilities.views import GetRelatedModelsMixin, register_model_view
|
||||
|
||||
from . import filtersets, forms, tables
|
||||
from .constants import RESERVED_ACTIONS
|
||||
from .models import Group, ObjectPermission, Owner, OwnerGroup, Token, User
|
||||
|
||||
#
|
||||
@@ -273,6 +272,7 @@ class ObjectPermissionView(generic.ObjectView):
|
||||
left_panels=[
|
||||
panels.ObjectPermissionPanel(),
|
||||
panels.ObjectPermissionActionsPanel(),
|
||||
panels.ObjectPermissionCustomActionsPanel(),
|
||||
JSONPanel('constraints', title=_('Constraints')),
|
||||
],
|
||||
right_panels=[
|
||||
@@ -286,11 +286,6 @@ class ObjectPermissionView(generic.ObjectView):
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'reserved_actions': RESERVED_ACTIONS,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ObjectPermission, 'add', detail=False)
|
||||
@register_model_view(ObjectPermission, 'edit')
|
||||
|
||||
Reference in New Issue
Block a user