Add "View Networks": front-facing subnet browse + detail with attached devices
Some checks failed
CI / backend (push) Successful in 1m38s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s
CI / migrations-mysql (push) Failing after 7s

Subnets previously lived only under Settings, easily confused with the Network
Devices asset list. Add a front-facing browse + detail:

- Nav: rename "Network" -> "Network Devices"; add "View Networks" (subnets), both
  under Assets (network plugin get_navigation_items; frontend fallback matched).
- /networks (SubnetsBrowse): all subnets with name / CIDR / type / VLAN / notes,
  searchable, row-click to detail.
- /networks/:id (SubnetDetail): the subnet (CIDR, network address, type, VLAN,
  gateway, notes) plus the network devices whose primary IP falls inside its
  CIDR - get_subnet now computes that membership in Python (a device's IP lives
  in a Communication row, so it is not a plain SQL join).

Verified on the import DB: 37 networks list (real WJ subnets), detail renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-13 14:06:47 -04:00
parent 5f51aa2383
commit dd541fba0a
6 changed files with 219 additions and 4 deletions

View File

@@ -814,7 +814,8 @@ def list_subnets():
@network_bp.route('/subnets/<int:subnet_id>', methods=['GET'])
@jwt_required(optional=True)
def get_subnet(subnet_id: int):
"""Get a single subnet."""
"""Get a single subnet plus the network devices whose primary IP falls in
its CIDR range."""
subnet = db.session.get(Subnet, subnet_id)
if not subnet:
@@ -824,7 +825,42 @@ def get_subnet(subnet_id: int):
http_code=404
)
return success_response(subnet.to_dict())
data = subnet.to_dict()
data['devices'] = _devices_in_subnet(subnet.cidr)
return success_response(data)
def _devices_in_subnet(cidr):
"""Network devices (with their primary IP) whose IP is inside cidr. Matching
is done in Python because a device's IP lives in a Communication row, not a
column, so it cannot be a simple SQL join."""
import ipaddress
try:
net = ipaddress.ip_network(cidr, strict=False)
except (ValueError, TypeError):
return []
result = []
for netdev in NetworkDevice.query.all():
ip = _primary_ip(netdev.assetid)
if not ip:
continue
try:
if ipaddress.ip_address(ip.strip()) in net:
asset = netdev.asset
result.append({
'networkdeviceid': netdev.networkdeviceid,
'assetid': netdev.assetid,
'assetnumber': asset.assetnumber if asset else None,
'name': asset.name if asset else None,
'hostname': netdev.hostname,
'ipaddress': ip,
'networkdevicetypename': (netdev.networkdevicetype.networkdevicetype
if netdev.networkdevicetype else None),
})
except (ValueError, TypeError):
continue
result.sort(key=lambda d: d['ipaddress'])
return result
@network_bp.route('/subnets', methods=['POST'])

View File

@@ -208,11 +208,17 @@ class NetworkPlugin(BasePlugin):
"""Return navigation menu items."""
return [
{
'name': 'Network',
'name': 'Network Devices',
'icon': 'network-wired',
'route': '/network',
'position': 18,
},
{
'name': 'View Networks',
'icon': 'globe',
'route': '/networks',
'position': 19,
},
]
def get_permissions(self) -> List: