Skip to content

feat(search): local saved filter sets (saved searches)#123

Merged
thomaschristory merged 3 commits into
mainfrom
feat/saved-searches
Jun 26, 2026
Merged

feat(search): local saved filter sets (saved searches)#123
thomaschristory merged 3 commits into
mainfrom
feat/saved-searches

Conversation

@thomaschristory

Copy link
Copy Markdown
Owner

What

Adds LOCAL, named, persisted filter sets ("saved searches") per resource, mirroring the existing columns column-prefs pattern end to end.

A saved search is stored as the raw FilterState.as_params() dict (key=value), so it round-trips back through the TUI filter screen AND through parse_filters on the CLI. Kept framework-free and reusable by both surfaces.

Changes

  • config: new Config.saved_searches: dict[tag][resource][name] = {filter_key: value} field; new framework-free nsc/config/saved_searches.py with get_saved_search / list_saved_searches.
  • writer: _save_search / _delete_search round-trip persisters in tui_commands (set_path / unset_path under acquire_lock + atomic_write, pruning empty parents).
  • TUI: FilterScreen gains Ctrl+W (save current state under a typed name) and Ctrl+O (load a saved set into the current FilterState + chips); new SavedSearchNamePrompt / SavedSearchPicker modals. NscTuiApp gains saved_searches_for / save_search / delete_search updating the in-memory map and calling write-through callbacks. tag/resource threaded into FilterScreen from ListScreen.
  • CLI: --saved <name> global read flag on list ops. Resolves against config.saved_searches[tag][resource][name]; saved params are the base layer, explicit --filter and typed query options override them; unknown name → typer.BadParameter, exit 2.
  • keymap/help/README: filter-context bindings added to the single-source-of-truth keymap; README documents the feature and its distinction from server-side extras saved-filters.

Precedence

Saved params merge UNDER explicit overrides: a typed query option or a --filter for the same key wins over the saved value.

Notes

  • This is a LOCAL feature, deliberately distinct from NetBox's server-side /api/extras/saved-filters/ (different value format, per-user/instance, not available offline). Documented in --saved help text and README.
  • Like columns, a config written by a newer nsc with saved_searches will not load on an older nsc (extra='forbid'). Consistent with existing behavior.
  • FK filters persist as {name}_id=<id>; loading restores the id but not the human-readable FK label (chips show the id). Acceptable for v1.

Tests

TDD (red-first). New/updated tests:

  • tests/config/test_models.py, tests/config/test_saved_searches.py
  • tests/cli/test_tui_command.py (persist/delete), tests/cli/test_registration.py (resolve, override precedence, unknown-name exit 2)
  • tests/tui/test_app.py, tests/tui/test_filter_screen.py (save callback, blank-name no-op, load repopulates), tests/tui/test_keymap.py

just lint (ruff + mypy --strict) and just test (1172 passed, 1 skipped) both green.

Closes #119

🤖 Generated with Claude Code

Persist named, per-resource filter sets locally under `saved_searches` in
config, mirroring the existing `columns` column-prefs pattern end to end.

- config: new `Config.saved_searches` field + framework-free
  `get_saved_search`/`list_saved_searches` helpers.
- writer: `_save_search`/`_delete_search` round-trip persisters in tui_commands.
- TUI: FilterScreen gains Ctrl+W (save current state under a name) and Ctrl+O
  (load a saved set); NscTuiApp gains `saved_searches_for`/`save_search`/
  `delete_search` with write-through callbacks; new name-prompt + picker modals.
- CLI: `--saved <name>` on list ops resolves saved params as the base layer;
  explicit `--filter` and typed query options override; unknown name exits 2.
- keymap/help/README updated; distinct from server-side `extras saved-filters`.

Closes #119

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thomaschristory

Copy link
Copy Markdown
Owner Author

Claiming: fixing the 3 code-review findings (dotted-name config corruption, missing TUI delete affordance, FilterScreen BINDINGS consistency) on branch feat/saved-searches. TDD, failing tests first.

Saved searches persist at the dotted config path
`saved_searches.<tag>.<resource>.<name>`, and the writer splits on `.`. A
name containing `.` (e.g. `prod.v2`) was written as a nested map instead of a
leaf, so config.yaml then failed `Config.model_validate` on the next run,
bricking every subsequent nsc invocation.

Add a framework-free `validate_saved_search_name` that rejects names
containing `.`, control characters, or surrounding whitespace. Enforce it at
the point of entry (FilterScreen, with a user-facing error notification) and
defensively in the `_save_search` persister before any disk write.

Also wire a delete affordance into SavedSearchPicker: a `d` key binding
dismisses with a typed `SavedSearchChoice("delete", name)` that FilterScreen
routes to the existing `delete_search` plumbing, which previously had no UI
path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thomaschristory

Copy link
Copy Markdown
Owner Author

Pushed fixes (1eed396):

  • BLOCKING (config corruption): added framework-free validate_saved_search_name (nsc/config/saved_searches.py) rejecting names with ., control chars, or surrounding whitespace. Enforced at entry (FilterScreen.action_save_search → error notification, no save) AND defensively in the _save_search persister before any disk write. Failing-first test proves a dotted name (prod.v2) no longer corrupts config.yaml.
  • MAJOR (dead delete path): wired a delete affordance — d key binding in SavedSearchPicker dismisses with a typed SavedSearchChoice("delete", name) routed to the existing delete_search plumbing. Integration test covers it.
  • MINOR (BINDINGS): skipped intentionally. FilterScreen is a ModalScreen that deliberately overrides global keys (escape→cancel, not go_back) and must NOT inherit globals like q→quit into a text-input form; deriving from textual_bindings('filter') would inject those and break typing — a real regression, which the finding said to skip.

lint + full suite green (1189 passed, 1 skipped).

@thomaschristory thomaschristory merged commit b4d6592 into main Jun 26, 2026
12 checks passed
@thomaschristory thomaschristory deleted the feat/saved-searches branch June 26, 2026 19:05
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.

TUI/CLI: support saved searches (saved filter sets)

1 participant