Skip to content

Commit 5f6016a

Browse files
committed
Add Configure Table button with per-user column preferences
Adds a Configure Table button to the Custom Objects tab card header. Authenticated users can show, hide, and reorder the Type, Object, Value, Field, and Tags columns via the standard NetBox modal; preferences are persisted per-user in UserConfig under tables.CustomObjectsTabTable.columns and applied on every page load, including HTMX partial updates. The Actions column is exempt and always visible. Introduces CustomObjectsTabTable (a BaseTable subclass) in views.py used solely for the column-preference machinery — no data is passed through it. Column visibility is resolved from UserConfig on each request and communicated to the templates via the selected_columns context variable. Both templates guard each configurable <th>/<td> pair with {% if 'col' in selected_columns %}.
1 parent 2c4b9e9 commit 5f6016a

6 files changed

Lines changed: 79 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5353
objects with a specific tag. Tag filtering composes with `?q=`, `?type=`, sort, and
5454
pagination. Tags are pre-fetched in bulk (`prefetch_related('tags')`) so there is no
5555
N+1 query cost.
56+
- **Configure Table** — a "Configure Table" button in the card header opens a NetBox
57+
modal that lets authenticated users show, hide, and reorder columns (Type, Object,
58+
Value, Field, Tags). Preferences are persisted per-user in `UserConfig` under
59+
`tables.CustomObjectsTabTable.columns` and respected on every subsequent page load,
60+
including HTMX partial updates. The Actions column is always visible and cannot be
61+
hidden.
5662

