diff --git a/frontend/src/lib/components/layout/TopNavbar.svelte b/frontend/src/lib/components/layout/TopNavbar.svelte index d876823..e304b88 100644 --- a/frontend/src/lib/components/layout/TopNavbar.svelte +++ b/frontend/src/lib/components/layout/TopNavbar.svelte @@ -17,7 +17,9 @@ */ import { createEventDispatcher, onMount } from "svelte"; + import { goto } from "$app/navigation"; import { page } from "$app/stores"; + import { api } from "$lib/api.js"; import { activityStore } from "$lib/stores/activity.js"; import { taskDrawerStore, @@ -41,6 +43,15 @@ let showUserMenu = false; let isSearchFocused = false; + let searchQuery = ""; + let showSearchDropdown = false; + let isSearchLoading = false; + let searchResults = []; + let searchTimer = null; + + const SEARCH_DEBOUNCE_MS = 250; + const SEARCH_MIN_LENGTH = 2; + const SEARCH_LIMIT = 5; $: isExpanded = $sidebarStore?.isExpanded ?? true; $: activeCount = $activityStore?.activeCount || 0; @@ -84,20 +95,23 @@ function handleSearchFocus() { isSearchFocused = true; + showSearchDropdown = searchResults.length > 0; } - function handleSearchBlur() { - isSearchFocused = false; + function clearSearchState() { + showSearchDropdown = false; + isSearchLoading = false; + searchResults = []; } function handleDocumentClick(event) { if (!event.target.closest(".user-menu-container")) { closeUserMenu(); } - } - - if (typeof document !== "undefined") { - document.addEventListener("click", handleDocumentClick); + if (!event.target.closest(".global-search-container")) { + isSearchFocused = false; + clearSearchState(); + } } function handleHamburgerClick(event) { @@ -107,10 +121,107 @@ function handleGlobalEnvironmentChange(event) { setSelectedEnvironment(event.target.value); + if (searchQuery.trim().length >= SEARCH_MIN_LENGTH) { + triggerSearch(searchQuery.trim()); + } + } + + function buildSearchResultItems(dashboardResponse, datasetResponse) { + const dashboards = (dashboardResponse?.dashboards || []).slice( + 0, + SEARCH_LIMIT, + ); + const datasets = (datasetResponse?.datasets || []).slice(0, SEARCH_LIMIT); + const dashboardItems = dashboards.map((dashboard) => ({ + key: `dashboard-${dashboard.id}`, + type: "dashboard", + title: dashboard.title || dashboard.dashboard_title || `#${dashboard.id}`, + subtitle: `ID: ${dashboard.id}`, + href: `/dashboards/${dashboard.id}?env_id=${encodeURIComponent(globalSelectedEnvId)}`, + })); + const datasetItems = datasets.map((dataset) => ({ + key: `dataset-${dataset.id}`, + type: "dataset", + title: dataset.table_name || `#${dataset.id}`, + subtitle: dataset.schema || "-", + href: `/datasets/${dataset.id}?env_id=${encodeURIComponent(globalSelectedEnvId)}`, + })); + return [...dashboardItems, ...datasetItems]; + } + + async function triggerSearch(query) { + const normalizedQuery = String(query || "").trim(); + if (normalizedQuery.length < SEARCH_MIN_LENGTH || !globalSelectedEnvId) { + clearSearchState(); + return; + } + isSearchLoading = true; + showSearchDropdown = true; + try { + const [dashboardResponse, datasetResponse] = await Promise.all([ + api.getDashboards(globalSelectedEnvId, { + search: normalizedQuery, + page: 1, + page_size: SEARCH_LIMIT, + }), + api.getDatasets(globalSelectedEnvId, { + search: normalizedQuery, + page: 1, + page_size: SEARCH_LIMIT, + }), + ]); + searchResults = buildSearchResultItems(dashboardResponse, datasetResponse); + } catch (error) { + console.error("[TopNavbar][Coherence:Failed] Global search failed", error); + searchResults = []; + } finally { + isSearchLoading = false; + } + } + + function handleSearchInput(event) { + searchQuery = event.target.value; + if (searchTimer) { + clearTimeout(searchTimer); + } + searchTimer = setTimeout(() => { + triggerSearch(searchQuery); + }, SEARCH_DEBOUNCE_MS); + } + + async function openSearchResult(item) { + clearSearchState(); + searchQuery = ""; + isSearchFocused = false; + await goto(item.href); + } + + async function handleSearchKeydown(event) { + if (event.key === "Escape") { + clearSearchState(); + searchQuery = ""; + isSearchFocused = false; + return; + } + if (event.key === "Enter" && searchResults.length > 0) { + event.preventDefault(); + await openSearchResult(searchResults[0]); + } } onMount(async () => { await initializeEnvironmentContext(); + if (typeof document !== "undefined") { + document.addEventListener("click", handleDocumentClick); + } + return () => { + if (searchTimer) { + clearTimeout(searchTimer); + } + if (typeof document !== "undefined") { + document.removeEventListener("click", handleDocumentClick); + } + }; }); @@ -141,16 +252,42 @@ - -