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:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user