Add "View Networks": front-facing subnet browse + detail with attached devices
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:
@@ -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'])
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user