Skip to content

feat(saved-searches): back saved searches with NetBox native SavedFilter#130

Merged
thomaschristory merged 1 commit into
mainfrom
feat/native-saved-filters
Jun 28, 2026
Merged

feat(saved-searches): back saved searches with NetBox native SavedFilter#130
thomaschristory merged 1 commit into
mainfrom
feat/native-saved-filters

Conversation

@thomaschristory

Copy link
Copy Markdown
Owner

Closes #129.

What

Saved searches are now backed by NetBox's native saved filters
(extras.saved-filters) instead of a local-only config.yaml store, making them
interchangeable with the web UI: a search saved in nsc (TUI Ctrl+W, or
applied via --saved) shows up in the NetBox web UI's filter dropdown for that
object — and a filter created in the web UI loads in the TUI (Ctrl+O).

How

  • New nsc/savedfilters/ package (framework-free):
    • objecttypes.py — resolves a resource's list path to its app_label.model
      object 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 is ip-addressesipaddress,
      etc.), and the match tolerates sub-path installs, absolute URLs, and casing.
    • params.py — translates between nsc's flat dict[str, str] and NetBox's
      list-valued parameters, plus slug generation.
    • store.pyNativeSavedFilterStore: CRUD on extras.saved-filters
      (object_types plural on writes, object_type singular filter on reads),
      upsert-by-name (create, else PATCH preserving an existing slug), with a
      config.yaml fallback.
  • config.yaml fallback (ConfigFileSavedSearchStore): when NetBox is
    unreachable or the user lacks permission on extras.savedfilter, saves/loads
    degrade to the previous local store and surface a notice rather than
    failing silently.
  • Wiring: NscTuiApp's three saved-search methods route through the store;
    the tui command and the --saved CLI option construct it. TUI store calls
    run off the Textual event loop (threaded, like ListScreen) so the UI never
    freezes.

Notes / decisions

  • New filters are created shared=True, enabled=True so they're visible in the
    web UI like a normal saved filter (a private filter without a resolvable owner
    can be invisible).
  • nsc's filter state is one value per key, so a web-UI multi-select reads as its
    first value; an explicit re-save overwrites with the current state (documented
    in params.py).
  • Targets NetBox 4.x (bundled schemas 4.5/4.6); on older servers without
    /api/core/object-types/, resolution returns None and the local fallback
    is used.

Testing

  • New unit tests: 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.
  • Updated tests/cli/test_registration.py, tests/cli/test_tui_command.py,
    tests/tui/test_filter_screen.py for the new backing + threaded workers.
  • Full suite green (1279 passed, 1 skipped); ruff + mypy --strict clean.
  • Hardened against findings from an adversarial multi-agent review (sub-path
    endpoint mismatch, event-loop blocking, duplicate-name silent drop, fallback
    test gaps).

🤖 Generated with Claude Code

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>
@thomaschristory thomaschristory merged commit 2cedeac into main Jun 28, 2026
11 checks passed
@thomaschristory thomaschristory deleted the feat/native-saved-filters branch June 28, 2026 22:49
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Back saved searches with NetBox native SavedFilter (extras.saved-filters) instead of local config.yaml

1 participant