From b7d1ee2b71c85b53c246274e8802769193b69a95 Mon Sep 17 00:00:00 2001 From: busya Date: Wed, 25 Feb 2026 21:09:42 +0300 Subject: [PATCH] feat(search): add grouped global results for tasks and reports --- .../lib/components/layout/TopNavbar.svelte | 135 ++++++++++++++---- 1 file changed, 111 insertions(+), 24 deletions(-) diff --git a/frontend/src/lib/components/layout/TopNavbar.svelte b/frontend/src/lib/components/layout/TopNavbar.svelte index e304b88..d3f7e00 100644 --- a/frontend/src/lib/components/layout/TopNavbar.svelte +++ b/frontend/src/lib/components/layout/TopNavbar.svelte @@ -20,6 +20,7 @@ import { goto } from "$app/navigation"; import { page } from "$app/stores"; import { api } from "$lib/api.js"; + import { getReports } from "$lib/api/reports.js"; import { activityStore } from "$lib/stores/activity.js"; import { taskDrawerStore, @@ -46,7 +47,7 @@ let searchQuery = ""; let showSearchDropdown = false; let isSearchLoading = false; - let searchResults = []; + let groupedSearchResults = []; let searchTimer = null; const SEARCH_DEBOUNCE_MS = 250; @@ -95,13 +96,13 @@ function handleSearchFocus() { isSearchFocused = true; - showSearchDropdown = searchResults.length > 0; + showSearchDropdown = groupedSearchResults.length > 0; } function clearSearchState() { showSearchDropdown = false; isSearchLoading = false; - searchResults = []; + groupedSearchResults = []; } function handleDocumentClick(event) { @@ -126,7 +127,13 @@ } } - function buildSearchResultItems(dashboardResponse, datasetResponse) { + function buildSearchResultSections( + dashboardResponse, + datasetResponse, + tasksResponse, + reportsResponse, + query, + ) { const dashboards = (dashboardResponse?.dashboards || []).slice( 0, SEARCH_LIMIT, @@ -146,7 +153,61 @@ subtitle: dataset.schema || "-", href: `/datasets/${dataset.id}?env_id=${encodeURIComponent(globalSelectedEnvId)}`, })); - return [...dashboardItems, ...datasetItems]; + const q = String(query || "").toLowerCase(); + const tasks = (tasksResponse || []).slice(0, 30); + const taskItems = tasks + .filter((task) => { + const haystack = `${task?.id || ""} ${task?.plugin_id || ""} ${task?.status || ""}`.toLowerCase(); + return q && haystack.includes(q); + }) + .slice(0, SEARCH_LIMIT) + .map((task) => ({ + key: `task-${task.id}`, + type: "task", + title: task.plugin_id || "task", + subtitle: `${task.id} · ${task.status || "-"}`, + taskId: task.id, + })); + const reportItems = (reportsResponse?.items || []) + .slice(0, SEARCH_LIMIT) + .map((report) => ({ + key: `report-${report.report_id}`, + type: "report", + title: report.summary || report.report_id, + subtitle: `${report.task_type || "-"} · ${report.status || "-"}`, + href: "/reports", + })); + + const sections = []; + if (dashboardItems.length > 0) { + sections.push({ + key: "dashboards", + label: $t.nav?.dashboards || "Dashboards", + items: dashboardItems, + }); + } + if (datasetItems.length > 0) { + sections.push({ + key: "datasets", + label: $t.nav?.datasets || "Datasets", + items: datasetItems, + }); + } + if (taskItems.length > 0) { + sections.push({ + key: "tasks", + label: $t.nav?.tasks || "Tasks", + items: taskItems, + }); + } + if (reportItems.length > 0) { + sections.push({ + key: "reports", + label: $t.nav?.reports || "Reports", + items: reportItems, + }); + } + return sections; } async function triggerSearch(query) { @@ -158,7 +219,8 @@ isSearchLoading = true; showSearchDropdown = true; try { - const [dashboardResponse, datasetResponse] = await Promise.all([ + const [dashboardResponse, datasetResponse, tasksResponse, reportsResponse] = + await Promise.all([ api.getDashboards(globalSelectedEnvId, { search: normalizedQuery, page: 1, @@ -169,11 +231,25 @@ page: 1, page_size: SEARCH_LIMIT, }), + api.getTasks({ limit: 30, offset: 0 }), + getReports({ + page: 1, + page_size: SEARCH_LIMIT, + search: normalizedQuery, + sort_by: "updated_at", + sort_order: "desc", + }), ]); - searchResults = buildSearchResultItems(dashboardResponse, datasetResponse); + groupedSearchResults = buildSearchResultSections( + dashboardResponse, + datasetResponse, + tasksResponse, + reportsResponse, + normalizedQuery, + ); } catch (error) { console.error("[TopNavbar][Coherence:Failed] Global search failed", error); - searchResults = []; + groupedSearchResults = []; } finally { isSearchLoading = false; } @@ -193,7 +269,13 @@ clearSearchState(); searchQuery = ""; isSearchFocused = false; - await goto(item.href); + if (item.type === "task" && item.taskId) { + openDrawerForTask(item.taskId); + return; + } + if (item.href) { + await goto(item.href); + } } async function handleSearchKeydown(event) { @@ -203,9 +285,10 @@ isSearchFocused = false; return; } - if (event.key === "Enter" && searchResults.length > 0) { + const firstItem = groupedSearchResults[0]?.items?.[0]; + if (event.key === "Enter" && firstItem) { event.preventDefault(); - await openSearchResult(searchResults[0]); + await openSearchResult(firstItem); } } @@ -268,22 +351,26 @@
{#if isSearchLoading}
{$t.common?.loading || "Loading..."}
- {:else if searchResults.length === 0} + {:else if groupedSearchResults.length === 0}
{$t.common?.not_found || "No results found"}
{:else} - {#each searchResults as result} - + {#each section.items as result} + + {/each} +
{/each} {/if}