From 00424b494a1e11882868639ffca6beadc281c1ed Mon Sep 17 00:00:00 2001 From: Marker689 Date: Sun, 10 May 2026 03:12:26 +0300 Subject: [PATCH] =?UTF-8?q?ui:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA,=20=D1=84=D0=B8?= =?UTF-8?q?=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BE=D1=80=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D1=83=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 сохраняет все параметры фильтрации/сортировки --- guarddog_nexus/api/packages.py | 32 ++++++++-- guarddog_nexus/api/scans.py | 31 +++++++++- guarddog_nexus/web/routes.py | 48 +++++++++++++- .../web/templates/packages_list.html | 47 +++++++++++--- guarddog_nexus/web/templates/scans_list.html | 62 +++++++++++++++---- 5 files changed, 192 insertions(+), 28 deletions(-) diff --git a/guarddog_nexus/api/packages.py b/guarddog_nexus/api/packages.py index c3cb9af..11ae274 100644 --- a/guarddog_nexus/api/packages.py +++ b/guarddog_nexus/api/packages.py @@ -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 { diff --git a/guarddog_nexus/api/scans.py b/guarddog_nexus/api/scans.py index d43e680..4c45b83 100644 --- a/guarddog_nexus/api/scans.py +++ b/guarddog_nexus/api/scans.py @@ -10,18 +10,47 @@ from guarddog_nexus.models import Finding, Scan router = APIRouter(prefix="/api/v1/scans", tags=["scans"]) +VALID_SORT_FIELDS = { + "id": Scan.id, + "package_name": Scan.package_name, + "started_at": Scan.started_at, + "status": Scan.status, + "total_findings": Scan.total_findings, + "flagged": Scan.flagged, +} + @router.get("") async def list_scans( limit: int = Query(50, le=200), offset: int = Query(0, ge=0), flagged: bool | None = Query(None), + search: str | None = Query(None), + status: str | None = Query(None), + repository: str | None = Query(None), + sort_by: str = Query("started_at"), + sort_dir: str = Query("desc"), session: AsyncSession = Depends(get_session), ): q = select(Scan) + if flagged is not None: q = q.where(Scan.flagged == flagged) - q = q.order_by(Scan.started_at.desc()).offset(offset).limit(limit) + if status: + q = q.where(Scan.status == status) + if repository: + q = q.where(Scan.repository == repository) + 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) + sort_dir = "asc" if sort_dir.lower() == "asc" else "desc" + q = q.order_by(sort_field.desc() if sort_dir == "desc" else sort_field.asc()) + + q = q.offset(offset).limit(limit) total = await session.scalar(select(func.count(Scan.id))) diff --git a/guarddog_nexus/web/routes.py b/guarddog_nexus/web/routes.py index 364522c..bf08770 100644 --- a/guarddog_nexus/web/routes.py +++ b/guarddog_nexus/web/routes.py @@ -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, ) diff --git a/guarddog_nexus/web/templates/packages_list.html b/guarddog_nexus/web/templates/packages_list.html index 629f7b5..c00e921 100644 --- a/guarddog_nexus/web/templates/packages_list.html +++ b/guarddog_nexus/web/templates/packages_list.html @@ -1,23 +1,41 @@ {% extends "base.html" %} +{% block title %}Packages — GuardDog Nexus{% endblock %} +{% block breadcrumbs %} + +{% endblock %} {% block content %}

Packages

-

- +

+ + {% if flagged_filter == '1' %}Show all{% else %}Flagged only{% endif %} -

+
+
- + - - - + + + @@ -32,17 +50,28 @@ {% endfor %} + {% if not packages %} + + + + {% endif %}
Name + Name {% if sort_by == 'name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %} + Version Ecosystem RepoFlaggedFindingsLast Scan + Flagged {% if sort_by == 'flagged' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %} + + Findings {% if sort_by == 'total_findings' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %} + + Last Scan {% if sort_by == 'last_scanned_at' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %} +
{{ p.last_scan.strftime('%Y-%m-%d %H:%M') if p.last_scan }}
+ +

No packages found

+ Try adjusting your search or filters. +
+
{% set total_pages = (total // per_page) + (1 if total % per_page else 0) %} {% if total_pages > 1 %} {% endif %} +{{ total }} total packages {% endblock %} diff --git a/guarddog_nexus/web/templates/scans_list.html b/guarddog_nexus/web/templates/scans_list.html index 8f405cb..c06fa1e 100644 --- a/guarddog_nexus/web/templates/scans_list.html +++ b/guarddog_nexus/web/templates/scans_list.html @@ -1,23 +1,50 @@ {% extends "base.html" %} +{% block title %}Scans — GuardDog Nexus{% endblock %} +{% block breadcrumbs %} + +{% endblock %} {% block content %}

Scans

-

- +

+ + + {% if flagged_filter == '1' %}Show all{% else %}Flagged only{% endif %} -

+
+
- - + + - - - + + + @@ -27,22 +54,35 @@ - + {% endfor %} + {% if not scans %} + + + + {% endif %}
IDPackage + ID {% if sort_by == 'id' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %} + + Package {% if sort_by == 'package_name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %} + Version RepoStatusFindingsTime + Status {% if sort_by == 'status' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %} + + Findings {% if sort_by == 'total_findings' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %} + + Time {% if sort_by == 'started_at' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %} +
{{ s.package_name }} {{ s.package_version }} {{ s.repository }}{{ s.status }} + {% if s.status == 'scanning' %}scanning{% else %}{{ s.status }}{% endif %} + {% if s.flagged %}{{ s.total_findings }}{% else %}0{% endif %} {{ s.started_at.strftime('%Y-%m-%d %H:%M') if s.started_at }}
+ +

No scans found

+ Try adjusting your search or filters. +
+
{% set total_pages = (total // per_page) + (1 if total % per_page else 0) %} {% if total_pages > 1 %} {% endif %} +{{ total }} total scans {% endblock %}