feat(search): add grouped global results for tasks and reports

This commit is contained in:
2026-02-25 21:09:42 +03:00
parent 87285d8f0a
commit b7d1ee2b71

View File

@@ -20,6 +20,7 @@
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { api } from "$lib/api.js"; import { api } from "$lib/api.js";
import { getReports } from "$lib/api/reports.js";
import { activityStore } from "$lib/stores/activity.js"; import { activityStore } from "$lib/stores/activity.js";
import { import {
taskDrawerStore, taskDrawerStore,
@@ -46,7 +47,7 @@
let searchQuery = ""; let searchQuery = "";
let showSearchDropdown = false; let showSearchDropdown = false;
let isSearchLoading = false; let isSearchLoading = false;
let searchResults = []; let groupedSearchResults = [];
let searchTimer = null; let searchTimer = null;
const SEARCH_DEBOUNCE_MS = 250; const SEARCH_DEBOUNCE_MS = 250;
@@ -95,13 +96,13 @@
function handleSearchFocus() { function handleSearchFocus() {
isSearchFocused = true; isSearchFocused = true;
showSearchDropdown = searchResults.length > 0; showSearchDropdown = groupedSearchResults.length > 0;
} }
function clearSearchState() { function clearSearchState() {
showSearchDropdown = false; showSearchDropdown = false;
isSearchLoading = false; isSearchLoading = false;
searchResults = []; groupedSearchResults = [];
} }
function handleDocumentClick(event) { function handleDocumentClick(event) {
@@ -126,7 +127,13 @@
} }
} }
function buildSearchResultItems(dashboardResponse, datasetResponse) { function buildSearchResultSections(
dashboardResponse,
datasetResponse,
tasksResponse,
reportsResponse,
query,
) {
const dashboards = (dashboardResponse?.dashboards || []).slice( const dashboards = (dashboardResponse?.dashboards || []).slice(
0, 0,
SEARCH_LIMIT, SEARCH_LIMIT,
@@ -146,7 +153,61 @@
subtitle: dataset.schema || "-", subtitle: dataset.schema || "-",
href: `/datasets/${dataset.id}?env_id=${encodeURIComponent(globalSelectedEnvId)}`, 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) { async function triggerSearch(query) {
@@ -158,7 +219,8 @@
isSearchLoading = true; isSearchLoading = true;
showSearchDropdown = true; showSearchDropdown = true;
try { try {
const [dashboardResponse, datasetResponse] = await Promise.all([ const [dashboardResponse, datasetResponse, tasksResponse, reportsResponse] =
await Promise.all([
api.getDashboards(globalSelectedEnvId, { api.getDashboards(globalSelectedEnvId, {
search: normalizedQuery, search: normalizedQuery,
page: 1, page: 1,
@@ -169,11 +231,25 @@
page: 1, page: 1,
page_size: SEARCH_LIMIT, 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) { } catch (error) {
console.error("[TopNavbar][Coherence:Failed] Global search failed", error); console.error("[TopNavbar][Coherence:Failed] Global search failed", error);
searchResults = []; groupedSearchResults = [];
} finally { } finally {
isSearchLoading = false; isSearchLoading = false;
} }
@@ -193,7 +269,13 @@
clearSearchState(); clearSearchState();
searchQuery = ""; searchQuery = "";
isSearchFocused = false; 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) { async function handleSearchKeydown(event) {
@@ -203,9 +285,10 @@
isSearchFocused = false; isSearchFocused = false;
return; return;
} }
if (event.key === "Enter" && searchResults.length > 0) { const firstItem = groupedSearchResults[0]?.items?.[0];
if (event.key === "Enter" && firstItem) {
event.preventDefault(); event.preventDefault();
await openSearchResult(searchResults[0]); await openSearchResult(firstItem);
} }
} }
@@ -268,22 +351,26 @@
<div class="absolute left-0 right-0 top-12 z-50 rounded-lg border border-slate-200 bg-white shadow-lg"> <div class="absolute left-0 right-0 top-12 z-50 rounded-lg border border-slate-200 bg-white shadow-lg">
{#if isSearchLoading} {#if isSearchLoading}
<div class="px-4 py-3 text-sm text-slate-500">{$t.common?.loading || "Loading..."}</div> <div class="px-4 py-3 text-sm text-slate-500">{$t.common?.loading || "Loading..."}</div>
{:else if searchResults.length === 0} {:else if groupedSearchResults.length === 0}
<div class="px-4 py-3 text-sm text-slate-500">{$t.common?.not_found || "No results found"}</div> <div class="px-4 py-3 text-sm text-slate-500">{$t.common?.not_found || "No results found"}</div>
{:else} {:else}
{#each searchResults as result} {#each groupedSearchResults as section}
<button <div class="border-b border-slate-100 last:border-b-0">
class="flex w-full items-center justify-between px-4 py-2 text-left hover:bg-slate-50" <div class="px-4 py-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
on:click={() => openSearchResult(result)} {section.label}
>
<div class="min-w-0">
<div class="truncate text-sm font-medium text-slate-800">{result.title}</div>
<div class="truncate text-xs text-slate-500">{result.subtitle}</div>
</div> </div>
<span class="ml-4 rounded border border-slate-200 px-2 py-0.5 text-xs text-slate-600"> {#each section.items as result}
{result.type === "dashboard" ? ($t.nav?.dashboards || "Dashboards") : ($t.nav?.datasets || "Datasets")} <button
</span> class="flex w-full items-center justify-between px-4 py-2 text-left hover:bg-slate-50"
</button> on:click={() => openSearchResult(result)}
>
<div class="min-w-0">
<div class="truncate text-sm font-medium text-slate-800">{result.title}</div>
<div class="truncate text-xs text-slate-500">{result.subtitle}</div>
</div>
</button>
{/each}
</div>
{/each} {/each}
{/if} {/if}
</div> </div>