diff --git a/src/plugins/data/public/query/query_string/dataset_service/lib/utils.test.ts b/src/plugins/data/public/query/query_string/dataset_service/lib/utils.test.ts new file mode 100644 index 000000000000..d9231e1015da --- /dev/null +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/utils.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LanguageServiceContract } from '../..'; +import { IQueryStart } from '../../..'; +import { DataStructure, DATA_STRUCTURE_META_TYPES } from '../../../../../common'; +import { dataPluginMock } from '../../../../mocks'; +import { setQueryService } from '../../../../services'; +import { injectMetaToDataStructures } from './utils'; + +const mockDataStructures: DataStructure[] = [ + { + id: 'fe25e2a0-6566-11ef-bb0e-0b6b1035facb', + title: 'mock-index-pattern-title', + type: 'INDEX_PATTERN', + parent: { + id: '8f26d980-63f5-11ef-b231-09f3ad4fb0e0', + title: 'mock-data-source-title', + type: 'OpenSearch', + }, + meta: { type: DATA_STRUCTURE_META_TYPES.CUSTOM }, + }, +]; + +const dataMock = dataPluginMock.createSetupContract(); +const languageServiceMock = dataMock.query.queryString.getLanguageService() as jest.Mocked< + LanguageServiceContract +>; +setQueryService({ queryString: dataMock.query.queryString } as IQueryStart); + +languageServiceMock.getQueryEditorExtensionMap.mockReturnValue({ + 'mock-extension-1': { + id: 'mock-extension-1', + order: 1, + isEnabled$: jest.fn(), + getDataStructureMeta: (dataSourceId) => + Promise.resolve({ + type: DATA_STRUCTURE_META_TYPES.FEATURE, + icon: { type: 'icon1' }, + }), + }, + 'mock-extension-2': { + id: 'mock-extension-2', + order: 2, + isEnabled$: jest.fn(), + getDataStructureMeta: (dataSourceId) => + Promise.resolve({ + type: DATA_STRUCTURE_META_TYPES.FEATURE, + icon: { type: 'icon2' }, + tooltip: 'mock-extension-2', + }), + }, +}); + +describe('Utils injectMetaToDataStructures', () => { + it('should inject meta', async () => { + const dataStructures = await injectMetaToDataStructures(mockDataStructures); + expect(dataStructures[0].meta).toMatchInlineSnapshot(` + Object { + "icon": Object { + "type": "icon1", + }, + "tooltip": "mock-extension-2", + "type": "CUSTOM", + } + `); + }); + + it('does not change meta if not available', async () => { + languageServiceMock.getQueryEditorExtensionMap.mockReturnValue({ + 'mock-extension-3': { + id: 'mock-extension-3', + order: 3, + isEnabled$: jest.fn(), + }, + }); + const dataStructures = await injectMetaToDataStructures(mockDataStructures); + expect(dataStructures[0].meta).toBe(mockDataStructures[0].meta); + }); +}); diff --git a/src/plugins/query_enhancements/public/query_assist/utils/create_extension.test.tsx b/src/plugins/query_enhancements/public/query_assist/utils/create_extension.test.tsx index 955fd8cd1569..6d915d96ff09 100644 --- a/src/plugins/query_enhancements/public/query_assist/utils/create_extension.test.tsx +++ b/src/plugins/query_enhancements/public/query_assist/utils/create_extension.test.tsx @@ -6,11 +6,12 @@ import { firstValueFrom } from '@osd/std'; import { act, render, screen } from '@testing-library/react'; import React from 'react'; +import { of } from 'rxjs'; import { coreMock } from '../../../../../core/public/mocks'; -import { QueryEditorExtensionDependencies } from '../../../../data/public'; +import { QueryEditorExtensionDependencies, QueryStringContract } from '../../../../data/public'; import { dataPluginMock } from '../../../../data/public/mocks'; import { ConfigSchema } from '../../../common/config'; -import { createQueryAssistExtension } from './create_extension'; +import { clearCache, createQueryAssistExtension } from './create_extension'; const coreSetupMock = coreMock.createSetup({ pluginStartDeps: { @@ -21,14 +22,32 @@ const coreSetupMock = coreMock.createSetup({ }); const httpMock = coreSetupMock.http; const dataMock = dataPluginMock.createSetupContract(); +const queryStringMock = dataMock.query.queryString as jest.Mocked; + +const mockQueryWithIndexPattern = { + query: '', + language: 'kuery', + dataset: { + id: 'mock-index-pattern-id', + title: 'mock-index', + type: 'INDEX_PATTERN', + dataSource: { + id: 'mock-data-source-id', + title: 'test-mds', + type: 'OpenSearch', + }, + }, +}; + +queryStringMock.getQuery.mockReturnValue(mockQueryWithIndexPattern); +queryStringMock.getUpdates$.mockReturnValue(of(mockQueryWithIndexPattern)); jest.mock('../components', () => ({ QueryAssistBar: jest.fn(() =>
QueryAssistBar
), QueryAssistBanner: jest.fn(() =>
QueryAssistBanner
), })); -// TODO: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/7860 -describe.skip('CreateExtension', () => { +describe('CreateExtension', () => { const dependencies: QueryEditorExtensionDependencies = { language: 'PPL', onSelectLanguage: jest.fn(), @@ -37,6 +56,7 @@ describe.skip('CreateExtension', () => { }; afterEach(() => { jest.clearAllMocks(); + clearCache(); }); const config: ConfigSchema['queryAssist'] = { @@ -53,7 +73,7 @@ describe.skip('CreateExtension', () => { }); }); - it('should be disabled for unsupported language', async () => { + it('should be disabled when there is an error', async () => { httpMock.get.mockRejectedValueOnce(new Error('network failure')); const extension = createQueryAssistExtension(httpMock, dataMock, config); const isEnabled = await firstValueFrom(extension.isEnabled$(dependencies)); @@ -63,6 +83,35 @@ describe.skip('CreateExtension', () => { }); }); + it('creates data structure meta', async () => { + httpMock.get.mockResolvedValueOnce({ configuredLanguages: ['PPL'] }); + const extension = createQueryAssistExtension(httpMock, dataMock, config); + const meta = await extension.getDataStructureMeta?.('mock-data-source-id2'); + expect(meta).toMatchInlineSnapshot(` + Object { + "icon": Object { + "type": "test-file-stub", + }, + "tooltip": "Query assist is available", + "type": "FEATURE", + } + `); + expect(httpMock.get).toBeCalledWith('/api/enhancements/assist/languages', { + query: { dataSourceId: 'mock-data-source-id2' }, + }); + }); + + it('does not send multiple requests for the same data source', async () => { + httpMock.get.mockResolvedValueOnce({ configuredLanguages: ['PPL'] }); + const extension = createQueryAssistExtension(httpMock, dataMock, config); + const metas = await Promise.all( + Array.from({ length: 10 }, () => extension.getDataStructureMeta?.('mock-data-source-id2')) + ); + metas.push(await extension.getDataStructureMeta?.('mock-data-source-id2')); + metas.forEach((meta) => expect(meta?.type).toBe('FEATURE')); + expect(httpMock.get).toBeCalledTimes(1); + }); + it('should render the component if language is supported', async () => { httpMock.get.mockResolvedValueOnce({ configuredLanguages: ['PPL'] }); const extension = createQueryAssistExtension(httpMock, dataMock, config); diff --git a/src/plugins/query_enhancements/public/query_assist/utils/create_extension.tsx b/src/plugins/query_enhancements/public/query_assist/utils/create_extension.tsx index 3b0b12760749..2b86245cc43d 100644 --- a/src/plugins/query_enhancements/public/query_assist/utils/create_extension.tsx +++ b/src/plugins/query_enhancements/public/query_assist/utils/create_extension.tsx @@ -18,35 +18,41 @@ import { ConfigSchema } from '../../../common/config'; import assistantMark from '../../assets/query_assist_mark.svg'; import { QueryAssistBanner, QueryAssistBar } from '../components'; -/** - * @returns list of query assist supported languages for the given data source. - */ -const getAvailableLanguagesForDataSource = (() => { +const [getAvailableLanguagesForDataSource, clearCache] = (() => { const availableLanguagesByDataSource: Map = new Map(); const pendingRequests: Map> = new Map(); - return async (http: HttpSetup, dataSourceId: string | undefined) => { - const cached = availableLanguagesByDataSource.get(dataSourceId); - if (cached !== undefined) return cached; - - const pendingRequest = pendingRequests.get(dataSourceId); - if (pendingRequest !== undefined) return pendingRequest; - - const languagesPromise = http - .get<{ configuredLanguages: string[] }>(API.QUERY_ASSIST.LANGUAGES, { - query: { dataSourceId }, - }) - .then((response) => response.configuredLanguages) - .catch(() => []) - .finally(() => pendingRequests.delete(dataSourceId)); - pendingRequests.set(dataSourceId, languagesPromise); - - const languages = await languagesPromise; - availableLanguagesByDataSource.set(dataSourceId, languages); - return languages; - }; + return [ + async (http: HttpSetup, dataSourceId: string | undefined) => { + const cached = availableLanguagesByDataSource.get(dataSourceId); + if (cached !== undefined) return cached; + + const pendingRequest = pendingRequests.get(dataSourceId); + if (pendingRequest !== undefined) return pendingRequest; + + const languagesPromise = http + .get<{ configuredLanguages: string[] }>(API.QUERY_ASSIST.LANGUAGES, { + query: { dataSourceId }, + }) + .then((response) => response.configuredLanguages) + .catch(() => []) + .finally(() => pendingRequests.delete(dataSourceId)); + pendingRequests.set(dataSourceId, languagesPromise); + + const languages = await languagesPromise; + availableLanguagesByDataSource.set(dataSourceId, languages); + return languages; + }, + () => { + availableLanguagesByDataSource.clear(); + pendingRequests.clear(); + }, + ]; })(); +// visible for testing +export { clearCache }; + /** * @returns observable list of query assist agent configured languages in the * selected data source.