The rack device modal listed inventory entries in a flat <select>: one line per device, no search, no filters, nothing to tell two look-alike hosts apart. It now opens the Device Inventory modal itself in a picker mode — a card click returns the device instead of approving it, and the bulk/clear controls (plus the s/a/Enter shortcuts) are hidden, since a picker has no business deleting anything. That list also carried VMs, containers and mesh devices, which no rack can hold. A new "Rackable" filter drops them, armed by default when the rack opens the modal. It is an exclusion list, not an allow list, so new hardware types show up by default and an unclassified device (a bare ARP hit) stays visible. `utils/rackable.ts` mirrors `_UNRACKABLE_TYPES` in racks.py — the list the inventory endpoint already filtered on — and test_rackable_sync.py fails the backend suite on drift. The rack side keeps the last word: a device missing from its own inventory, or already mounted in this design, is refused by name rather than mounted as a ghost. Nothing is preselected anymore either, so submitting without picking says so instead of quietly mounting whichever entry happened to be first. Standalone keeps the <select>: the inventory modal reads pending_devices over REST and there is no backend there. The modal itself was max-w-md and one column, which pushed the port list below the fold. It now matches NodeModal's width with the fields on the left and the ports on the right. ha-relevant: maybe
35 lines
1.3 KiB
Python
35 lines
1.3 KiB
Python
"""The rackable exclusion list is duplicated in the frontend — keep them equal.
|
|
|
|
`GET /api/v1/racks/inventory` filters on `_UNRACKABLE_TYPES`, while the Device
|
|
Inventory modal's "Rackable" filter (used as the rack picker's default) filters
|
|
on `UNRACKABLE_TYPES` in `frontend/src/utils/rackable.ts`. If the two drift, the
|
|
picker offers devices the rack side then refuses.
|
|
"""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from app.api.routes.racks import _UNRACKABLE_TYPES
|
|
|
|
FRONTEND_FILE = (
|
|
Path(__file__).resolve().parents[2] / "frontend" / "src" / "utils" / "rackable.ts"
|
|
)
|
|
|
|
|
|
def _frontend_unrackable() -> set[str]:
|
|
src = FRONTEND_FILE.read_text()
|
|
match = re.search(r"export const UNRACKABLE_TYPES = new Set\(\[(.*?)\]\)", src, re.S)
|
|
assert match, (
|
|
"Could not locate UNRACKABLE_TYPES in frontend/src/utils/rackable.ts — "
|
|
"update this parser if the file's shape changed."
|
|
)
|
|
return set(re.findall(r"'([^']+)'", match.group(1)))
|
|
|
|
|
|
def test_unrackable_types_in_sync_with_frontend():
|
|
frontend = _frontend_unrackable()
|
|
assert frontend == _UNRACKABLE_TYPES, (
|
|
"racks.py _UNRACKABLE_TYPES is out of sync with frontend rackable.ts.\n"
|
|
f"Missing from the backend: {sorted(frontend - _UNRACKABLE_TYPES)}\n"
|
|
f"Missing from the frontend: {sorted(_UNRACKABLE_TYPES - frontend)}"
|
|
)
|