From 394bb59cc24b63bba3af16d6aa85dd1921336eb5 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:08:46 -0800 Subject: [PATCH 1/2] :backporting due to COE Add setting to turn extending numeric precision on or off (#8837) (#9083) * Add setting to turn extending numeric precision on or off * Changeset file for PR #8837 created/updated --------- (cherry picked from commit 791f5d84c8a9a041bdb4b5487e5c60d14531b365) Signed-off-by: Miki Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8837.yml | 5 + .../console/opensearch_dashboards.json | 2 +- .../send_request_to_opensearch.test.ts | 59 ++- .../send_request_to_opensearch.ts | 4 +- ...end_current_request_to_opensearch.test.tsx | 56 +++ .../use_send_current_request_to_opensearch.ts | 23 +- .../public/lib/opensearch/opensearch.ts | 5 +- src/plugins/data/common/constants.ts | 1 + .../index_patterns/index_patterns.test.ts | 16 +- .../index_patterns/index_patterns.ts | 4 + src/plugins/data/server/ui_settings.ts | 11 + .../application/components/doc/doc.test.tsx | 1 + .../doc/use_opensearch_doc_search.test.tsx | 74 +++- .../doc/use_opensearch_doc_search.ts | 3 +- .../view_components/utils/use_search.test.tsx | 363 ++++++++++++++++++ .../view_components/utils/use_search.ts | 9 +- 16 files changed, 612 insertions(+), 24 deletions(-) create mode 100644 changelogs/fragments/8837.yml create mode 100644 src/plugins/discover/public/application/view_components/utils/use_search.test.tsx diff --git a/changelogs/fragments/8837.yml b/changelogs/fragments/8837.yml new file mode 100644 index 000000000000..c5960bec454e --- /dev/null +++ b/changelogs/fragments/8837.yml @@ -0,0 +1,5 @@ +fix: +- Fix a typo while inspecting values for large numerals in OSD and the JS client ([#8837](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8837)) + +feat: +- Add setting to turn extending numeric precision on or off ([#8837](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8837)) \ No newline at end of file diff --git a/src/plugins/console/opensearch_dashboards.json b/src/plugins/console/opensearch_dashboards.json index 630eb58a6de7..944b49645b9e 100644 --- a/src/plugins/console/opensearch_dashboards.json +++ b/src/plugins/console/opensearch_dashboards.json @@ -3,7 +3,7 @@ "version": "opensearchDashboards", "server": true, "ui": true, - "requiredPlugins": ["devTools"], + "requiredPlugins": ["devTools", "data"], "optionalPlugins": ["usageCollection", "home", "dataSource"], "requiredBundles": [ "opensearchUiShared", diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.test.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.test.ts index 47a73e4af98a..2e6a291b83a3 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.test.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.test.ts @@ -31,6 +31,10 @@ const dummyArgs: OpenSearchRequestArgs = { }; describe('test sendRequestToOpenSearch', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('test request success, json', () => { const mockHttpResponse = createMockHttpResponse( 200, @@ -47,9 +51,9 @@ describe('test sendRequestToOpenSearch', () => { }); }); - it('test request success, json with long numerals', () => { - const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n; - const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n; + it('test request success, json with long numerals when precision enabled', () => { + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n + 1n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n + 1n; const mockHttpResponse = createMockHttpResponse( 200, 'ok', @@ -60,14 +64,59 @@ describe('test sendRequestToOpenSearch', () => { } ); - jest.spyOn(opensearch, 'send').mockResolvedValue(mockHttpResponse); - sendRequestToOpenSearch(dummyArgs).then((result) => { + const send = jest.spyOn(opensearch, 'send'); + send.mockResolvedValue(mockHttpResponse); + sendRequestToOpenSearch({ + ...dummyArgs, + withLongNumeralsSupport: true, + }).then((result) => { + expect(send).toHaveBeenCalledWith( + expect.anything(), + dummyArgs.requests[0].method, + dummyArgs.requests[0].url, + dummyArgs.requests[0].data.join('\n') + '\n', + undefined, + true + ); const value = (result as any)[0].response.value; expect(value).toMatch(new RegExp(`"long-max": ${longPositive}[,\n]`)); expect(value).toMatch(new RegExp(`"long-min": ${longNegative}[,\n]`)); }); }); + it('test request success, json with long numerals when precision disabled', () => { + const longPositive = BigInt(Number.MAX_SAFE_INTEGER) * 2n + 1n; + const longNegative = BigInt(Number.MIN_SAFE_INTEGER) * 2n + 1n; + const mockHttpResponse = createMockHttpResponse( + 200, + 'ok', + [['Content-Type', 'application/json, utf-8']], + { + 'long-max': Number(longPositive), + 'long-min': Number(longNegative), + } + ); + + const send = jest.spyOn(opensearch, 'send'); + send.mockResolvedValue(mockHttpResponse); + sendRequestToOpenSearch({ + ...dummyArgs, + withLongNumeralsSupport: false, + }).then((result) => { + expect(send).toHaveBeenCalledWith( + expect.anything(), + dummyArgs.requests[0].method, + dummyArgs.requests[0].url, + dummyArgs.requests[0].data.join('\n') + '\n', + undefined, + false + ); + const value = (result as any)[0].response.value; + expect(value).toMatch(new RegExp(`"long-max": ${Number(longPositive)}[,\n]`)); + expect(value).toMatch(new RegExp(`"long-min": ${Number(longNegative)}[,\n]`)); + }); + }); + it('test request success, text', () => { const mockHttpResponse = createMockHttpResponse( 200, diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts index 1cb992a7a99c..415d9dcbcf43 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/send_request_to_opensearch.ts @@ -41,6 +41,7 @@ export interface OpenSearchRequestArgs { http: HttpSetup; requests: any; dataSourceId?: string; + withLongNumeralsSupport?: boolean; } export interface OpenSearchRequestObject { @@ -104,7 +105,8 @@ export function sendRequestToOpenSearch( opensearchMethod, opensearchPath, opensearchData, - args.dataSourceId + args.dataSourceId, + args.withLongNumeralsSupport ); if (reqId !== CURRENT_REQ_ID) { return; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.test.tsx b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.test.tsx index 8955972d27a0..1a272652b6dc 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.test.tsx +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.test.tsx @@ -28,6 +28,8 @@ * under the License. */ +import { UI_SETTINGS } from '../../../../../data/common'; + jest.mock('./send_request_to_opensearch', () => ({ sendRequestToOpenSearch: jest.fn() })); jest.mock('../../contexts/editor_context/editor_registry', () => ({ instance: { getInputEditor: jest.fn() }, @@ -74,9 +76,63 @@ describe('useSendCurrentRequestToOpenSearch', () => { const { result } = renderHook(() => useSendCurrentRequestToOpenSearch(), { wrapper: contexts }); await act(() => result.current()); + + expect(sendRequestToOpenSearch).toHaveBeenCalledWith({ + requests: ['test'], + http: mockContextValue.services.http, + }); + + // Second call should be the request success + const [, [requestSucceededCall]] = (dispatch as jest.Mock).mock.calls; + expect(requestSucceededCall).toEqual({ type: 'requestSuccess', payload: { data: [] } }); + }); + + it('calls sendRequestToOpenSearch turning withLongNumeralsSupport on when long-numerals is enabled', async () => { + // Set up mocks + (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({}); + (mockContextValue.services.uiSettings.get as jest.Mock).mockImplementation((key: string) => + Promise.resolve(key === UI_SETTINGS.DATA_WITH_LONG_NUMERALS ? true : undefined) + ); + + // This request should succeed + (sendRequestToOpenSearch as jest.Mock).mockResolvedValue([]); + (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ + getRequestsInRange: () => ['test'], + })); + + const { result } = renderHook(() => useSendCurrentRequestToOpenSearch(), { wrapper: contexts }); + await act(() => result.current()); + expect(sendRequestToOpenSearch).toHaveBeenCalledWith({ + requests: ['test'], + http: mockContextValue.services.http, + withLongNumeralsSupport: true, + }); + + // Second call should be the request success + const [, [requestSucceededCall]] = (dispatch as jest.Mock).mock.calls; + expect(requestSucceededCall).toEqual({ type: 'requestSuccess', payload: { data: [] } }); + }); + + it('calls sendRequestToOpenSearch turning withLongNumeralsSupport off when long-numerals is disabled', async () => { + // Set up mocks + (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({}); + (mockContextValue.services.uiSettings.get as jest.Mock).mockImplementation((key: string) => + Promise.resolve(key === UI_SETTINGS.DATA_WITH_LONG_NUMERALS ? false : undefined) + ); + + // This request should succeed + (sendRequestToOpenSearch as jest.Mock).mockResolvedValue([]); + (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ + getRequestsInRange: () => ['test'], + })); + + const { result } = renderHook(() => useSendCurrentRequestToOpenSearch(), { wrapper: contexts }); + await act(() => result.current()); + expect(sendRequestToOpenSearch).toHaveBeenCalledWith({ requests: ['test'], http: mockContextValue.services.http, + withLongNumeralsSupport: false, }); // Second call should be the request success diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.ts index 81eabdf7a264..6e179276a888 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_opensearch/use_send_current_request_to_opensearch.ts @@ -37,10 +37,11 @@ import { track } from './track'; // @ts-ignore import { retrieveAutoCompleteInfo } from '../../../lib/mappings/mappings'; +import { UI_SETTINGS } from '../../../../../data/common'; export const useSendCurrentRequestToOpenSearch = (dataSourceId?: string) => { const { - services: { history, settings, notifications, trackUiMetric, http }, + services: { history, settings, notifications, trackUiMetric, http, uiSettings }, } = useServicesContext(); const dispatch = useRequestActionContext(); @@ -64,7 +65,14 @@ export const useSendCurrentRequestToOpenSearch = (dataSourceId?: string) => { // Fire and forget setTimeout(() => track(requests, editor, trackUiMetric), 0); - const results = await sendRequestToOpenSearch({ http, requests, dataSourceId }); + const withLongNumeralsSupport = await uiSettings.get(UI_SETTINGS.DATA_WITH_LONG_NUMERALS); + + const results = await sendRequestToOpenSearch({ + http, + requests, + dataSourceId, + withLongNumeralsSupport, + }); results.forEach(({ request: { path, method, data } }) => { try { @@ -112,5 +120,14 @@ export const useSendCurrentRequestToOpenSearch = (dataSourceId?: string) => { }); } } - }, [dispatch, http, dataSourceId, settings, notifications.toasts, trackUiMetric, history]); + }, [ + dispatch, + http, + dataSourceId, + settings, + notifications.toasts, + trackUiMetric, + history, + uiSettings, + ]); }; diff --git a/src/plugins/console/public/lib/opensearch/opensearch.ts b/src/plugins/console/public/lib/opensearch/opensearch.ts index 907323611358..e0d2163b4af4 100644 --- a/src/plugins/console/public/lib/opensearch/opensearch.ts +++ b/src/plugins/console/public/lib/opensearch/opensearch.ts @@ -46,7 +46,8 @@ export async function send( method: string, path: string, data: any, - dataSourceId?: string + dataSourceId?: string, + withLongNumeralsSupport?: boolean ): Promise { return await http.post('/api/console/proxy', { query: { @@ -57,7 +58,7 @@ export async function send( body: data, prependBasePath: true, asResponse: true, - withLongNumeralsSupport: true, + withLongNumeralsSupport, }); } diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 43db1fe72b9e..b1cd39b65037 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -60,4 +60,5 @@ export const UI_SETTINGS = { INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder', FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault', FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues', + DATA_WITH_LONG_NUMERALS: 'data:withLongNumerals', } as const; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index 4361e12dec16..eb14671992dd 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -33,6 +33,7 @@ import { IndexPatternsService, IndexPattern } from '.'; import { fieldFormatsMock } from '../../field_formats/mocks'; import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_saved_object_index_pattern'; import { UiSettingsCommon, SavedObjectsClientCommon, SavedObject } from '../types'; +import { UI_SETTINGS } from '../../constants'; const createFieldsFetcher = jest.fn().mockImplementation(() => ({ getFieldsForWildcard: jest.fn().mockImplementation(() => { @@ -51,6 +52,7 @@ function setDocsourcePayload(id: string | null, providedPayload: any) { describe('IndexPatterns', () => { let indexPatterns: IndexPatternsService; let savedObjectsClient: SavedObjectsClientCommon; + const uiSettingsGet = jest.fn(); beforeEach(() => { const indexPatternObj = { id: 'id', version: 'a', attributes: { title: 'title' } }; @@ -83,9 +85,12 @@ describe('IndexPatterns', () => { }; }); + uiSettingsGet.mockClear(); + uiSettingsGet.mockReturnValue(Promise.resolve(false)); + indexPatterns = new IndexPatternsService({ uiSettings: ({ - get: () => Promise.resolve(false), + get: uiSettingsGet, getAll: () => {}, } as any) as UiSettingsCommon, savedObjectsClient: (savedObjectsClient as unknown) as SavedObjectsClientCommon, @@ -249,4 +254,13 @@ describe('IndexPatterns', () => { expect(indexPatterns.savedObjectToSpec(savedObject)).toMatchSnapshot(); }); + + test('correctly detects long-numerals support', async () => { + expect(await indexPatterns.isLongNumeralsSupported()).toBe(false); + + uiSettingsGet.mockImplementation((key: string) => + Promise.resolve(key === UI_SETTINGS.DATA_WITH_LONG_NUMERALS ? true : undefined) + ); + expect(await indexPatterns.isLongNumeralsSupported()).toBe(true); + }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 688605821097..e2b5919c15f0 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -699,6 +699,10 @@ export class IndexPatternsService { indexPatternCache.clear(indexPatternId); return this.savedObjectsClient.delete('index-pattern', indexPatternId); } + + isLongNumeralsSupported() { + return this.config.get(UI_SETTINGS.DATA_WITH_LONG_NUMERALS); + } } export type IndexPatternsContract = PublicMethodsOf; diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 77f4afd11887..9a9adca432ca 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -513,6 +513,17 @@ export function getUiSettings(): Record> { }), schema: schema.string(), }, + [UI_SETTINGS.DATA_WITH_LONG_NUMERALS]: { + name: i18n.translate('data.advancedSettings.data.withLongNumeralsTitle', { + defaultMessage: 'Extend Numeric Precision', + }), + value: true, + description: i18n.translate('data.advancedSettings.data.withLongNumeralsText', { + defaultMessage: + "Turn on for precise handling of extremely large numbers. Turn off to optimize performance when high precision for large values isn't required.", + }), + schema: schema.boolean(), + }, [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { name: i18n.translate('data.advancedSettings.timepicker.refreshIntervalDefaultsTitle', { defaultMessage: 'Time filter refresh interval', diff --git a/src/plugins/discover/public/application/components/doc/doc.test.tsx b/src/plugins/discover/public/application/components/doc/doc.test.tsx index 4a3fb740492a..50bc71012721 100644 --- a/src/plugins/discover/public/application/components/doc/doc.test.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.test.tsx @@ -96,6 +96,7 @@ async function mountDoc(update = false, indexPatternGetter: any = null) { }; const indexPatternService = { get: indexPatternGetter ? indexPatternGetter : jest.fn(() => Promise.resolve(indexPattern)), + isLongNumeralsSupported: jest.fn(), } as any; const props = { diff --git a/src/plugins/discover/public/application/components/doc/use_opensearch_doc_search.test.tsx b/src/plugins/discover/public/application/components/doc/use_opensearch_doc_search.test.tsx index cb716a4f17cb..e3377e900d73 100644 --- a/src/plugins/discover/public/application/components/doc/use_opensearch_doc_search.test.tsx +++ b/src/plugins/discover/public/application/components/doc/use_opensearch_doc_search.test.tsx @@ -38,20 +38,30 @@ import { DocProps } from './doc'; import { Observable } from 'rxjs'; const mockSearchResult = new Observable(); +const mockDataSearchSearch = jest.fn(() => { + return mockSearchResult; +}); jest.mock('../../../opensearch_dashboards_services', () => ({ getServices: () => ({ data: { search: { - search: jest.fn(() => { - return mockSearchResult; - }), + search: mockDataSearchSearch, }, }, }), })); +const mockIndexPatternService = { + get: jest.fn(), + isLongNumeralsSupported: jest.fn(), +} as any; + describe('Test of helper / hook', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test('buildSearchBody', () => { const indexPattern = { getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }), @@ -78,21 +88,69 @@ describe('Test of helper / hook', () => { const indexPattern = { getComputedFields: () => [], }; - const indexPatternService = { - get: jest.fn(() => Promise.resolve(indexPattern)), - } as any; + (mockIndexPatternService.get as jest.Mock).mockResolvedValue(indexPattern); + const props = { id: '1', index: 'index1', indexPatternId: 'xyz', - indexPatternService, + indexPatternService: mockIndexPatternService, } as DocProps; let hook; await act(async () => { hook = renderHook((p: DocProps) => useOpenSearchDocSearch(p), { initialProps: props }); }); + // @ts-ignore expect(hook.result.current).toEqual([OpenSearchRequestState.Loading, null, indexPattern]); - expect(indexPatternService.get).toHaveBeenCalled(); + expect(mockIndexPatternService.get).toHaveBeenCalled(); + }); + + test('useOpenSearchDocSearch using withLongNumeralsSupport when configured to', async () => { + (mockIndexPatternService.isLongNumeralsSupported as jest.Mock).mockReturnValue( + Promise.resolve(true) + ); + + const props = { + id: '1', + index: 'index1', + indexPatternId: 'xyz', + indexPatternService: mockIndexPatternService, + } as DocProps; + + await act(async () => { + renderHook((p: DocProps) => useOpenSearchDocSearch(p), { initialProps: props }); + }); + + expect(mockDataSearchSearch).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withLongNumeralsSupport: true, + }) + ); + }); + + test('useOpenSearchDocSearch without withLongNumeralsSupport when configured not to', async () => { + (mockIndexPatternService.isLongNumeralsSupported as jest.Mock).mockReturnValue( + Promise.resolve(false) + ); + + const props = { + id: '1', + index: 'index1', + indexPatternId: 'xyz', + indexPatternService: mockIndexPatternService, + } as DocProps; + + await act(async () => { + renderHook((p: DocProps) => useOpenSearchDocSearch(p), { initialProps: props }); + }); + + expect(mockDataSearchSearch).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + withLongNumeralsSupport: false, + }) + ); }); }); diff --git a/src/plugins/discover/public/application/components/doc/use_opensearch_doc_search.ts b/src/plugins/discover/public/application/components/doc/use_opensearch_doc_search.ts index 4389485a3a77..ed06c0f4abce 100644 --- a/src/plugins/discover/public/application/components/doc/use_opensearch_doc_search.ts +++ b/src/plugins/discover/public/application/components/doc/use_opensearch_doc_search.ts @@ -77,6 +77,7 @@ export function useOpenSearchDocSearch({ useEffect(() => { async function requestData() { try { + const withLongNumeralsSupport = await indexPatternService.isLongNumeralsSupported(); const indexPatternEntity = await indexPatternService.get(indexPatternId); setIndexPattern(indexPatternEntity); @@ -90,7 +91,7 @@ export function useOpenSearchDocSearch({ }, }, { - withLongNumeralsSupport: true, + withLongNumeralsSupport, } ) .toPromise(); diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx b/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx new file mode 100644 index 000000000000..62153bb064fc --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/use_search.test.tsx @@ -0,0 +1,363 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { createMemoryHistory } from 'history'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router-dom'; +import { Subject } from 'rxjs'; +import { createDataExplorerServicesMock } from '../../../../../data_explorer/public/utils/mocks'; +import { DiscoverViewServices } from '../../../build_services'; +import { discoverPluginMock } from '../../../mocks'; +import { ResultStatus, useSearch } from './use_search'; +import { ISearchSource, UI_SETTINGS } from '../../../../../data/common'; + +jest.mock('./use_index_pattern', () => ({ + useIndexPattern: jest.fn().mockReturnValue(true), +})); + +jest.mock('../../helpers/validate_time_range', () => ({ + validateTimeRange: jest.fn().mockReturnValue(true), +})); + +const mockQuery = { + query: 'test query', + language: 'test language', +}; + +const mockDefaultQuery = { + query: 'default query', + language: 'default language', +}; + +const mockSavedSearch = { + id: 'test-saved-search', + title: 'Test Saved Search', + searchSource: { + setField: jest.fn(), + getField: jest.fn().mockReturnValue(mockQuery), + fetch: jest.fn(), + getSearchRequestBody: jest.fn().mockResolvedValue({}), + getOwnField: jest.fn(), + getDataFrame: jest.fn(() => ({ name: 'test-pattern' })), + }, + getFullPath: jest.fn(), + getOpenSearchType: jest.fn(), +}; + +const mockSavedSearchEmptyQuery = { + id: 'test-saved-search', + title: 'Test Saved Search', + searchSource: { + setField: jest.fn(), + getField: jest.fn().mockReturnValue(undefined), + fetch: jest.fn(), + getSearchRequestBody: jest.fn().mockResolvedValue({}), + getOwnField: jest.fn(), + getDataFrame: jest.fn(() => ({ name: 'test-pattern' })), + }, + getFullPath: jest.fn(), + getOpenSearchType: jest.fn(), +}; + +jest.mock('./update_search_source', () => ({ + updateSearchSource: ({ searchSource }: { searchSource?: ISearchSource }) => searchSource, +})); + +const createMockServices = (): DiscoverViewServices => { + const dataExplorerServicesMock = createDataExplorerServicesMock(); + const discoverServicesMock = discoverPluginMock.createDiscoverServicesMock(); + const services: DiscoverViewServices = { + ...dataExplorerServicesMock, + ...discoverServicesMock, + }; + + (services.data.query.timefilter.timefilter.getRefreshInterval as jest.Mock).mockReturnValue({ + pause: false, + value: 10, + }); + services.getSavedSearchById = jest.fn().mockResolvedValue(mockSavedSearch); + return services; +}; + +const history = createMemoryHistory(); +const mockStore = { + getState: () => ({ + discover: { + savedSearch: 'test-saved-search', + sort: [], + interval: 'auto', + savedQuery: undefined, + }, + }), + subscribe: jest.fn(), + dispatch: jest.fn(), +}; +const wrapper: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; + +describe('useSearch', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should initialize with loading state when search on page load is enabled', async () => { + const services = createMockServices(); + (services.uiSettings.get as jest.Mock).mockReturnValueOnce(true); + + const { result, waitForNextUpdate } = renderHook(() => useSearch(services), { wrapper }); + + expect(result.current.data$.getValue()).toEqual( + expect.objectContaining({ status: ResultStatus.LOADING }) + ); + + // useSearch updates state async in useEffect, wait for it to finish to + // avoid warning + await act(async () => { + await waitForNextUpdate(); + }); + }); + + it('should initialize with uninitialized state when search on page load is disabled', async () => { + const services = createMockServices(); + (services.uiSettings.get as jest.Mock).mockReturnValueOnce(false); + (services.data.query.timefilter.timefilter.getRefreshInterval as jest.Mock).mockReturnValue({ + pause: true, + value: 10, + }); + + const { result, waitForNextUpdate } = renderHook(() => useSearch(services), { wrapper }); + expect(result.current.data$.getValue()).toEqual( + expect.objectContaining({ status: ResultStatus.UNINITIALIZED }) + ); + + await act(async () => { + await waitForNextUpdate(); + }); + }); + + it('should initialize with uninitialized state when dataset type config search on page load is disabled', async () => { + const services = createMockServices(); + (services.uiSettings.get as jest.Mock).mockReturnValueOnce(true); + (services.data.query.queryString.getDatasetService as jest.Mock).mockReturnValue({ + meta: { searchOnLoad: false }, + }); + (services.data.query.timefilter.timefilter.getRefreshInterval as jest.Mock).mockReturnValue({ + pause: true, + value: 10, + }); + + const { result, waitForNextUpdate } = renderHook(() => useSearch(services), { wrapper }); + expect(result.current.data$.getValue()).toEqual( + expect.objectContaining({ status: ResultStatus.UNINITIALIZED }) + ); + + await act(async () => { + await waitForNextUpdate(); + }); + }); + + it('should initialize with uninitialized state when dataset type config search on page load is enabled but the UI setting is disabled', async () => { + const services = createMockServices(); + (services.uiSettings.get as jest.Mock).mockReturnValueOnce(false); + (services.data.query.queryString.getDatasetService as jest.Mock).mockReturnValue({ + meta: { searchOnLoad: true }, + }); + (services.data.query.timefilter.timefilter.getRefreshInterval as jest.Mock).mockReturnValue({ + pause: true, + value: 10, + }); + + const { result, waitForNextUpdate } = renderHook(() => useSearch(services), { wrapper }); + expect(result.current.data$.getValue()).toEqual( + expect.objectContaining({ status: ResultStatus.UNINITIALIZED }) + ); + + await act(async () => { + await waitForNextUpdate(); + }); + }); + + it('should update startTime when hook rerenders', async () => { + const services = createMockServices(); + + const { result, rerender, waitForNextUpdate } = renderHook(() => useSearch(services), { + wrapper, + }); + + const initialStartTime = result.current.data$.getValue().queryStatus?.startTime; + expect(initialStartTime).toBeDefined(); + + act(() => { + rerender(); + }); + + const newStartTime = result.current.data$.getValue().queryStatus?.startTime; + expect(newStartTime).toBeDefined(); + expect(newStartTime).not.toEqual(initialStartTime); + + await act(async () => { + await waitForNextUpdate(); + }); + }); + + it('should reset data observable when dataset changes', async () => { + const services = createMockServices(); + const mockDatasetUpdates$ = new Subject(); + services.data.query.queryString.getUpdates$ = jest.fn().mockReturnValue(mockDatasetUpdates$); + + const { result, waitForNextUpdate } = renderHook(() => useSearch(services), { + wrapper, + }); + + act(() => { + mockDatasetUpdates$.next({ + dataset: { id: 'new-dataset-id', title: 'New Dataset', type: 'INDEX_PATTERN' }, + }); + }); + + act(() => { + result.current.data$.next({ status: ResultStatus.READY }); + }); + + act(() => { + mockDatasetUpdates$.next({ + dataset: { id: 'new-dataset-id', title: 'New Dataset', type: 'INDEX_PATTERN' }, + }); + }); + + expect(result.current.data$.getValue()).toEqual( + expect.objectContaining({ status: ResultStatus.READY }) + ); + + act(() => { + mockDatasetUpdates$.next({ + dataset: { id: 'different-dataset-id', title: 'New Dataset', type: 'INDEX_PATTERN' }, + }); + }); + + await act(async () => { + await waitForNextUpdate(); + }); + + expect(result.current.data$.getValue()).toEqual( + expect.objectContaining({ status: ResultStatus.LOADING, rows: [] }) + ); + }); + + it('should load saved search', async () => { + const services = createMockServices(); + services.data.query.queryString.setQuery = jest.fn(); + + const { waitForNextUpdate } = renderHook(() => useSearch(services), { + wrapper, + }); + + await act(async () => { + await waitForNextUpdate(); + }); + + expect(services.data.query.queryString.setQuery).toBeCalledWith(mockQuery); + }); + + it('if no saved search, use get query', async () => { + const services = createMockServices(); + services.getSavedSearchById = jest.fn().mockResolvedValue(mockSavedSearchEmptyQuery); + services.data.query.queryString.getQuery = jest.fn().mockReturnValue(mockDefaultQuery); + services.data.query.queryString.setQuery = jest.fn(); + + const { waitForNextUpdate } = renderHook(() => useSearch(services), { + wrapper, + }); + + await act(async () => { + await waitForNextUpdate(); + }); + + expect(services.data.query.queryString.setQuery).toBeCalledWith(mockDefaultQuery); + }); + + it('should call fetch without long numerals support when configured not to', async () => { + const services = createMockServices(); + (services.uiSettings.get as jest.Mock).mockImplementation((key) => + Promise.resolve(key === UI_SETTINGS.DATA_WITH_LONG_NUMERALS ? false : undefined) + ); + + const mockDatasetUpdates$ = new Subject(); + services.data.query.queryString.getUpdates$ = jest.fn().mockReturnValue(mockDatasetUpdates$); + + const { waitForNextUpdate } = renderHook(() => useSearch(services), { + wrapper, + }); + + await act(async () => { + await waitForNextUpdate(); + }); + + act(() => { + mockDatasetUpdates$.next({ + dataset: { id: 'new-dataset-id', title: 'New Dataset', type: 'INDEX_PATTERN' }, + }); + }); + + await act(async () => { + try { + await waitForNextUpdate({ timeout: 1000 }); + } catch (_) { + // Do nothing. + } + }); + + expect(mockSavedSearch.searchSource.fetch).toHaveBeenCalledWith( + expect.objectContaining({ + withLongNumeralsSupport: false, + }) + ); + }); + + it('should call fetch with long numerals support when configured to', async () => { + const services = createMockServices(); + (services.uiSettings.get as jest.Mock).mockImplementation((key) => + Promise.resolve(key === UI_SETTINGS.DATA_WITH_LONG_NUMERALS ? true : undefined) + ); + + const mockDatasetUpdates$ = new Subject(); + services.data.query.queryString.getUpdates$ = jest.fn().mockReturnValue(mockDatasetUpdates$); + + const { waitForNextUpdate } = renderHook(() => useSearch(services), { + wrapper, + }); + + await act(async () => { + await waitForNextUpdate(); + }); + + act(() => { + mockDatasetUpdates$.next({ + dataset: { id: 'new-dataset-id', title: 'New Dataset', type: 'INDEX_PATTERN' }, + }); + }); + + await act(async () => { + try { + await waitForNextUpdate({ timeout: 1000 }); + } catch (_) { + // Do nothing. + } + }); + + expect(mockSavedSearch.searchSource.fetch).toHaveBeenCalledWith( + expect.objectContaining({ + withLongNumeralsSupport: true, + }) + ); + }); +}); diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts index 06eabb1e139f..ecbe7c0446c2 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_search.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts @@ -11,7 +11,7 @@ import { useEffect } from 'react'; import { cloneDeep } from 'lodash'; import { RequestAdapter } from '../../../../../inspector/public'; import { DiscoverViewServices } from '../../../build_services'; -import { search } from '../../../../../data/public'; +import { search, UI_SETTINGS } from '../../../../../data/public'; import { validateTimeRange } from '../../helpers/validate_time_range'; import { updateSearchSource } from './update_search_source'; import { useIndexPattern } from './use_index_pattern'; @@ -156,7 +156,7 @@ export const useSearch = (services: DiscoverViewServices) => { // Execute the search const fetchResp = await searchSource.fetch({ abortSignal: fetchStateRef.current.abortController.signal, - withLongNumeralsSupport: true, + withLongNumeralsSupport: await services.uiSettings.get(UI_SETTINGS.DATA_WITH_LONG_NUMERALS), }); inspectorRequest @@ -235,6 +235,11 @@ export const useSearch = (services: DiscoverViewServices) => { ).pipe(debounceTime(100)); const subscription = fetch$.subscribe(() => { + if (skipInitialFetch.current) { + skipInitialFetch.current = false; // Reset so future fetches will proceed normally + return; // Skip the first fetch + } + (async () => { try { await fetch(); From e19b9ec9b54450aef3d60ee9325b448cb1327be1 Mon Sep 17 00:00:00 2001 From: sumukhswamy Date: Thu, 2 Oct 2025 17:12:14 -0700 Subject: [PATCH 2/2] dependencies for COR backport Signed-off-by: sumukhswamy --- .../view_components/utils/use_search.ts | 5 ---- src/plugins/discover/public/mocks.ts | 29 +++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts index ecbe7c0446c2..c596bb8e619c 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_search.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts @@ -235,11 +235,6 @@ export const useSearch = (services: DiscoverViewServices) => { ).pipe(debounceTime(100)); const subscription = fetch$.subscribe(() => { - if (skipInitialFetch.current) { - skipInitialFetch.current = false; // Reset so future fetches will proceed normally - return; // Skip the first fetch - } - (async () => { try { await fetch(); diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts index 4724ced290ff..fdb725860cd4 100644 --- a/src/plugins/discover/public/mocks.ts +++ b/src/plugins/discover/public/mocks.ts @@ -29,6 +29,17 @@ */ import { DiscoverSetup, DiscoverStart } from '.'; +import { coreMock } from '../../../core/public/mocks'; +import { chartPluginMock } from '../../charts/public/mocks'; +import { dataPluginMock } from '../../data/public/mocks'; +import { embeddablePluginMock } from '../../embeddable/public/mocks'; +import { inspectorPluginMock } from '../../inspector/public/mocks'; +import { navigationPluginMock } from '../../navigation/public/mocks'; +import { opensearchDashboardsLegacyPluginMock } from '../../opensearch_dashboards_legacy/public/mocks'; +import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; +import { urlForwardingPluginMock } from '../../url_forwarding/public/mocks'; +import { visualizationsPluginMock } from '../../visualizations/public/mocks'; +import { buildServices, DiscoverServices } from './build_services'; export type Setup = jest.Mocked; export type Start = jest.Mocked; @@ -55,7 +66,25 @@ const createStartContract = (): Start => { return startContract; }; +const createDiscoverServicesMock = (): DiscoverServices => + buildServices( + coreMock.createStart(), + { + data: dataPluginMock.createStartContract(), + charts: chartPluginMock.createStartContract(), + embeddable: embeddablePluginMock.createStartContract(), + inspector: inspectorPluginMock.createStartContract(), + navigation: navigationPluginMock.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), + urlForwarding: urlForwardingPluginMock.createStartContract(), + visualizations: visualizationsPluginMock.createStartContract(), + opensearchDashboardsLegacy: opensearchDashboardsLegacyPluginMock.createStartContract(), + }, + coreMock.createPluginInitializerContext() + ); + export const discoverPluginMock = { createSetupContract, createStartContract, + createDiscoverServicesMock, };