feat(search): add grouped global results for tasks and reports
This commit is contained in:
@@ -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,8 +269,14 @@
|
|||||||
clearSearchState();
|
clearSearchState();
|
||||||
searchQuery = "";
|
searchQuery = "";
|
||||||
isSearchFocused = false;
|
isSearchFocused = false;
|
||||||
|
if (item.type === "task" && item.taskId) {
|
||||||
|
openDrawerForTask(item.taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.href) {
|
||||||
await goto(item.href);
|
await goto(item.href);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSearchKeydown(event) {
|
async function handleSearchKeydown(event) {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
@@ -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,10 +351,15 @@
|
|||||||
<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}
|
||||||
|
<div class="border-b border-slate-100 last:border-b-0">
|
||||||
|
<div class="px-4 py-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||||
|
{section.label}
|
||||||
|
</div>
|
||||||
|
{#each section.items as result}
|
||||||
<button
|
<button
|
||||||
class="flex w-full items-center justify-between px-4 py-2 text-left hover:bg-slate-50"
|
class="flex w-full items-center justify-between px-4 py-2 text-left hover:bg-slate-50"
|
||||||
on:click={() => openSearchResult(result)}
|
on:click={() => openSearchResult(result)}
|
||||||
@@ -280,11 +368,10 @@
|
|||||||
<div class="truncate text-sm font-medium text-slate-800">{result.title}</div>
|
<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 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">
|
|
||||||
{result.type === "dashboard" ? ($t.nav?.dashboards || "Dashboards") : ($t.nav?.datasets || "Datasets")}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user