Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@
"@hapi/podium": "^4.1.3",
"@hapi/vision": "^6.1.0",
"@hapi/wreck": "^17.1.0",
"@huggingface/transformers": "^3.5.1",
"@opensearch-dashboards-test/opensearch-dashboards-test-library": "https://github.com/opensearch-project/opensearch-dashboards-test-library/archive/refs/tags/1.0.6.tar.gz",
"@opensearch-project/opensearch": "^2.13.0",
"@opensearch/datemath": "5.0.3",
Expand All @@ -235,6 +236,7 @@
"@reduxjs/toolkit": "^1.6.1",
"@types/ndjson": "^2.0.4",
"@types/yauzl": "^2.9.1",
"@xenova/transformers": "1.0.0",
"@xyflow/react": "^12.8.2",
"JSONStream": "1.3.5",
"ajv": "^8.11.0",
Expand Down
14 changes: 14 additions & 0 deletions packages/osd-optimizer/src/worker/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,20 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker:
},
},
},
{
test: /\.js$/,
include: [
/[\/\\]node_modules[\/\\]onnxruntime-web[\/\\]/, // ONLY include onnxruntime-web
],
use: {
loader: 'babel-loader',
options: {
babelrc: false,
envName: worker.dist ? 'production' : 'development',
presets: [BABEL_PRESET_PATH],
},
},
},
],
},

Expand Down
18 changes: 12 additions & 6 deletions src/core/public/chrome/chrome_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ import { notificationServiceMock } from '../notifications/notifications_service.
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { ChromeService } from './chrome_service';
import { getAppInfo } from '../application/utils';
import { overlayServiceMock, workspacesServiceMock } from '../mocks';
import { coreMock, overlayServiceMock, workspacesServiceMock } from '../mocks';
import { HeaderVariant } from './constants';
import { keyboardShortcutServiceMock } from '../keyboard_shortcut/keyboard_shortcut_service.mock';
import { HttpSetup } from '../http';

