Metadata-Version: 2.2
Name: netbox-scope-switcher
Version: 1.0.1
Summary: Configurable multi-dimension scope selector in the NetBox navbar that scopes list views by tenant, site, region, tag, and more.
Author-email: Du10777 <du103682@gmail.com>
License: Apache-2.0
Project-URL: Homepage, https://github.com/Du10777/netbox-scope-switcher
Project-URL: Repository, https://github.com/Du10777/netbox-scope-switcher
Project-URL: Documentation, https://github.com/Du10777/netbox-scope-switcher#readme
Project-URL: Issues, https://github.com/Du10777/netbox-scope-switcher/issues
Project-URL: Changelog, https://github.com/Du10777/netbox-scope-switcher/blob/main/CHANGELOG.md
Keywords: netbox,netbox-plugin,plugin,scope,tenant,site,region,tag,multitenancy,filter,dcim,ipam
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Networking
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE

# NetBox Scope Switcher

**Pick your scope once — tenant, region, site, tag, whatever you configure — and
every list in NetBox narrows to it, with the filters cascading on each other.**

`netbox-scope-switcher` adds one compact, configurable selector to the top
navigation bar. Choose values across any number of *scope dimensions* (grouped
however you like) and, as you move between sections (Devices, Prefixes, VLANs,
Sites, …), each list view is automatically scoped to your selection.

It generalizes
[`netbox-tenant-switcher`](https://github.com/Du10777/netbox-tenant-switcher):
tenant is just the default dimension.

## Screenshots

![Grouped scope panel with a tenant selected; the Site list has cascaded to that tenant](docs/img/cascade.png)

<p>
  <img src="docs/img/panel.png" width="340" alt="Grouped dimensions: Tenant / Location / Tag">
  <img src="docs/img/i18n.png" width="340" alt="Labels follow the NetBox UI language">
</p>

## Features

- **Configurable, grouped dimensions.** Scope by tenant, tenant group, region,
  site group, site, location, tag, or any filter you declare in `PLUGINS_CONFIG`
  — arranged under your own group headings. No code changes.
- **Cascading dependent filters.** Selecting values in one dimension narrows the
  option lists of the others, in both directions: pick a *Tenant Group* and only
  its tenants remain; pick a *Tenant* and Regions/Sites/Racks shrink to where that
  tenant is present. Powered by NetBox's own filtersets (so tree descendants and
  every relationship are honoured), with no hard-coded model knowledge.
- **Token-style control.** One dropdown with a section per dimension: selected
  values shown as removable chips, a `+ add` picker with instant search, an `Any`
  placeholder, a `None` option (for objects with no value), per-dimension and
  global *clear*. Chips stay name-sorted. No external JS dependencies.
- **Localized.** Dimension labels come straight from each model's NetBox
  `verbose_name`, and the service words are translated via NetBox's catalog, so
  the panel follows the active UI language automatically.
- **Resizable & viewport-bounded panel.** Drag the corner grip to resize (the
  size is remembered for the session); the panel never grows past the screen and
  scrolls internally.
- **Honest URLs.** By default the plugin issues a single redirect that adds the
  filter params to the address bar, so links are shareable, NetBox's own filter
  panel shows them, and *Clear filters* behaves predictably. A `silent` mode is
  available.
- **Per-scope applicability.** Each dimension is applied only on list views whose
  filterset actually supports it — a prefixes list may scope by tenant and tag but
  not region — computed by introspection, not hard-coding. The control dims when
  nothing applies to the current page.
- **Combining semantics:** several values within a dimension are OR-combined;
  different dimensions are AND-combined; `None` maps to `?param=null`.
- **Permission-aware.** Only values the user may view are listed and selectable,
  and scoping only ever *narrows* results — NetBox object permissions still apply
  on top.
- **No models, no migrations.** Selection lives in the session.

## Compatibility

| Plugin version | NetBox version | Python      |
| -------------- | -------------- | ----------- |
| 1.0.x          | 4.6            | 3.10 – 3.12 |

Developed and verified against **NetBox 4.6**, using only stable plugin APIs
(`PluginTemplateExtension.navbar()` / `head()`, plugin middleware). The CI matrix
(`.github/workflows/ci.yml`) additionally exercises 4.4 and 4.5.

## Installation

```bash
source /opt/netbox/venv/bin/activate
pip install netbox-scope-switcher
```

Enable it in `configuration.py`:

```python
PLUGINS = ["netbox_scope_switcher"]
```

Restart NetBox (`sudo systemctl restart netbox netbox-rq`). No migrations needed.

### Docker (netbox-docker)

See [`deploy/`](deploy/) for a ready-to-use `Dockerfile-Plugins`,
`docker-compose.override.yml`, and `plugins.py`.

## Configuration

Out of the box the plugin scopes by **tenant only** (matching
`netbox-tenant-switcher`). To configure dimensions and grouping, set
`PLUGINS_CONFIG`:

