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:
@@ -1,7 +1,7 @@
|
||||
"""REST API for packages (distinct packages across scans)."""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func, select, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from guarddog_nexus.database import get_session
|
||||
@@ -9,6 +9,14 @@ from guarddog_nexus.models import Finding, Scan
|
||||
|
||||
router = APIRouter(prefix="/api/v1/packages", tags=["packages"])
|
||||
|
||||
VALID_SORT_FIELDS = {
|
||||
"name": Scan.package_name,
|
||||
"version": Scan.package_version,
|
||||
"last_scanned_at": Scan.started_at,
|
||||
"total_findings": Scan.total_findings,
|
||||
"flagged": Scan.flagged,
|
||||
}
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_packages(
|
||||
@@ -16,6 +24,10 @@ async def list_packages(
|
||||
offset: int = Query(0, ge=0),
|
||||
ecosystem: str | None = Query(None),
|
||||
flagged: bool | None = Query(None),
|
||||
search: str | None = Query(None),
|
||||
repository: str | None = Query(None),
|
||||
sort_by: str = Query("last_scanned_at"),
|
||||
sort_dir: str = Query("desc"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
subq = select(
|
||||
@@ -33,14 +45,26 @@ async def list_packages(
|
||||
subq = subq.where(Scan.ecosystem == ecosystem)
|
||||
if flagged is not None:
|
||||
subq = subq.having(func.max(Scan.flagged) == flagged)
|
||||
if repository:
|
||||
subq = subq.where(Scan.repository == repository)
|
||||
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_dir = "asc" if sort_dir.lower() == "asc" else "desc"
|
||||
sort_col = func.max(sort_field)
|
||||
subq = subq.order_by(
|
||||
sort_col.desc() if sort_dir == "desc" else sort_col.asc()
|
||||
)
|
||||
|
||||
total_q = select(func.count()).select_from(subq.subquery())
|
||||
total = await session.scalar(total_q)
|
||||
|
||||
rows = (
|
||||
await session.execute(
|
||||
subq.order_by(func.max(Scan.started_at).desc()).offset(offset).limit(limit)
|
||||
)
|
||||
await session.execute(subq.offset(offset).limit(limit))
|
||||
).all()
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user