feat(saved-searches): back saved searches with NetBox native SavedFilter#130
Merged
Conversation
Saved searches in the TUI (Ctrl+W/Ctrl+O) and the CLI `--saved` option now resolve against NetBox's native saved filters (`extras.saved-filters`), scoped to the resource's object type, so they are interchangeable with the web UI: a search saved in nsc appears in the web UI's filter dropdown and vice versa. The object type is resolved exactly from NetBox's own registry (`/api/core/object-types/` `rest_api_endpoint`) rather than guessed by depluralizing the URL, and the endpoint match tolerates sub-path installs, absolute URLs, and casing. Filter params translate between nsc's flat `dict[str, str]` and NetBox's list-valued `parameters`. Saves upsert by name (create, else PATCH, preserving an existing slug). When NetBox is unreachable or the user lacks permission on `extras.savedfilter`, saves/loads transparently fall back to the previous local `config.yaml` store and surface a notice instead of failing. TUI store calls run off the Textual event loop (threaded, like ListScreen) so the UI never freezes. Closes #129. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6 tasks
This was referenced Jun 29, 2026
Merged
thomaschristory
added a commit
that referenced
this pull request
Jun 29, 2026
… polish (#132, #133, #134) (#135) * feat(savedfilters): custom-field + tags resolvers for TUI label/widget use Mirror the #130 ObjectTypeResolver: fetch-once-per-scope, cache, return None on failure. CustomFieldResolver maps custom_fields.<name> -> label/type/choices scoped by object type; TagsResolver fetches the global tag list. custom_field_labels builds a display-label map with collision/fallback handling. Foundation for #132, #134. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tui): render list-of-object fields in detail view like the list table (#133) The detail view stringified list values (tags, other M2M/list fields) to a raw list-of-dicts repr. Route them through the shared flatten/render_cell pipeline so they show a comma-joined display string, colored when object colors are enabled, matching the list table. Promotes view._cell to public render_cell. Closes #133 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tui,output): show custom-field labels in column headers and chooser (#132) Custom-field columns showed the raw custom_fields.<name> key everywhere. Add a display-label layer (CustomFieldResolver + custom_field_labels) that maps the key to the field's NetBox label for: TUI list headers, the column chooser, and CLI table/CSV headers. The raw key stays the selector/persistence key, so --columns and saved prefs are unchanged. Labels resolve only when a custom-field column is shown; missing defs, offline, unknown fields, and label collisions all fall back to the raw key. Closes #132 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tui): bulk-edit polish, per-field custom fields, tags multi-select (#134) - Expand custom_fields into one typed widget per field (text/number/switch/ select/multiselect) in both the single EditForm and the BulkEditForm, via a shared forms.py layer. Values stage flat as custom_fields.<name> and are folded back into a nested custom_fields object before the PATCH/POST. - Render tags as a SelectionList multi-select populated from /api/extras/tags/, submitting the NetBox-writable [{name, slug}] list. Single edit only stages a tags change when the selection actually differs; bulk replaces (Set tags to…). - Polish the bulk form layout (CSS) to align include-toggle + label + widget with the single-edit experience. - Widget ids encode the custom_fields dot as a hyphen (Textual rejects dots). - Degrade gracefully to the prior single text inputs when tag/custom-field definitions can't be fetched. Closes #134 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tui,cli): address adversarial review of #132/#134 - EditForm: pre-seed custom-field multiselect widgets from the record's current values (previously only tags seeded; cf multiselects rendered empty). Stage a multiselect change only when the value set actually differs, so an unchanged pre-seeded selection — regardless of order — never forces a spurious patch. - CLI: relabel custom-field column headers in 'get' and create/update response output too (handle_get + _render_response), deriving the collection path from a detail path so the resolver matches. Matches handle_list; gated on a cf column. - BulkEditForm: encode FK widget/button ids consistently at creation (Textual rejects dots), matching how they are queried back. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Thomas Christory <tchristory@partner.auchan.fr> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #129.
What
Saved searches are now backed by NetBox's native saved filters
(
extras.saved-filters) instead of a local-onlyconfig.yamlstore, making theminterchangeable with the web UI: a search saved in
nsc(TUICtrl+W, orapplied via
--saved) shows up in the NetBox web UI's filter dropdown for thatobject — and a filter created in the web UI loads in the TUI (
Ctrl+O).How
nsc/savedfilters/package (framework-free):objecttypes.py— resolves a resource's list path to itsapp_label.modelobject type using NetBox's own registry (
/api/core/object-types/,rest_api_endpoint). This is exact, not a depluralization guess (devices'schema is
DeviceWithConfigContext, the URL isip-addresses→ipaddress,etc.), and the match tolerates sub-path installs, absolute URLs, and casing.
params.py— translates between nsc's flatdict[str, str]and NetBox'slist-valued
parameters, plus slug generation.store.py—NativeSavedFilterStore: CRUD onextras.saved-filters(
object_typesplural on writes,object_typesingular filter on reads),upsert-by-name (create, else PATCH preserving an existing slug), with a
config.yaml fallback.
config.yamlfallback (ConfigFileSavedSearchStore): when NetBox isunreachable or the user lacks permission on
extras.savedfilter, saves/loadsdegrade to the previous local store and surface a notice rather than
failing silently.
NscTuiApp's three saved-search methods route through the store;the
tuicommand and the--savedCLI option construct it. TUI store callsrun off the Textual event loop (threaded, like
ListScreen) so the UI neverfreezes.
Notes / decisions
shared=True, enabled=Trueso they're visible in theweb UI like a normal saved filter (a private filter without a resolvable owner
can be invisible).
first value; an explicit re-save overwrites with the current state (documented
in
params.py)./api/core/object-types/, resolution returnsNoneand the local fallbackis used.
Testing
tests/savedfilters/(params, object-type resolution incl.sub-path/casing, store CRUD + upsert + API-error fallback + duplicate-name
handling),
tests/config/test_saved_search_fallback.py,tests/tui/test_saved_filter_integration.py.tests/cli/test_registration.py,tests/cli/test_tui_command.py,tests/tui/test_filter_screen.pyfor the new backing + threaded workers.ruff+mypy --strictclean.endpoint mismatch, event-loop blocking, duplicate-name silent drop, fallback
test gaps).
🤖 Generated with Claude Code