Warranties: fix N+1 slowness, filter alignment, and Covers hover
- Perf: list_warranties did a db.session.get(Asset) per link per warranty (~1.8s for the full list). Eager-load the links and batch-fetch every linked asset in one query -> ~0.19s. - Filters: the "Status" label wrapped its select onto a second line, so the dropdown sat above the search box; keep the label inline so they align. - Covers: each asset chip now shows the asset name (often the hostname/alias) on hover, keeping the machine number as the label.
This commit is contained in:
@@ -51,7 +51,7 @@
|
|||||||
<td>{{ w.enddate ? formatDate(w.enddate) : '-' }}</td>
|
<td>{{ w.enddate ? formatDate(w.enddate) : '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="!w.assets.length" class="muted">-</span>
|
<span v-if="!w.assets.length" class="muted">-</span>
|
||||||
<router-link v-for="a in w.assets" :key="a.assetid" :to="assetLink(a)" class="asset-chip">{{ a.assetnumber }}</router-link>
|
<router-link v-for="a in w.assets" :key="a.assetid" :to="assetLink(a)" class="asset-chip" :title="a.name || a.assetnumber">{{ a.assetnumber }}</router-link>
|
||||||
</td>
|
</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button v-if="w.provider !== 'manual'" class="btn btn-secondary btn-sm" @click="refresh(w)">Refresh</button>
|
<button v-if="w.provider !== 'manual'" class="btn btn-secondary btn-sm" @click="refresh(w)">Refresh</button>
|
||||||
@@ -350,6 +350,9 @@ async function refresh(w) {
|
|||||||
.muted { color: var(--text-light); }
|
.muted { color: var(--text-light); }
|
||||||
.status-badge { padding: 0.15rem 0.6rem; border-radius: 12px; font-size: 0.78rem; font-weight: 600; }
|
.status-badge { padding: 0.15rem 0.6rem; border-radius: 12px; font-size: 0.78rem; font-weight: 600; }
|
||||||
.filters { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
.filters { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
||||||
|
/* Keep the "Status" label + its select on one line so it aligns with the
|
||||||
|
single-line search box next to it. */
|
||||||
|
.filters label { display: inline-flex; align-items: center; gap: 0.4rem; }
|
||||||
.filters .form-control { max-width: 320px; }
|
.filters .form-control { max-width: 320px; }
|
||||||
.result-count { color: var(--text-light); font-size: 0.85rem; }
|
.result-count { color: var(--text-light); font-size: 0.85rem; }
|
||||||
.servicelevel-cell { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.servicelevel-cell { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from datetime import date, datetime, timezone
|
|||||||
|
|
||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
from shopdb.api import (
|
from shopdb.api import (
|
||||||
db, Asset,
|
db, Asset,
|
||||||
@@ -41,12 +42,15 @@ def _asset_summary(asset):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _warranty_payload(warranty, today=None):
|
def _warranty_payload(warranty, today=None, assetmap=None):
|
||||||
"""to_dict plus the linked-asset summaries."""
|
"""to_dict plus the linked-asset summaries. Pass assetmap (assetid -> Asset)
|
||||||
|
to avoid a per-link query when serializing a list; without it, falls back to
|
||||||
|
a per-link get (fine for a single warranty)."""
|
||||||
data = warranty.to_dict(today)
|
data = warranty.to_dict(today)
|
||||||
assets = []
|
assets = []
|
||||||
for link in warranty.links:
|
for link in warranty.links:
|
||||||
asset = db.session.get(Asset, link.assetid)
|
asset = assetmap.get(link.assetid) if assetmap is not None \
|
||||||
|
else db.session.get(Asset, link.assetid)
|
||||||
if asset:
|
if asset:
|
||||||
assets.append(_asset_summary(asset))
|
assets.append(_asset_summary(asset))
|
||||||
data['assets'] = assets
|
data['assets'] = assets
|
||||||
@@ -86,10 +90,17 @@ def list_warranties():
|
|||||||
if assetid:
|
if assetid:
|
||||||
query = (query.join(WarrantyAsset, WarrantyAsset.warrantyid == Warranty.warrantyid)
|
query = (query.join(WarrantyAsset, WarrantyAsset.warrantyid == Warranty.warrantyid)
|
||||||
.filter(WarrantyAsset.assetid == assetid))
|
.filter(WarrantyAsset.assetid == assetid))
|
||||||
warranties = query.order_by(Warranty.enddate.is_(None), Warranty.enddate).all()
|
warranties = (query.options(joinedload(Warranty.links))
|
||||||
|
.order_by(Warranty.enddate.is_(None), Warranty.enddate).all())
|
||||||
|
|
||||||
|
# Batch-fetch every linked asset in ONE query (was N+1: a db.session.get per
|
||||||
|
# link per warranty, ~1.8s for the full list).
|
||||||
|
assetids = {link.assetid for w in warranties for link in w.links}
|
||||||
|
assetmap = ({a.assetid: a for a in Asset.query.filter(Asset.assetid.in_(assetids)).all()}
|
||||||
|
if assetids else {})
|
||||||
|
|
||||||
today = date.today()
|
today = date.today()
|
||||||
items = [_warranty_payload(w, today) for w in warranties]
|
items = [_warranty_payload(w, today, assetmap) for w in warranties]
|
||||||
|
|
||||||
status_filter = request.args.get('status')
|
status_filter = request.args.get('status')
|
||||||
if status_filter:
|
if status_filter:
|
||||||
|
|||||||
Reference in New Issue
Block a user