From 7804e32a9b8f9e23720fa5581787ed2ff3167da4 Mon Sep 17 00:00:00 2001 From: Mikhail Schukin Date: Tue, 5 May 2026 14:52:50 +0300 Subject: [PATCH] feat: Add task-scoped kwargs key/value filtering to task search - extend task search query with `task_name`, `arg_key`, `arg_value` - add dynamic task key options endpoint for selected task name - apply key/value filtering in repository for both Postgres and SQLite - preserve filter state through HTMX updates, sorting links, and infinite scroll --- taskiq_dashboard/api/routers/task.py | 68 +++++++- taskiq_dashboard/api/templates/home.html | 143 +++++++++++++++- .../templates/partial/arg_key_options.html | 6 + .../api/templates/partial/task_list.html | 4 +- taskiq_dashboard/domain/repositories/task.py | 16 ++ .../infrastructure/repositories/task.py | 67 +++++++- tests/integration/test_task_service.py | 157 ++++++++++++++++++ tests/unit/test_task_router_filters.py | 105 ++++++++++++ tests/unit/test_task_service_sqlite.py | 152 +++++++++++++++++ 9 files changed, 709 insertions(+), 9 deletions(-) create mode 100644 taskiq_dashboard/api/templates/partial/arg_key_options.html create mode 100644 tests/unit/test_task_router_filters.py create mode 100644 tests/unit/test_task_service_sqlite.py diff --git a/taskiq_dashboard/api/routers/task.py b/taskiq_dashboard/api/routers/task.py index 7ff41d3..754f9a3 100644 --- a/taskiq_dashboard/api/routers/task.py +++ b/taskiq_dashboard/api/routers/task.py @@ -22,6 +22,9 @@ class TaskFilter(pydantic.BaseModel): q: str = '' + task_name: str = '' + arg_key: str = '' + arg_value: str = '' status: TaskStatus | None = None limit: int = 30 offset: int = 0 @@ -63,33 +66,94 @@ async def search_tasks( query: tp.Annotated[TaskFilter, fastapi.Query(...)], hx_request: tp.Annotated[bool, fastapi.Header(description='Request from htmx')] = False, # noqa: FBT002 ) -> HTMLResponse: + normalized_task_name = query.task_name.strip() + normalized_arg_value = query.arg_value.strip() + normalized_arg_key = query.arg_key.strip() if normalized_task_name and normalized_arg_value else '' + if not normalized_task_name: + normalized_arg_value = '' tasks = await repository.find_tasks( name=query.q, + task_name=normalized_task_name, + arg_key=normalized_arg_key, + arg_value=normalized_arg_value, status=query.status, limit=query.limit, offset=query.offset, sort_by=query.sort_by, sort_order=query.sort_order, ) + keys: list[str] = [] + task_names: list[str] = [] + if not hx_request: + keys = await repository.find_argument_keys(task_name=normalized_task_name) + task_names = await repository.find_task_names() + headers: dict[str, str] = {} template_name = 'home.html' if hx_request: + query_params = query.model_dump(exclude={'limit', 'offset'}) + query_params.update( + { + 'task_name': normalized_task_name, + 'arg_key': normalized_arg_key, + 'arg_value': normalized_arg_value, + } + ) headers = { - 'HX-Push-Url': '/?' + urlencode(query.model_dump(exclude={'limit', 'offset'})), + 'HX-Push-Url': '/?' + urlencode(query_params), } template_name = 'partial/task_list.html' + template_context = query.model_dump() + template_context.update( + { + 'task_name': normalized_task_name, + 'arg_key': normalized_arg_key, + 'arg_value': normalized_arg_value, + } + ) + return jinja_templates.TemplateResponse( request, template_name, { 'request': request, 'results': [task.model_dump() for task in tasks], - **query.model_dump(), + 'keys': keys, + 'selected_arg_key': normalized_arg_key, + 'task_names': task_names, + **template_context, }, headers=headers, ) +@router.get( + '/filters/arg-keys', + name='Task argument key options', + response_class=HTMLResponse, +) +async def argument_key_options( + request: fastapi.Request, + repository: dishka_fastapi.FromDishka[AbstractTaskRepository], + task_name: str = '', + arg_key: str = '', +) -> HTMLResponse: + normalized_task_name = task_name.strip() + normalized_arg_key = arg_key.strip() + keys = await repository.find_argument_keys(task_name=normalized_task_name) + if normalized_arg_key not in keys: + normalized_arg_key = '' + return jinja_templates.TemplateResponse( + request, + 'partial/arg_key_options.html', + { + 'request': request, + 'keys': keys, + 'selected_arg_key': normalized_arg_key, + }, + ) + + @router.get( '/tasks/{task_id:uuid}', name='Task details view', diff --git a/taskiq_dashboard/api/templates/home.html b/taskiq_dashboard/api/templates/home.html index 5f209d4..53771b2 100644 --- a/taskiq_dashboard/api/templates/home.html +++ b/taskiq_dashboard/api/templates/home.html @@ -30,7 +30,7 @@ hx-trigger="keyup changed delay:500ms" hx-target="#task-list-body" hx-push-url="true" - hx-include="[name='status']" + hx-include="[name='status'], [name='task_name'], [name='arg_key'], [name='arg_value']" >
+ +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
@@ -62,7 +139,7 @@ hx-trigger="change" hx-target="#task-list-body" hx-push-url="true" - hx-include="[name='q']" + hx-include="[name='q'], [name='task_name'], [name='arg_key'], [name='arg_value']" > {% set status = status if status is defined else "all" %} @@ -158,7 +235,7 @@ Status Worker - Started At @@ -175,7 +252,7 @@ - Finished At @@ -203,6 +280,64 @@
+ +