Add full search parity and optimize map performance
Search: refactor into modular helpers, add IP/hostname/notification/ vendor/model/type search, smart auto-redirects for exact matches, ServiceNOW prefix detection, filter buttons with counts, share links with highlight support, and dark mode badge colors. Map: fix N+1 queries with eager loading (248->28 queries), switch to canvas-rendered circleMarkers for better performance with 500+ assets. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from flask import Blueprint, request
|
||||
from flask_jwt_extended import jwt_required
|
||||
from sqlalchemy.orm import joinedload, subqueryload
|
||||
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import Asset, AssetType, AssetStatus, AssetRelationship, RelationshipType
|
||||
@@ -73,8 +74,8 @@ def create_asset_type():
|
||||
|
||||
t = AssetType(
|
||||
assettype=data['assettype'],
|
||||
plugin_name=data.get('plugin_name'),
|
||||
table_name=data.get('table_name'),
|
||||
pluginname=data.get('pluginname'),
|
||||
tablename=data.get('tablename'),
|
||||
description=data.get('description'),
|
||||
icon=data.get('icon')
|
||||
)
|
||||
@@ -411,26 +412,26 @@ def get_asset_relationships(asset_id: int):
|
||||
|
||||
# Get outgoing relationships (this asset is source)
|
||||
outgoing = AssetRelationship.query.filter_by(
|
||||
source_assetid=asset_id
|
||||
sourceassetid=asset_id
|
||||
).filter(AssetRelationship.isactive == True).all()
|
||||
|
||||
# Get incoming relationships (this asset is target)
|
||||
incoming = AssetRelationship.query.filter_by(
|
||||
target_assetid=asset_id
|
||||
targetassetid=asset_id
|
||||
).filter(AssetRelationship.isactive == True).all()
|
||||
|
||||
outgoing_data = []
|
||||
for rel in outgoing:
|
||||
r = rel.to_dict()
|
||||
r['target_asset'] = rel.target_asset.to_dict() if rel.target_asset else None
|
||||
r['relationship_type_name'] = rel.relationship_type.relationshiptype if rel.relationship_type else None
|
||||
r['targetasset'] = rel.targetasset.to_dict() if rel.targetasset else None
|
||||
r['relationshiptypename'] = rel.relationshiptype.relationshiptype if rel.relationshiptype else None
|
||||
outgoing_data.append(r)
|
||||
|
||||
incoming_data = []
|
||||
for rel in incoming:
|
||||
r = rel.to_dict()
|
||||
r['source_asset'] = rel.source_asset.to_dict() if rel.source_asset else None
|
||||
r['relationship_type_name'] = rel.relationship_type.relationshiptype if rel.relationship_type else None
|
||||
r['sourceasset'] = rel.sourceasset.to_dict() if rel.sourceasset else None
|
||||
r['relationshiptypename'] = rel.relationshiptype.relationshiptype if rel.relationshiptype else None
|
||||
incoming_data.append(r)
|
||||
|
||||
return success_response({
|
||||
@@ -449,13 +450,13 @@ def create_asset_relationship():
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||
|
||||
# Validate required fields
|
||||
required = ['source_assetid', 'target_assetid', 'relationshiptypeid']
|
||||
required = ['sourceassetid', 'targetassetid', 'relationshiptypeid']
|
||||
for field in required:
|
||||
if not data.get(field):
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, f'{field} is required')
|
||||
|
||||
source_id = data['source_assetid']
|
||||
target_id = data['target_assetid']
|
||||
source_id = data['sourceassetid']
|
||||
target_id = data['targetassetid']
|
||||
type_id = data['relationshiptypeid']
|
||||
|
||||
# Validate assets exist
|
||||
@@ -468,8 +469,8 @@ def create_asset_relationship():
|
||||
|
||||
# Check for duplicate relationship
|
||||
existing = AssetRelationship.query.filter_by(
|
||||
source_assetid=source_id,
|
||||
target_assetid=target_id,
|
||||
sourceassetid=source_id,
|
||||
targetassetid=target_id,
|
||||
relationshiptypeid=type_id
|
||||
).first()
|
||||
|
||||
@@ -481,8 +482,8 @@ def create_asset_relationship():
|
||||
)
|
||||
|
||||
rel = AssetRelationship(
|
||||
source_assetid=source_id,
|
||||
target_assetid=target_id,
|
||||
sourceassetid=source_id,
|
||||
targetassetid=target_id,
|
||||
relationshiptypeid=type_id,
|
||||
notes=data.get('notes')
|
||||
)
|
||||
@@ -536,9 +537,85 @@ def get_assets_map():
|
||||
- locationid: Filter by location ID
|
||||
- search: Search by assetnumber, name, or serialnumber
|
||||
"""
|
||||
from shopdb.core.models import Location, BusinessUnit, MachineType, Machine
|
||||
from shopdb.core.models import Location, BusinessUnit, MachineType, Machine, Communication
|
||||
|
||||
query = Asset.query.filter(
|
||||
# Eager-load all relationships to avoid N+1 queries.
|
||||
# Core relationships via joinedload, extension tables via subqueryload
|
||||
# with their nested relationships (vendor, model, type) also eager-loaded.
|
||||
eager_options = [
|
||||
joinedload(Asset.assettype),
|
||||
joinedload(Asset.status),
|
||||
joinedload(Asset.location),
|
||||
joinedload(Asset.businessunit),
|
||||
]
|
||||
|
||||
# Eager-load plugin extension tables AND their relationships
|
||||
try:
|
||||
from plugins.equipment.models import Equipment
|
||||
eager_options.append(
|
||||
subqueryload(Asset.equipment)
|
||||
.joinedload(Equipment.equipmenttype)
|
||||
)
|
||||
eager_options.append(
|
||||
subqueryload(Asset.equipment)
|
||||
.joinedload(Equipment.vendor)
|
||||
)
|
||||
eager_options.append(
|
||||
subqueryload(Asset.equipment)
|
||||
.joinedload(Equipment.model)
|
||||
)
|
||||
eager_options.append(
|
||||
subqueryload(Asset.equipment)
|
||||
.joinedload(Equipment.controllervendor)
|
||||
)
|
||||
eager_options.append(
|
||||
subqueryload(Asset.equipment)
|
||||
.joinedload(Equipment.controllermodel)
|
||||
)
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
try:
|
||||
from plugins.computers.models import Computer
|
||||
eager_options.append(
|
||||
subqueryload(Asset.computer)
|
||||
.joinedload(Computer.computertype)
|
||||
)
|
||||
eager_options.append(
|
||||
subqueryload(Asset.computer)
|
||||
.joinedload(Computer.operatingsystem)
|
||||
)
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
try:
|
||||
from plugins.network.models import NetworkDevice
|
||||
eager_options.append(
|
||||
subqueryload(Asset.network_device)
|
||||
.joinedload(NetworkDevice.networkdevicetype)
|
||||
)
|
||||
eager_options.append(
|
||||
subqueryload(Asset.network_device)
|
||||
.joinedload(NetworkDevice.vendor)
|
||||
)
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
try:
|
||||
from plugins.printers.models import Printer
|
||||
eager_options.append(
|
||||
subqueryload(Asset.printer)
|
||||
.joinedload(Printer.printertype)
|
||||
)
|
||||
eager_options.append(
|
||||
subqueryload(Asset.printer)
|
||||
.joinedload(Printer.vendor)
|
||||
)
|
||||
eager_options.append(
|
||||
subqueryload(Asset.printer)
|
||||
.joinedload(Printer.model)
|
||||
)
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
query = Asset.query.options(*eager_options).filter(
|
||||
Asset.isactive == True,
|
||||
Asset.mapleft.isnot(None),
|
||||
Asset.maptop.isnot(None)
|
||||
@@ -555,7 +632,6 @@ def get_assets_map():
|
||||
subtype_id = int(subtype_id)
|
||||
asset_type_lower = selected_assettype.lower() if selected_assettype else ''
|
||||
if asset_type_lower == 'equipment':
|
||||
# Filter by equipment type
|
||||
try:
|
||||
from plugins.equipment.models import Equipment
|
||||
query = query.join(Equipment, Equipment.assetid == Asset.assetid).filter(
|
||||
@@ -564,7 +640,6 @@ def get_assets_map():
|
||||
except ImportError:
|
||||
pass
|
||||
elif asset_type_lower == 'computer':
|
||||
# Filter by computer type
|
||||
try:
|
||||
from plugins.computers.models import Computer
|
||||
query = query.join(Computer, Computer.assetid == Asset.assetid).filter(
|
||||
@@ -573,7 +648,6 @@ def get_assets_map():
|
||||
except ImportError:
|
||||
pass
|
||||
elif asset_type_lower == 'network device':
|
||||
# Filter by network device type
|
||||
try:
|
||||
from plugins.network.models import NetworkDevice
|
||||
query = query.join(NetworkDevice, NetworkDevice.assetid == Asset.assetid).filter(
|
||||
@@ -582,7 +656,6 @@ def get_assets_map():
|
||||
except ImportError:
|
||||
pass
|
||||
elif asset_type_lower == 'printer':
|
||||
# Filter by printer type
|
||||
try:
|
||||
from plugins.printers.models import Printer
|
||||
query = query.join(Printer, Printer.assetid == Asset.assetid).filter(
|
||||
@@ -615,7 +688,29 @@ def get_assets_map():
|
||||
|
||||
assets = query.all()
|
||||
|
||||
# Build response with type-specific data
|
||||
# Batch-load primary IPs in a single query instead of N+1 per asset.
|
||||
# Prefer isprimary=True IP, fall back to any IP (comtypeid=1).
|
||||
asset_ids = [a.assetid for a in assets]
|
||||
primary_ip_map = {}
|
||||
if asset_ids:
|
||||
ip_rows = db.session.query(
|
||||
Communication.assetid,
|
||||
Communication.ipaddress,
|
||||
Communication.isprimary
|
||||
).filter(
|
||||
Communication.assetid.in_(asset_ids),
|
||||
Communication.comtypeid == 1,
|
||||
Communication.isactive == True
|
||||
).order_by(
|
||||
Communication.isprimary.desc()
|
||||
).all()
|
||||
|
||||
for row in ip_rows:
|
||||
# First match wins (isprimary=True sorted first)
|
||||
if row.assetid not in primary_ip_map:
|
||||
primary_ip_map[row.assetid] = row.ipaddress
|
||||
|
||||
# Build response - all relationship data is already loaded, no extra queries
|
||||
data = []
|
||||
for asset in assets:
|
||||
item = {
|
||||
@@ -635,36 +730,32 @@ def get_assets_map():
|
||||
'locationid': asset.locationid,
|
||||
'businessunit': asset.businessunit.businessunit if asset.businessunit else None,
|
||||
'businessunitid': asset.businessunitid,
|
||||
'primaryip': asset.primary_ip,
|
||||
'primaryip': primary_ip_map.get(asset.assetid),
|
||||
}
|
||||
|
||||
# Add type-specific data
|
||||
# Extension data is already eager-loaded via lazy='joined' backrefs
|
||||
type_data = asset._get_extension_data()
|
||||
if type_data:
|
||||
item['typedata'] = type_data
|
||||
|
||||
data.append(item)
|
||||
|
||||
# Get available asset types for filters
|
||||
# Get filter options - these are small reference tables, no N+1 concern
|
||||
asset_types = AssetType.query.filter(AssetType.isactive == True).all()
|
||||
types_data = [{'assettypeid': t.assettypeid, 'assettype': t.assettype, 'icon': t.icon} for t in asset_types]
|
||||
|
||||
# Get status options
|
||||
statuses = AssetStatus.query.filter(AssetStatus.isactive == True).all()
|
||||
status_data = [{'statusid': s.statusid, 'status': s.status, 'color': s.color} for s in statuses]
|
||||
|
||||
# Get business units
|
||||
business_units = BusinessUnit.query.filter(BusinessUnit.isactive == True).all()
|
||||
bu_data = [{'businessunitid': bu.businessunitid, 'businessunit': bu.businessunit} for bu in business_units]
|
||||
|
||||
# Get locations
|
||||
locations = Location.query.filter(Location.isactive == True).all()
|
||||
loc_data = [{'locationid': loc.locationid, 'locationname': loc.locationname} for loc in locations]
|
||||
|
||||
# Get subtypes based on asset type categories (keys match database asset type values)
|
||||
# Get subtypes for filter dropdowns
|
||||
subtypes = {}
|
||||
|
||||
# Equipment types from equipment plugin
|
||||
try:
|
||||
from plugins.equipment.models import EquipmentType
|
||||
equipment_types = EquipmentType.query.filter(EquipmentType.isactive == True).order_by(EquipmentType.equipmenttype).all()
|
||||
@@ -672,7 +763,6 @@ def get_assets_map():
|
||||
except ImportError:
|
||||
subtypes['Equipment'] = []
|
||||
|
||||
# Computer types from computers plugin
|
||||
try:
|
||||
from plugins.computers.models import ComputerType
|
||||
computer_types = ComputerType.query.filter(ComputerType.isactive == True).order_by(ComputerType.computertype).all()
|
||||
@@ -680,7 +770,6 @@ def get_assets_map():
|
||||
except ImportError:
|
||||
subtypes['Computer'] = []
|
||||
|
||||
# Network device types
|
||||
try:
|
||||
from plugins.network.models import NetworkDeviceType
|
||||
net_types = NetworkDeviceType.query.filter(NetworkDeviceType.isactive == True).order_by(NetworkDeviceType.networkdevicetype).all()
|
||||
@@ -688,7 +777,6 @@ def get_assets_map():
|
||||
except ImportError:
|
||||
subtypes['Network Device'] = []
|
||||
|
||||
# Printer types
|
||||
try:
|
||||
from plugins.printers.models import PrinterType
|
||||
printer_types = PrinterType.query.filter(PrinterType.isactive == True).order_by(PrinterType.printertype).all()
|
||||
|
||||
Reference in New Issue
Block a user