```python
PLUGINS_CONFIG = {
    "netbox_scope_switcher": {
        "mode": "redirect",  # or "silent"
        "scopes": [
            {"param": "tenant_group_id", "model": "tenancy.TenantGroup", "group": "Tenant"},
            {"param": "tenant_id",       "model": "tenancy.Tenant",      "group": "Tenant"},
            {"param": "region_id",       "model": "dcim.Region",         "group": "Location"},
            {"param": "site_group_id",   "model": "dcim.SiteGroup",      "group": "Location"},
            {"param": "site_id",         "model": "dcim.Site",           "group": "Location"},
            {"param": "location_id",     "model": "dcim.Location",       "group": "Location"},
            {"param": "tag",             "model": "extras.Tag",          "group": "Tag",
             "value_field": "slug"},
        ],
    }
}
```

### `PLUGINS_CONFIG` keys

| Key      | Type   | Default     | Description |
| -------- | ------ | ----------- | ----------- |
| `scopes` | list   | tenant only | Ordered list of scope dimensions (below). |
| `mode`   | string | `redirect`  | `redirect` (add params to the URL via a 302) or `silent` (rewrite the request in place). |

Each entry in `scopes`:

| Field         | Required | Default            | Description |
| ------------- | -------- | ------------------ | ----------- |
| `param`       | yes      | —                  | GET parameter of the filter to apply (e.g. `tenant_id`, `tag`). |
| `model`       | yes      | —                  | `app_label.ModelName` the dropdown values come from. |
| `group`       | no       | —                  | Group heading to list this dimension under (translated via NetBox). |
| `label`       | no       | model verbose name | Section heading. Omit it to follow the active UI language. |
| `value_field` | no       | `pk`               | Value to put in the param: `pk` or `slug` (tags filter by slug). |

The configuration is validated at startup: an unknown model or field raises a
clear `ImproperlyConfigured` error on load, rather than a 500 at runtime.

## How it works

- **Navbar widget** (`template_content.py`) — a `PluginTemplateExtension` renders
  the control + a section per dimension via `navbar()`; CSS/JS are injected once
  via `head()`. Options are fetched from the values endpoint on open (so they can
  cascade); only the static `None` option is server-rendered.
- **Set endpoint** (`views.py`) — `GET /plugins/scope-switcher/set/` stores the
  chosen values per dimension in the session and redirects back. `next` is
  validated against the current host (no open redirects); every value is checked
  against the user's view permission.
- **Values endpoint** (`views.py`) —
  `GET /plugins/scope-switcher/values/<param>/?q=…&<other>=…` returns a
  permission-scoped, cascaded, searchable JSON option list.
- **Cascade** (`cascade.py`) — narrows a dimension's options to those consistent
  with the current selection in the others. A *forward* pass filters a model by
  every other scope its own filterset supports (Site by Region, Rack by Location,
  …); a *reverse* pass handles the "upward" direction by pivoting through a bridge
  model that has a FK to the target and whose filterset does support the source
  (Site bridges Region↔Tenant, Tenant bridges Tenant Group↔Tenant, …).
- **Applicability** (`applicability.py`) — introspects the current list view's
  filterset (`get_filters()` / `base_filters`) to decide which configured params
  it supports; used to dim the control and to choose what to apply.
- **Middleware** (`middleware.py`) — for `ObjectListView` pages, appends one
  `?param=<value>` per selected value for each supported, not-already-present
  dimension. `redirect` mode issues a single 302; `silent` mode rewrites
  `request.GET`. API, GraphQL, and HTMX requests are skipped; existing query
  params (pagination, sort, per_page) are preserved.

## Differences from netbox-tenant-switcher

| | `netbox-tenant-switcher` | `netbox-scope-switcher` |
| --- | --- | --- |
| Dimensions | tenant only (hard-coded) | any, configurable and grouped |
| Cascade | — | filters narrow each other, both directions |
| Control | one tenant field | one control, a section + chips per dimension |
| Localization | — | labels/service words follow the UI language |
| URL handling | redirect | redirect (default) or silent |
| Session state | list of tenant ids | `{param: [values]}` dict |

### Migrating

1. `pip uninstall netbox-tenant-switcher` and remove it from `PLUGINS`.
2. `pip install netbox-scope-switcher`, add `"netbox_scope_switcher"` to `PLUGINS`.
3. Do nothing else for identical behaviour — the default config is the tenant
   scope. Add more dimensions to `PLUGINS_CONFIG` when you want them.

Sessions do not carry over (different key/shape); users simply re-pick their
scope once.

## Testing

Tests live in `netbox_scope_switcher/tests/` and run inside a NetBox checkout:

```bash
python netbox/netbox/manage.py test netbox_scope_switcher -v 2
```

See `.github/workflows/ci.yml` for the full CI setup.

## License

[Apache-2.0](LICENSE).