5763
### Fixed
5864

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ Action buttons and column links use the `perms` templatetag from `utilities.temp
181181
- [x] 18. Add HTMX partial rendering (paginator, sort headers, search form, type dropdown)
182182
- [x] 19. Fix Edit/Delete return URL to redirect back to the Custom Objects tab
183183
- [x] 20. Add Tags column and tag filter dropdown to the Custom Objects tab
184+
- [x] 21. Add "Configure Table" button with per-user column show/hide/reorder preferences
184185

185186
## Critical Reference Files
186187

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ Object instance:
126126
- **Multi-Object** fields: comma-separated links to the related objects, truncated
127127
at 3 with an ellipsis when more are present.
128128

129+
### Configure Table
130+
A **Configure Table** button in the card header opens a NetBox modal that lets
131+
authenticated users show, hide, and reorder the table columns (Type, Object, Value,
132+
Field, Tags). Preferences are stored per-user in `UserConfig` and respected on every
133+
subsequent page load, including HTMX partial updates. The Actions column is always
134+
visible and cannot be hidden.
135+
129136
### Action buttons
130137
Each row has right-aligned action buttons, shown only when the user has the relevant permission:
131138

netbox_custom_objects_tab/templates/netbox_custom_objects_tab/custom_objects_tab.html

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends base_template %}
2-
{% load i18n custom_object_buttons perms %}
2+
{% load i18n custom_object_buttons perms helpers %}
33

44
{% block content %}
55
<div class="row">
@@ -70,6 +70,15 @@ <h2 class="mb-0">{% trans "Custom Objects" %}</h2>
7070
<input type="hidden" name="per_page" value="{{ request.GET.per_page }}">
7171
{% endif %}
7272
</form>
73+
{% if request.user.is_authenticated %}
74+
<button type="button"
75+
class="btn btn-sm btn-outline-secondary"
76+
data-bs-toggle="modal"
77+
data-bs-target="#CustomObjectsTabTable_config"
78+
title="{% trans 'Configure Table' %}">
79+
<i class="mdi mdi-cog"></i> {% trans "Configure Table" %}
80+
</button>
81+
{% endif %}
7382
</div>
7483

7584
{# --- table zone (swapped by HTMX) --- #}
@@ -78,4 +87,8 @@ <h2 class="mb-0">{% trans "Custom Objects" %}</h2>
7887
</div>
7988
</div>
8089
</div>
90+
{% block modals %}
91+
{{ block.super }}
92+
{% table_config_form tab_table %}
93+
{% endblock modals %}
8194
{% endblock content %}

netbox_custom_objects_tab/templates/netbox_custom_objects_tab/custom_objects_tab_partial.html

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,51 @@
1111
<table class="table table-hover attr-table mb-0">
1212
<thead hx-target="#custom_objects_list" hx-swap="outerHTML" hx-push-url="true">
1313
<tr>
14+
{% if 'type' in selected_columns %}
1415
<th>
1516
<a hx-get="{{ sort_headers.type.url }}">
1617
{% trans "Type" %}
1718
{% if sort_headers.type.icon %}<i class="mdi mdi-{{ sort_headers.type.icon }}"></i>{% endif %}
1819
</a>
1920
</th>
21+
{% endif %}
22+
{% if 'object' in selected_columns %}
2023
<th>
2124
<a hx-get="{{ sort_headers.object.url }}">
2225
{% trans "Object" %}
2326
{% if sort_headers.object.icon %}<i class="mdi mdi-{{ sort_headers.object.icon }}"></i>{% endif %}
2427
</a>
2528
</th>
26-
<th>{% trans "Value" %}</th>
29+
{% endif %}
30+
{% if 'value' in selected_columns %}<th>{% trans "Value" %}</th>{% endif %}
31+
{% if 'field' in selected_columns %}
2732
<th>
28-
<a hx-get="{{ sort_headers.field.url }}">
33+
<a hx-get="{{ sort_headers.field.url %}">
2934
{% trans "Field" %}
3035
{% if sort_headers.field.icon %}<i class="mdi mdi-{{ sort_headers.field.icon }}"></i>{% endif %}
3136
</a>
3237
</th>
33-
<th>{% trans "Tags" %}</th>
38+
{% endif %}
39+
{% if 'tags' in selected_columns %}<th>{% trans "Tags" %}</th>{% endif %}
3440
<th></th>
3541
</tr>
3642
</thead>
3743
<tbody>
3844
{% for obj, field, value in page_rows %}
3945
<tr>
46+
{% if 'type' in selected_columns %}
4047
<td>
4148
{% if request.user|can_view:field.custom_object_type %}
4249
<a href="{{ field.custom_object_type.get_absolute_url }}">{{ field.custom_object_type }}</a>
4350
{% else %}
4451
{{ field.custom_object_type }}
4552
{% endif %}
4653
</td>
54+
{% endif %}
55+
{% if 'object' in selected_columns %}
4756
<td><a href="{{ obj.get_absolute_url }}">{{ obj }}</a></td>
57+
{% endif %}
58+
{% if 'value' in selected_columns %}
4859
<td>
4960
{% if field.type == 'object' %}
5061
{% if value %}
@@ -65,14 +76,19 @@
6576
&mdash;
6677
{% endif %}
6778
</td>
79+
{% endif %}
80+
{% if 'field' in selected_columns %}
6881
<td>{{ field }}</td>
82+
{% endif %}
83+
{% if 'tags' in selected_columns %}
6984
<td>
7085
{% for t in obj.tags.all %}
7186
{% tag t %}{% if not forloop.last %} {% endif %}
7287
{% empty %}
7388
&mdash;
7489
{% endfor %}
7590
</td>
91+
{% endif %}
7692
<td class="text-end text-nowrap">
7793
{% if request.user|can_change:obj %}
7894
<a href="{% url 'plugins:netbox_custom_objects:customobject_edit' pk=obj.pk custom_object_type=obj.custom_object_type.slug %}?return_url={{ return_url|urlencode }}" class="btn btn-yellow" role="button">

netbox_custom_objects_tab/views.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,40 @@
22
from types import SimpleNamespace
33
from urllib.parse import urlencode
44

5+
import django_tables2 as tables2
56
from django.apps import apps
67
from django.contrib.contenttypes.models import ContentType
78
from django.core.paginator import InvalidPage
89
from django.shortcuts import get_object_or_404, render
10+
from django.utils.translation import gettext_lazy as _
911
from django.views.generic import View
1012

1113
from extras.choices import CustomFieldTypeChoices
1214
from netbox.plugins import get_plugin_config
15+
from netbox.tables import BaseTable
1316
from utilities.htmx import htmx_partial
1417
from utilities.paginator import EnhancedPaginator, get_paginate_count
1518
from utilities.views import ViewTab, register_model_view
1619

1720
logger = logging.getLogger('netbox_custom_objects_tab')
1821

22+
23+
class CustomObjectsTabTable(BaseTable):
24+
"""Lightweight table class used only for column-preference machinery."""
25+
type = tables2.Column(verbose_name=_('Type'), orderable=False)
26+
object = tables2.Column(verbose_name=_('Object'), orderable=False)
27+
value = tables2.Column(verbose_name=_('Value'), orderable=False)
28+
field = tables2.Column(verbose_name=_('Field'), orderable=False)
29+
tags = tables2.Column(verbose_name=_('Tags'), orderable=False)
30+
actions = tables2.Column(verbose_name='', orderable=False)
31+
32+
exempt_columns = ('actions',)
33+
34+
class Meta(BaseTable.Meta):
35+
fields = ('type', 'object', 'value', 'field', 'tags', 'actions')
36+
default_columns = ('type', 'object', 'value', 'field', 'tags', 'actions')
37+
38+
1939
# Maximum number of related objects to show in the Value column for MULTIOBJECT fields.
2040
# One extra is fetched to detect truncation without a COUNT query.
2141
_MAX_MULTIOBJECT_DISPLAY = 3
@@ -184,6 +204,16 @@ def get(self, request, pk):
184204
instance = get_object_or_404(qs, pk=pk)
185205
linked_all = _get_linked_custom_objects(instance)
186206

207+
# Build table object for column-preference machinery (no data, just column config)
208+
tab_table = CustomObjectsTabTable([], empty_text='')
209+
visible_cols = None
210+
if request.user.is_authenticated and (userconfig := getattr(request.user, 'config', None)):
211+
visible_cols = userconfig.get(f'tables.{tab_table.name}.columns')
212+
if visible_cols is None:
213+
visible_cols = list(CustomObjectsTabTable.Meta.default_columns)
214+
tab_table._set_columns(visible_cols)
215+
selected_columns = {col for col, _ in tab_table.selected_columns} | set(tab_table.exempt_columns)
216+
187217
# Collect unique types for the dropdown (always from the unfiltered list)
188218
seen_type_pks = set()
189219
available_types = []
@@ -280,6 +310,8 @@ def get(self, request, pk):
280310
'sort_headers': sort_headers,
281311
'htmx_table': SimpleNamespace(htmx_url=request.path, embedded=False),
282312
'return_url': request.get_full_path(),
313+
'tab_table': tab_table,
314+
'selected_columns': selected_columns,
283315
}
284316

285317
if htmx_partial(request):

0 commit comments

Comments
 (0)