Skip to content

Commit ce09c64

Browse files
authored
Merge branch 'main' into fix_8596
2 parents a2df19c + d3c5461 commit ce09c64

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2413
-162
lines changed

changelogs/fragments/10789.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
feat:
2+
- Add assets search command to find dashboards and visualizations from global search ([#10789](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10789))
3+
- Enhance global submit commands for Enter-key triggered actions in global search ([#10789](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10789))

changelogs/fragments/10800.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
feat:
2+
- Add a Content-Security-Policy-Report-Only header ([#10800](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10800))

changelogs/fragments/10808.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
feat:
2+
- Support numerical field as color field ([#10808](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10808))

src/core/public/chrome/chrome_service.mock.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ const createStartContractMock = () => {
9292
globalSearch: {
9393
getAllSearchCommands: jest.fn(() => []),
9494
unregisterSearchCommand: jest.fn(),
95+
getAllSearchCommands$: jest.fn(() => new BehaviorSubject([])),
96+
registerSearchCommand: jest.fn(),
9597
},
9698
setAppTitle: jest.fn(),
9799
setIsVisible: jest.fn(),

src/core/public/chrome/chrome_service.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ export class ChromeService {
432432
workspaceList$={workspaces.workspaceList$}
433433
currentWorkspace$={workspaces.currentWorkspace$}
434434
useUpdatedHeader={this.useUpdatedHeader}
435-
globalSearchCommands={globalSearch.getAllSearchCommands()}
435+
globalSearchCommands$={globalSearch.getAllSearchCommands$()}
436436
globalBanner$={this.globalBanner$.pipe(takeUntil(this.stop$))}
437437
keyboardShortcut={keyboardShortcut}
438438
/>

src/core/public/chrome/global_search/global_search_service.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ describe('GlobalSearchService', () => {
1212
const setup = globalSearchService.setup();
1313
const start = globalSearchService.start();
1414

15+
const mockAction = jest.fn();
16+
const customPlaceholder = 'Search for pages...';
17+
1518
setup.registerSearchCommand({
1619
id: 'test1',
1720
type: 'PAGES',
21+
inputPlaceholder: customPlaceholder,
22+
action: mockAction,
1823
run: async (query) => {
1924
return [];
2025
},
@@ -23,6 +28,12 @@ describe('GlobalSearchService', () => {
2328
expect(start.getAllSearchCommands()).toHaveLength(1);
2429
expect(start.getAllSearchCommands()[0].id).toEqual('test1');
2530
expect(start.getAllSearchCommands()[0].type).toEqual('PAGES');
31+
expect(start.getAllSearchCommands()[0].inputPlaceholder).toEqual(customPlaceholder);
32+
expect(start.getAllSearchCommands()[0].action).toBeDefined();
33+
34+
// Test that action can be called with payload
35+
start.getAllSearchCommands()[0].action?.({ content: 'test query' });
36+
expect(mockAction).toHaveBeenCalledWith({ content: 'test query' });
2637
});
2738

2839
it('unregisterSearchCommand', async () => {
@@ -69,4 +80,66 @@ describe('GlobalSearchService', () => {
6980
expect(start.getAllSearchCommands()[0].id).toEqual('test2');
7081
expect(start.getAllSearchCommands()[0].type).toEqual('PAGES');
7182
});
83+
84+
it('registerSearchCommand with action callback', async () => {
85+
const globalSearchService = new GlobalSearchService();
86+
const setup = globalSearchService.setup();
87+
const start = globalSearchService.start();
88+
89+
const mockAction = jest.fn();
90+
91+
setup.registerSearchCommand({
92+
id: 'test-action',
93+
type: 'ACTIONS',
94+
run: async (query) => {
95+
return [];
96+
},
97+
action: mockAction,
98+
});
99+
100+
const commands = start.getAllSearchCommands();
101+
expect(commands).toHaveLength(1);
102+
expect(commands[0].action).toBeDefined();
103+
104+
// Test that action can be called with payload
105+
commands[0].action?.({ content: 'test query' });
106+
expect(mockAction).toHaveBeenCalledWith({ content: 'test query' });
107+
});
108+
109+
it('getAllSearchCommands$', async () => {
110+
const globalSearchService = new GlobalSearchService();
111+
const setup = globalSearchService.setup();
112+
const start = globalSearchService.start();
113+
114+
const commands$ = start.getAllSearchCommands$();
115+
const receivedCommands: any[] = [];
116+
117+
const subscription = commands$.subscribe((commands) => {
118+
receivedCommands.push(commands);
119+
});
120+
121+
// Initially should have empty array
122+
expect(receivedCommands[0]).toHaveLength(0);
123+
124+
// Register a command
125+
setup.registerSearchCommand({
126+
id: 'test-observable',
127+
type: 'PAGES',
128+
run: async (query) => {
129+
return [];
130+
},
131+
});
132+
133+
// Should receive updated commands
134+
expect(receivedCommands[1]).toHaveLength(1);
135+
expect(receivedCommands[1][0].id).toEqual('test-observable');
136+
137+
// Unregister the command
138+
start.unregisterSearchCommand('test-observable');
139+
140+
// Should receive empty array again
141+
expect(receivedCommands[2]).toHaveLength(0);
142+
143+
subscription.unsubscribe();
144+
});
72145
});

src/core/public/chrome/global_search/global_search_service.ts

Lines changed: 151 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { ReactNode } from 'react';
77
import { i18n } from '@osd/i18n';
8+
import { BehaviorSubject, Observable } from 'rxjs';
89

910
/**
1011
* search input match with `@` will handled by saved objects search command
@@ -24,10 +25,27 @@ export const SearchCommandTypes = {
2425
}),
2526
alias: SAVED_OBJECTS_SYMBOL,
2627
},
28+
ACTIONS: {
29+
description: i18n.translate('core.globalSearch.actions.description', {
30+
defaultMessage: 'Actions',
31+
}),
32+
alias: null,
33+
},
2734
} as const;
2835

2936
export type SearchCommandKeyTypes = keyof typeof SearchCommandTypes;
3037

38+
/**
39+
* Options for the run method of GlobalSearchCommand
40+
* @experimental
41+
*/
42+
export interface GlobalSearchCommandRunOptions {
43+
/**
44+
* AbortSignal to cancel the search operation
45+
*/
46+
abortSignal?: AbortSignal;
47+
}
48+
3149
/**
3250
* @experimental
3351
*/
@@ -41,21 +59,140 @@ export interface GlobalSearchCommand {
4159
* @type {SearchCommandTypes}
4260
*/
4361
type: SearchCommandKeyTypes;
62+
63+
/**
64+
* Defines the placeholder text displayed in the global search input field.
65+
* When multiple commands specify a placeholder, only the first registered command's placeholder will be used.
66+
*
67+
* @example 'Search pages, assets, and actions...'
68+
*/
69+
inputPlaceholder?: string;
70+
4471
/**
4572
* do the search and return search result with a React element
4673
* @param value search query
4774
* @param callback callback function when search is done
75+
* @param options options object containing abortSignal and other future extensible properties
76+
*/
77+
run(
78+
value: string,
79+
callback?: () => void,
80+
options?: GlobalSearchCommandRunOptions
81+
): Promise<ReactNode[]>;
82+
83+
/**
84+
* Callback function executed when the user presses Enter in the global search bar.
85+
* This allows commands to perform custom actions based on the search query, such as navigation or triggering specific functionality.
86+
*
87+
* @param payload - The payload object containing the search content
88+
* @param payload.content - The search query string entered by the user
89+
*
90+
* @example
91+
* ```typescript
92+
* action: ({ content }) => {
93+
* // Navigate to search results page
94+
* window.location.href = `/search?q=${encodeURIComponent(content)}`;
95+
* }
96+
* ```
4897
*/
49-
run(value: string, callback?: () => void): Promise<ReactNode[]>;
98+
action?: (payload: { content: string }) => void;
5099
}
51100

101+
/**
102+
* Setup contract for the global search service.
103+
* Provides methods to register search commands and submit commands during the setup lifecycle.
104+
* @experimental
105+
*/
52106
export interface GlobalSearchServiceSetupContract {
107+
/**
108+
* Registers a search command that will be executed when users perform searches in the global search bar.
109+
* Each command must have a unique ID and will be invoked based on the search query pattern.
110+
*
111+
* @param searchCommand - The search command to register
112+
* @throws Warning if a command with the same ID already exists
113+
*
114+
* @example
115+
* ```typescript
116+
* chrome.globalSearch.registerSearchCommand({
117+
* id: 'my-search-command',
118+
* type: 'PAGES',
119+
* run: async (query, callback, abortSignal) => {
120+
* // Perform search logic
121+
* return [<SearchResult key="1">Result 1</SearchResult>];
122+
* }
123+
* });
124+
* ```
125+
*/
53126
registerSearchCommand(searchCommand: GlobalSearchCommand): void;
54127
}
55128

129+
/**
130+
* Start contract for the global search service.
131+
* Provides methods to retrieve and manage search commands during the start lifecycle.
132+
* @experimental
133+
*/
56134
export interface GlobalSearchServiceStartContract {
135+
/**
136+
* Retrieves all registered search commands.
137+
* Returns an array of all search commands that have been registered during the setup phase.
138+
*
139+
* @returns An array of all registered GlobalSearchCommand instances
140+
*
141+
* @example
142+
* ```typescript
143+
* const commands = chrome.globalSearch.getAllSearchCommands();
144+
* console.log(`Total commands: ${commands.length}`);
145+
* ```
146+
*/
57147
getAllSearchCommands(): GlobalSearchCommand[];
148+
149+
/**
150+
* Unregisters a previously registered search command by its ID.
151+
* This removes the command from the list of available search commands.
152+
*
153+
* @param id - The unique identifier of the search command to unregister
154+
*
155+
* @example
156+
* ```typescript
157+
* chrome.globalSearch.unregisterSearchCommand('my-search-command');
158+
* ```
159+
*/
58160
unregisterSearchCommand(id: string): void;
161+
162+
/**
163+
* Returns an observable stream of all registered search commands.
164+
* Subscribers will receive updates whenever search commands are added or removed.
165+
*
166+
* @returns An Observable that emits the current array of GlobalSearchCommand instances
167+
*
168+
* @example
169+
* ```typescript
170+
* chrome.globalSearch.getAllSearchCommands$().subscribe(commands => {
171+
* console.log(`Available commands: ${commands.length}`);
172+
* });
173+
* ```
174+
*/
175+
getAllSearchCommands$: () => Observable<GlobalSearchCommand[]>;
176+
/**
177+
* Registers a search command that will be executed when users perform searches in the global search bar.
178+
* Each command must have a unique ID and will be invoked based on the search query pattern.
179+
*
180+
* @param searchCommand - The search command to register
181+
* @throws Warning if a command with the same ID already exists
182+
*
183+
* @example
184+
* ```typescript
185+
* chrome.globalSearch.registerSearchCommand({
186+
* id: 'my-search-command',
187+
* type: 'PAGES',
188+
* run: async (query, callback, abortSignal) => {
189+
* // Perform search logic
190+
* return [<SearchResult key="1">Result 1</SearchResult>];
191+
* }
192+
* });
193+
* ```
194+
*/
195+
registerSearchCommand(searchCommand: GlobalSearchCommand): void;
59196
}
60197

61198
/**
@@ -76,7 +213,11 @@ export interface GlobalSearchServiceStartContract {
76213
* @experimental
77214
*/
78215
export class GlobalSearchService {
79-
private searchCommands = [] as GlobalSearchCommand[];
216+
private searchCommands$ = new BehaviorSubject<GlobalSearchCommand[]>([]);
217+
218+
private get searchCommands() {
219+
return this.searchCommands$.getValue();
220+
}
80221

81222
private registerSearchCommand(searchHandler: GlobalSearchCommand) {
82223
const exists = this.searchCommands.find((item) => {
@@ -87,15 +228,16 @@ export class GlobalSearchService {
87228
console.warn(`Duplicate SearchCommands id ${searchHandler.id} found`);
88229
return;
89230
}
90-
this.searchCommands.push(searchHandler);
231+
this.searchCommands$.next([...this.searchCommands, searchHandler]);
91232
}
92233

93234
private unregisterSearchCommand(id: string) {
94-
this.searchCommands = this.searchCommands.filter((item) => {
95-
return item.id !== id;
96-
});
235+
this.searchCommands$.next(
236+
this.searchCommands.filter((item) => {
237+
return item.id !== id;
238+
})
239+
);
97240
}
98-
99241
public setup(): GlobalSearchServiceSetupContract {
100242
return {
101243
registerSearchCommand: this.registerSearchCommand.bind(this),
@@ -106,6 +248,8 @@ export class GlobalSearchService {
106248
return {
107249
getAllSearchCommands: () => this.searchCommands,
108250
unregisterSearchCommand: this.unregisterSearchCommand.bind(this),
251+
getAllSearchCommands$: () => this.searchCommands$.asObservable(),
252+
registerSearchCommand: this.registerSearchCommand.bind(this),
109253
};
110254
}
111255
}

0 commit comments

Comments
 (0)