mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-22 08:48:41 +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 enabled = modelKey !== null && selectedModels.has(modelKey);
|
||||||
const el = group as HTMLElement;
|
const el = group as HTMLElement;
|
||||||
|
|
||||||
el.style.opacity = enabled ? '1' : '0.4';
|
// Toggle disabled on checkboxes, overriding Bootstrap's disabled opacity
|
||||||
|
// to keep them visible in dark mode
|
||||||
// Toggle disabled on checkboxes within the group
|
|
||||||
for (const checkbox of Array.from(
|
for (const checkbox of Array.from(
|
||||||
el.querySelectorAll<HTMLInputElement>('input[type="checkbox"]'),
|
el.querySelectorAll<HTMLInputElement>('input[type="checkbox"]'),
|
||||||
)) {
|
)) {
|
||||||
checkbox.disabled = !enabled;
|
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' %}
|
{% extends 'generic/object.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load helpers %}
|
|
||||||
{% load render_table from django_tables2 %}
|
|
||||||
|
|
||||||
{% block title %}{% trans "Permission" %} {{ object.name }}{% endblock %}
|
{% block title %}{% trans "Permission" %} {{ object.name }}{% endblock %}
|
||||||
|
|
||||||
{% block subtitle %}{% 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 django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from netbox.registry import registry
|
||||||
from netbox.ui import actions, attrs, panels
|
from netbox.ui import actions, attrs, panels
|
||||||
|
from users.constants import RESERVED_ACTIONS
|
||||||
|
|
||||||
|
|
||||||
class TokenPanel(panels.ObjectAttributesPanel):
|
class TokenPanel(panels.ObjectAttributesPanel):
|
||||||
@@ -54,6 +56,46 @@ class ObjectPermissionActionsPanel(panels.ObjectAttributesPanel):
|
|||||||
can_delete = attrs.BooleanAttr('can_delete', label=_('Delete'))
|
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):
|
class OwnerPanel(panels.ObjectAttributesPanel):
|
||||||
name = attrs.TextAttr('name')
|
name = attrs.TextAttr('name')
|
||||||
group = attrs.RelatedObjectAttr('group', linkify=True)
|
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 utilities.views import GetRelatedModelsMixin, register_model_view
|
||||||
|
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .constants import RESERVED_ACTIONS
|
|
||||||
from .models import Group, ObjectPermission, Owner, OwnerGroup, Token, User
|
from .models import Group, ObjectPermission, Owner, OwnerGroup, Token, User
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -273,6 +272,7 @@ class ObjectPermissionView(generic.ObjectView):
|
|||||||
left_panels=[
|
left_panels=[
|
||||||
panels.ObjectPermissionPanel(),
|
panels.ObjectPermissionPanel(),
|
||||||
panels.ObjectPermissionActionsPanel(),
|
panels.ObjectPermissionActionsPanel(),
|
||||||
|
panels.ObjectPermissionCustomActionsPanel(),
|
||||||
JSONPanel('constraints', title=_('Constraints')),
|
JSONPanel('constraints', title=_('Constraints')),
|
||||||
],
|
],
|
||||||
right_panels=[
|
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, 'add', detail=False)
|
||||||
@register_model_view(ObjectPermission, 'edit')
|
@register_model_view(ObjectPermission, 'edit')
|
||||||
|
|||||||
Reference in New Issue
Block a user