class FakeApp implements App {
public title: string;
Expand Down Expand Up @@ -106,7 +107,7 @@ async function start({
}: { options?: any; cspConfigMock?: any; startDeps?: ReturnType<typeof defaultStartDeps> } = {}) {
const service = new ChromeService(options);

service.setup({ uiSettings: startDeps.uiSettings });
service.setup({ uiSettings: startDeps.uiSettings, http: startDeps.http });

if (cspConfigMock) {
startDeps.injectedMetadata.getCspConfig.mockReturnValue(cspConfigMock);
Expand All @@ -129,6 +130,12 @@ afterAll(() => {
});

describe('setup', () => {
let http: HttpSetup;

beforeEach(() => {
const coreSetup = coreMock.createSetup();
http = coreSetup.http;
});
afterEach(() => {
jest.restoreAllMocks();
});
Expand All @@ -139,7 +146,7 @@ describe('setup', () => {
const chrome = new ChromeService({ browserSupportsCsp: true });
const uiSettings = uiSettingsServiceMock.createSetupContract();

const chromeSetup = chrome.setup({ uiSettings });
const chromeSetup = chrome.setup({ uiSettings, http });
chromeSetup.registerCollapsibleNavHeader(renderMock);

const chromeStart = await chrome.start(defaultStartDeps());
Expand All @@ -155,8 +162,7 @@ describe('setup', () => {
const renderMock = jest.fn().mockReturnValue(customHeaderMock);
const chrome = new ChromeService({ browserSupportsCsp: true });
const uiSettings = uiSettingsServiceMock.createSetupContract();

const chromeSetup = chrome.setup({ uiSettings });
const chromeSetup = chrome.setup({ uiSettings, http });
// call 1st time
chromeSetup.registerCollapsibleNavHeader(renderMock);
// call 2nd time
Expand All @@ -176,7 +182,7 @@ describe('setup', () => {
registerSearchCommand: registerSearchCommandSpy,
});

chrome.setup({ uiSettings });
chrome.setup({ uiSettings, http });

expect(registerSearchCommandSpy).toHaveBeenCalledWith({
id: 'pagesSearch',
Expand Down
7 changes: 4 additions & 3 deletions src/core/public/chrome/chrome_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { EuiLink } from '@elastic/eui';
import { mountReactNode } from '../utils/mount';
import { InternalApplicationStart } from '../application';
import { DocLinksStart } from '../doc_links';
import { HttpStart } from '../http';
import { HttpSetup, HttpStart } from '../http';
import { InjectedMetadataStart } from '../injected_metadata';
import { NotificationsStart } from '../notifications';
import { IUiSettingsClient } from '../ui_settings';
Expand Down Expand Up @@ -124,6 +124,7 @@ interface ConstructorParams {

export interface SetupDeps {
uiSettings: IUiSettingsClient;
http: HttpSetup;
}

export interface StartDeps {
Expand Down Expand Up @@ -222,15 +223,15 @@ export class ChromeService {
);
}

public setup({ uiSettings }: SetupDeps): ChromeSetup {
public setup({ uiSettings, http }: SetupDeps): ChromeSetup {
const navGroup = this.navGroup.setup({ uiSettings });
const globalSearch = this.globalSearch.setup();

globalSearch.registerSearchCommand({
id: 'pagesSearch',
type: 'PAGES',
run: async (query: string, callback: () => void) =>
searchPages(query, this.navGroupStart, this.applicationStart, callback),
searchPages(query, this.navGroupStart, this.applicationStart, callback, http),
});

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,31 @@ import {
DEFAULT_NAV_GROUPS,
renderNavGroupElement,
NavGroupType,
HttpStart,
} from '../../../../../core/public';

export const searchPages = async (
query: string,
navGroup?: ChromeNavGroupServiceStartContract,
application?: InternalApplicationStart,
callback?: () => void
callback?: () => void,
http?: HttpStart
): Promise<ReactNode[]> => {
if (navGroup && application) {
const navGroupMap = await navGroup.getNavGroupsMap$().pipe(first()).toPromise();

const searchResult = searchNavigationLinks(
const searchResult = await searchNavigationLinks(
[
DEFAULT_NAV_GROUPS.all.id,
DEFAULT_NAV_GROUPS.dataAdministration.id,
DEFAULT_NAV_GROUPS.settingsAndSetup.id,
],
navGroupMap,
query
query,
http
);

const pages = searchResult.slice(0, 10).map((link) => {
const pages = searchResult.slice(0, 10).map((link: any) => {
return (
<GlobalSearchPageItem
link={link}
Expand Down
11 changes: 8 additions & 3 deletions src/core/public/chrome/ui/header/header_search_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import {
EuiTitle,
EuiToolTip,
} from '@elastic/eui';
import React, { ReactNode, useCallback, useRef, useState } from 'react';
import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
import { i18n } from '@osd/i18n';
import { debounce } from 'lodash';
import {
GlobalSearchCommand,
SearchCommandKeyTypes,
Expand Down Expand Up @@ -136,7 +137,7 @@ export const HeaderSearchBar = ({ globalSearchCommands, panel, onSearchResultCli
</EuiText>
);

const onSearch = useCallback(
const search = useCallback(
async (value: string) => {
const filteredCommands = globalSearchCommands.filter((command) => {
const alias = SearchCommandTypes[command.type].alias;
Expand Down Expand Up @@ -196,11 +197,15 @@ export const HeaderSearchBar = ({ globalSearchCommands, panel, onSearchResultCli
[globalSearchCommands, onSearchResultClick]
);

const debouncedSearch = useMemo(() => {
return debounce(search, 500); // 300ms delay, adjust as needed
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Will 500ms has better UI performance than 300ms?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It give user more time to type before semantic search.

}, [search]);

const searchBar = (
<EuiFieldSearch
compressed
incremental
onSearch={onSearch}
onSearch={debouncedSearch}
fullWidth
placeholder={i18n.translate('core.globalSearch.input.placeholder', {
defaultMessage: 'Search the menu',
Expand Down
32 changes: 16 additions & 16 deletions src/core/public/chrome/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@ describe('searchNavigationLinks', () => {

const allAvailableCaseId = ['test-group'];

it('should return matching visible and enabled links', () => {
it('should return matching visible and enabled links', async () => {
const query = 'child';
const result = searchNavigationLinks(allAvailableCaseId, navGroupMap, query);
const result = await searchNavigationLinks(allAvailableCaseId, navGroupMap, query);

expect(result).toHaveLength(1);
expect(result[0]).toEqual(
Expand All @@ -210,9 +210,9 @@ describe('searchNavigationLinks', () => {
);
});

it('should return child links when searching by parent title', () => {
it('should return child links when searching by parent title', async () => {
const query = 'parent';
const result = searchNavigationLinks(allAvailableCaseId, navGroupMap, query);
const result = await searchNavigationLinks(allAvailableCaseId, navGroupMap, query);

expect(result).toHaveLength(1);
expect(result[0]).toEqual(
Expand All @@ -224,23 +224,23 @@ describe('searchNavigationLinks', () => {
);
});

it('should not return hidden links', () => {
it('should not return hidden links', async () => {
const query = 'hidden';
const result = searchNavigationLinks(allAvailableCaseId, navGroupMap, query);
const result = await searchNavigationLinks(allAvailableCaseId, navGroupMap, query);

expect(result).toHaveLength(0);
});

it('should not return disabled links', () => {
it('should not return disabled links', async () => {
const query = 'disabled';
const result = searchNavigationLinks(allAvailableCaseId, navGroupMap, query);
const result = await searchNavigationLinks(allAvailableCaseId, navGroupMap, query);

expect(result).toHaveLength(0);
});

it('should not return parent links', () => {
it('should not return parent links', async () => {
const query = 'Parent';
const result = searchNavigationLinks(allAvailableCaseId, navGroupMap, query);
const result = await searchNavigationLinks(allAvailableCaseId, navGroupMap, query);

expect(result).toHaveLength(1);
expect(result[0]).toEqual(
Expand All @@ -251,9 +251,9 @@ describe('searchNavigationLinks', () => {
);
});

it('should handle case-insensitive search', () => {
it('should handle case-insensitive search', async () => {
const query = 'CHILD';
const result = searchNavigationLinks(allAvailableCaseId, navGroupMap, query);
const result = await searchNavigationLinks(allAvailableCaseId, navGroupMap, query);

expect(result).toHaveLength(1);
expect(result[0]).toEqual(
Expand All @@ -264,15 +264,15 @@ describe('searchNavigationLinks', () => {
);
});

it('should handle non-existent nav group', () => {
const result = searchNavigationLinks(['non-existent'], navGroupMap, 'test');
it('should handle non-existent nav group', async () => {
const result = await searchNavigationLinks(['non-existent'], navGroupMap, 'test');

expect(result).toHaveLength(0);
});

it('should return empty array for empty query', () => {
it('should return empty array for empty query', async () => {
const query = '';
const result = searchNavigationLinks(allAvailableCaseId, navGroupMap, query);
const result = await searchNavigationLinks(allAvailableCaseId, navGroupMap, query);

expect(result).toHaveLength(1);
});
Expand Down
72 changes: 52 additions & 20 deletions src/core/public/chrome/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { AppCategory } from 'opensearch-dashboards/public';
import { AppCategory, HttpStart } from 'opensearch-dashboards/public';
import { ChromeNavLink } from './nav_links';
import { ChromeRegistrationNavLink, NavGroupItemInMap } from './nav_group';
import { NavGroupStatus } from '../../../core/types';

type KeyOf<T> = keyof T;

export const sortBy = <T>(key: KeyOf<T>) => {
Expand Down Expand Up @@ -240,12 +239,13 @@ export function setIsCategoryOpen(id: string, isOpen: boolean, storage: Storage)
storage.setItem(getCategoryLocalStorageKey(id), `${isOpen}`);
}

export function searchNavigationLinks(
export async function searchNavigationLinks(
allAvailableCaseId: string[],
navGroupMap: Record<string, NavGroupItemInMap>,
query: string
query: string,
http?: HttpStart
) {
return allAvailableCaseId.flatMap((useCaseId) => {
const allSearchAbleLinks = allAvailableCaseId.flatMap((useCaseId) => {
const navGroup = navGroupMap[useCaseId];
if (!navGroup) return [];

Expand All @@ -255,27 +255,59 @@ export function searchNavigationLinks(

return links
.filter((link) => {
const title = link.title;
return !link.hidden && !link.disabled && !parentNavLinkIds.includes(link.id);
})
.map((link) => {
let parentNavLinkTitle;
// parent title also taken into consideration for search its sub items
if (link.parentNavLinkId) {
parentNavLinkTitle = navGroup.navLinks.find(
(navLink) => navLink.id === link.parentNavLinkId
)?.title;
}
const titleMatch = title && title.toLowerCase().includes(query.toLowerCase());
const parentTitleMatch =
parentNavLinkTitle && parentNavLinkTitle.toLowerCase().includes(query.toLowerCase());
return (
!link.hidden &&
!link.disabled &&
(titleMatch || parentTitleMatch) &&
!parentNavLinkIds.includes(link.id)
);
})
.map((link) => ({
...link,
navGroup,
}));
return {
...link,
parentNavLinkTitle,
navGroup,
};
});
});

try {
const linksContext = allSearchAbleLinks.map((link) => ({
id: link.id,
title: link.title,
description: link.description ?? link.title,
}));
const semanticSearchResult = await http?.post('/api/workspaces/_semantic_search', {
body: JSON.stringify({
query: query,
links: linksContext,
}),
});

// Handle case when semanticSearchResult is undefined (http not provided)
if (!semanticSearchResult) {
// Fallback to simple string matching if semantic search isn't available
return allSearchAbleLinks.filter(
(link) =>
link.title.toLowerCase().includes(query.toLowerCase()) ||
(link.parentNavLinkTitle &&
link.parentNavLinkTitle.toLowerCase().includes(query.toLowerCase()))
);
}

const finalResult = semanticSearchResult
.map((link: any) => {
const originalFullLink = allSearchAbleLinks.find((fullLink) => fullLink.id === link.id);
return originalFullLink || null;
})
.filter(Boolean) as Array<ChromeRegistrationNavLink & ChromeNavLink>;

console.log('Final Semantic Search Result (from backend): ', finalResult);
return finalResult;
} catch (error) {
console.error('Frontend API call error for semantic search:', error);
throw error;
}
}
Loading
Loading