Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
3 changes: 3 additions & 0 deletions changelogs/fragments/10789.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
feat:
- Add assets search command to find dashboards and visualizations from global search ([#10789](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10789))
- Add search submit commands for Enter-key triggered actions in global search ([#10789](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10789))
3 changes: 3 additions & 0 deletions src/core/public/chrome/chrome_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const createSetupContractMock = () => {
},
globalSearch: {
registerSearchCommand: jest.fn(),
registerSearchSubmitCommand: jest.fn(),
},
};
};
Expand Down Expand Up @@ -92,6 +93,8 @@ const createStartContractMock = () => {
globalSearch: {
getAllSearchCommands: jest.fn(() => []),
unregisterSearchCommand: jest.fn(),
getSearchSubmitCommands$: jest.fn(() => new BehaviorSubject([])),
unregisterSearchSubmitCommand: jest.fn(),
},
setAppTitle: jest.fn(),
setIsVisible: jest.fn(),
Expand Down
1 change: 1 addition & 0 deletions src/core/public/chrome/chrome_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ export class ChromeService {
currentWorkspace$={workspaces.currentWorkspace$}
useUpdatedHeader={this.useUpdatedHeader}
globalSearchCommands={globalSearch.getAllSearchCommands()}
globalSearchSubmitCommands$={globalSearch.getSearchSubmitCommands$()}
globalBanner$={this.globalBanner$.pipe(takeUntil(this.stop$))}
keyboardShortcut={keyboardShortcut}
/>
Expand Down
115 changes: 115 additions & 0 deletions src/core/public/chrome/global_search/global_search_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,119 @@ describe('GlobalSearchService', () => {
expect(start.getAllSearchCommands()[0].id).toEqual('test2');
expect(start.getAllSearchCommands()[0].type).toEqual('PAGES');
});

it('registerSearchSubmitCommand', () => {
const globalSearchService = new GlobalSearchService();
const setup = globalSearchService.setup();
const start = globalSearchService.start();

const mockRun = jest.fn();
setup.registerSearchSubmitCommand({
id: 'submit1',
name: 'Submit Command 1',
run: mockRun,
});

let commands: any[] = [];
start.getSearchSubmitCommands$().subscribe((cmds) => {
commands = cmds;
});

expect(commands).toHaveLength(1);
expect(commands[0].id).toEqual('submit1');
expect(commands[0].name).toEqual('Submit Command 1');
});

it('unregisterSearchSubmitCommand', () => {
const globalSearchService = new GlobalSearchService();
const setup = globalSearchService.setup();
const start = globalSearchService.start();

setup.registerSearchSubmitCommand({
id: 'submit1',
name: 'Submit Command 1',
run: jest.fn(),
});

let commands: any[] = [];
start.getSearchSubmitCommands$().subscribe((cmds) => {
commands = cmds;
});

expect(commands).toHaveLength(1);

start.unregisterSearchSubmitCommand('submit1');

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

it('registerSearchSubmitCommand with duplicate id', () => {
const globalSearchService = new GlobalSearchService();
const setup = globalSearchService.setup();
const start = globalSearchService.start();

setup.registerSearchSubmitCommand({
id: 'submit1',
name: 'Submit Command 1',
run: jest.fn(),
});

setup.registerSearchSubmitCommand({
id: 'submit1',
name: 'Submit Command 1 Duplicate',
run: jest.fn(),
});

let commands: any[] = [];
start.getSearchSubmitCommands$().subscribe((cmds) => {
commands = cmds;
});

// the second one will not overwrite the first one
expect(commands).toHaveLength(1);
expect(commands[0].name).toEqual('Submit Command 1');
});

it('getSearchSubmitCommands$ returns observable that emits updates', () => {
const globalSearchService = new GlobalSearchService();
const setup = globalSearchService.setup();
const start = globalSearchService.start();

const emittedValues: any[][] = [];
start.getSearchSubmitCommands$().subscribe((cmds) => {
emittedValues.push([...cmds]);
});

// Initial empty state
expect(emittedValues).toHaveLength(1);
expect(emittedValues[0]).toHaveLength(0);

// Register first command
setup.registerSearchSubmitCommand({
id: 'submit1',
name: 'Submit Command 1',
run: jest.fn(),
});

expect(emittedValues).toHaveLength(2);
expect(emittedValues[1]).toHaveLength(1);
expect(emittedValues[1][0].id).toEqual('submit1');

// Register second command
setup.registerSearchSubmitCommand({
id: 'submit2',
name: 'Submit Command 2',
run: jest.fn(),
});

expect(emittedValues).toHaveLength(3);
expect(emittedValues[2]).toHaveLength(2);

// Unregister first command
start.unregisterSearchSubmitCommand('submit1');

expect(emittedValues).toHaveLength(4);
expect(emittedValues[3]).toHaveLength(1);
expect(emittedValues[3][0].id).toEqual('submit2');
});
});
41 changes: 39 additions & 2 deletions src/core/public/chrome/global_search/global_search_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { ReactNode } from 'react';
import { i18n } from '@osd/i18n';
import { BehaviorSubject, Observable } from 'rxjs';

/**
* search input match with `@` will handled by saved objects search command
Expand Down Expand Up @@ -46,16 +47,28 @@ export interface GlobalSearchCommand {
* @param value search query
* @param callback callback function when search is done
*/
run(value: string, callback?: () => void): Promise<ReactNode[]>;
run(value: string, callback?: () => void, abortSignal?: AbortSignal): Promise<ReactNode[]>;
Copy link
Member

Choose a reason for hiding this comment

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

Can we update the function interface for better extensibility? in case there will be more context we will need to pass in the future

Suggested change
run(value: string, callback?: () => void, abortSignal?: AbortSignal): Promise<ReactNode[]>;
run(value: string, options: {callback?: () => void, abortSignal?: AbortSignal}): Promise<ReactNode[]>;

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good suggestion. I'm prefer to keep value: string and callback?: () => void as the old implementation. Since I don't want to change the navigation search implementation. The new abortSignal can be wrapped with options. What do you think about it?

}

/**
* @experimental
*/
export interface GlobalSearchSubmitCommand {
Copy link
Member

Choose a reason for hiding this comment

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

Agreed to use an unified Command interface, instead of introducing a new GlobalSearchSubmitCommand, it should extend the existing interface GlobalSearchCommand

id: string;
name: string;
run: (payload: { content: string }) => void;
}

export interface GlobalSearchServiceSetupContract {
registerSearchCommand(searchCommand: GlobalSearchCommand): void;
registerSearchSubmitCommand(searchResultCommand: GlobalSearchSubmitCommand): void;
Copy link
Member

Choose a reason for hiding this comment

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

what's the difference between registerSearchCommand and registerSearchSubmitCommand? would be nice to add comment to each of them

}

export interface GlobalSearchServiceStartContract {
getAllSearchCommands(): GlobalSearchCommand[];
unregisterSearchCommand(id: string): void;
unregisterSearchSubmitCommand(id: string): void;
getSearchSubmitCommands$: () => Observable<GlobalSearchSubmitCommand[]>;
}

/**
Expand All @@ -76,7 +89,8 @@ export interface GlobalSearchServiceStartContract {
* @experimental
*/
export class GlobalSearchService {
private searchCommands = [] as GlobalSearchCommand[];
private searchCommands: GlobalSearchCommand[] = [];
private searchSubmitCommands$ = new BehaviorSubject<GlobalSearchSubmitCommand[]>([]);

private registerSearchCommand(searchHandler: GlobalSearchCommand) {
const exists = this.searchCommands.find((item) => {
Expand All @@ -96,16 +110,39 @@ export class GlobalSearchService {
});
}

private registerSearchSubmitCommand = (searchSubmitCommand: GlobalSearchSubmitCommand) => {
const commands = this.searchSubmitCommands$.getValue();
if (commands.find((command) => command.id === searchSubmitCommand.id)) {
// eslint-disable-next-line no-console
console.warn(`Duplicate SearchSubmitCommands id ${searchSubmitCommand.id} found`);
return;
}
this.searchSubmitCommands$.next([...commands, searchSubmitCommand]);
};

private unregisterSearchSubmitCommand(id: string) {
const newCommands = this.searchSubmitCommands$.getValue().filter((item) => {
return item.id !== id;
});
if (newCommands.length === this.searchSubmitCommands$.getValue().length) {
return;
}
this.searchSubmitCommands$.next(newCommands);
}

public setup(): GlobalSearchServiceSetupContract {
return {
registerSearchCommand: this.registerSearchCommand.bind(this),
registerSearchSubmitCommand: this.registerSearchSubmitCommand,
Copy link
Member

Choose a reason for hiding this comment

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

I don't seem to see where is this function called registerSearchSubmitCommand, did I miss anything?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For now the only search submit command is in the dashboards-assistant repo. Will raise a new PR for adding an example for this new methods.

};
}

public start(): GlobalSearchServiceStartContract {
return {
getAllSearchCommands: () => this.searchCommands,
getSearchSubmitCommands$: () => this.searchSubmitCommands$.asObservable(),
unregisterSearchCommand: this.unregisterSearchCommand.bind(this),
unregisterSearchSubmitCommand: this.unregisterSearchSubmitCommand.bind(this),
};
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ describe('<CollapsibleNavGroupEnabled />', () => {
}
},
capabilities: { ...capabilitiesServiceMock.createStartContract().capabilities },
globalSearchSubmitCommands$: new BehaviorSubject([]),
...props,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from '../../nav_group';
import { fulfillRegistrationLinksToChromeNavLinks, getVisibleUseCases, sortBy } from '../../utils';
import { ALL_USE_CASE_ID, DEFAULT_APP_CATEGORIES } from '../../../../../core/utils';
import { GlobalSearchCommand } from '../../global_search';
import { GlobalSearchCommand, GlobalSearchSubmitCommand } from '../../global_search';
import { CollapsibleNavTop } from './collapsible_nav_group_enabled_top';
import { HeaderNavControls } from './header_nav_controls';
import { NavGroups } from './collapsible_nav_groups';
Expand All @@ -50,6 +50,7 @@ export interface CollapsibleNavGroupEnabledProps {
capabilities: InternalApplicationStart['capabilities'];
currentWorkspace$: WorkspacesStart['currentWorkspace$'];
globalSearchCommands?: GlobalSearchCommand[];
globalSearchSubmitCommands$: Rx.Observable<GlobalSearchSubmitCommand[]>;
}

const titleForSeeAll = i18n.translate('core.ui.primaryNav.seeAllLabel', {
Expand Down Expand Up @@ -83,6 +84,10 @@ export function CollapsibleNavGroupEnabled({
const navGroupsMap = useObservable(observables.navGroupsMap$, {});
const currentNavGroup = useObservable(observables.currentNavGroup$, undefined);
const currentWorkspace = useObservable(observables.currentWorkspace$);
const globalSearchSubmitCommands = useObservable(
observables.globalSearchSubmitCommands$,
undefined
);

const visibleUseCases = useMemo(() => getVisibleUseCases(navGroupsMap), [navGroupsMap]);

Expand Down Expand Up @@ -221,7 +226,10 @@ export function CollapsibleNavGroupEnabled({
{!isNavOpen ? (
<div className="searchBarIcon euiHeaderSectionItemButton">
{globalSearchCommands && (
<HeaderSearchBarIcon globalSearchCommands={globalSearchCommands} />
<HeaderSearchBarIcon
globalSearchCommands={globalSearchCommands}
globalSearchSubmitCommands={globalSearchSubmitCommands}
/>
)}
</div>
) : (
Expand All @@ -232,7 +240,10 @@ export function CollapsibleNavGroupEnabled({
className="searchBar-wrapper"
>
{globalSearchCommands && (
<HeaderSearchBar globalSearchCommands={globalSearchCommands} />
<HeaderSearchBar
globalSearchCommands={globalSearchCommands}
globalSearchSubmitCommands={globalSearchSubmitCommands}
/>
)}
</EuiPanel>
)}
Expand Down
Loading
Loading