ui: добавить поиск, фильтрацию и сортировку на списки

API:
- scans.py: добавить search, status, sort_by, sort_dir параметры
- packages.py: добавить search, sort_by, sort_dir параметры
- web/routes.py: передать новые параметры в шаблоны

UI:
- scans_list.html: search input (htmx debounce), status filter dropdown,
  sortable columns с hx-get, empty state
- packages_list.html: search input (htmx debounce), sortable columns,
  empty state
- pagination сохраняет все параметры фильтрации/сортировки
This commit is contained in:
Marker689
2026-05-10 03:12:26 +03:00
parent d00cee3432
commit 00424b494a
5 changed files with 192 additions and 28 deletions

View File

@@ -11,7 +11,14 @@ from guarddog_nexus.database import get_session
from guarddog_nexus.models import Finding, Scan
router = APIRouter(tags=["web"])
TEMPLATES: dict[str, str] = {}
VALID_SORT_FIELDS = {
"id": Scan.id,
"package_name": Scan.package_name,
"started_at": Scan.started_at,
"status": Scan.status,
"total_findings": Scan.total_findings,
}
def _render(name: str, **context) -> HTMLResponse:
@@ -146,6 +153,10 @@ async def scans_list(
request: Request,
page: int = 1,
flagged: str = "",
search: str = "",
status: str = "",
sort_by: str = "started_at",
sort_dir: str = "desc",
session: AsyncSession = Depends(get_session),
):
per_page = 50
@@ -154,7 +165,17 @@ async def scans_list(
q = select(Scan)
if flagged == "1":
q = q.where(Scan.flagged == True)
q = q.order_by(Scan.started_at.desc()).offset(offset).limit(per_page)
if status:
q = q.where(Scan.status == status)
if search:
pattern = f"%{search}%"
q = q.where(
Scan.package_name.ilike(pattern) | Scan.package_version.ilike(pattern)
)
sort_field = VALID_SORT_FIELDS.get(sort_by, Scan.started_at)
q = q.order_by(sort_field.desc() if sort_dir == "desc" else sort_field.asc())
q = q.offset(offset).limit(per_page)
scans = (await session.execute(q)).scalars().all()
total = await session.scalar(select(func.count(Scan.id)))
@@ -166,6 +187,10 @@ async def scans_list(
per_page=per_page,
total=total,
flagged_filter=flagged,
search=search,
status_filter=status,
sort_by=sort_by,
sort_dir=sort_dir,
request=request,
)
@@ -188,6 +213,9 @@ async def packages_list(
request: Request,
page: int = 1,
flagged: str = "",
search: str = "",
sort_by: str = "last_scanned_at",
sort_dir: str = "desc",
session: AsyncSession = Depends(get_session),
):
per_page = 50
@@ -206,12 +234,23 @@ async def packages_list(
if flagged == "1":
subq = subq.having(func.max(Scan.flagged) == True)
if search:
pattern = f"%{search}%"
subq = subq.where(
Scan.package_name.ilike(pattern) | Scan.package_version.ilike(pattern)
)
sort_field = VALID_SORT_FIELDS.get(sort_by, Scan.started_at)
sort_col = func.max(sort_field)
subq = subq.order_by(
sort_col.desc() if sort_dir == "desc" else sort_col.asc()
)
subq = subq.subquery()
total = await session.scalar(select(func.count()).select_from(subq))
rows = (
await session.execute(
select(subq).order_by(subq.c.last_scan.desc()).offset(offset).limit(per_page)
select(subq).offset(offset).limit(per_page)
)
).all()
@@ -222,6 +261,9 @@ async def packages_list(
per_page=per_page,
total=total,
flagged_filter=flagged,
search=search,
sort_by=sort_by,
sort_dir=sort_dir,
request=request,
)