diff --git a/src/plugins/data/public/query/dataset_manager/dataset_manager.test.ts b/src/plugins/data/public/query/dataset_manager/dataset_manager.test.ts index fcf91e6b8f89..47a69000dd8d 100644 --- a/src/plugins/data/public/query/dataset_manager/dataset_manager.test.ts +++ b/src/plugins/data/public/query/dataset_manager/dataset_manager.test.ts @@ -14,20 +14,21 @@ describe('DataSetManager', () => { service = new DataSetManager(coreMock.createSetup().uiSettings); }); - test('getUpdates$ is a cold emits only after query changes', () => { + test('getUpdates$ emits initially and after data set changes', () => { const obs$ = service.getUpdates$(); - const emittedValues: SimpleDataSet[] = []; + const emittedValues: Array = []; obs$.subscribe((v) => { - emittedValues.push(v!); + emittedValues.push(v); }); - expect(emittedValues).toHaveLength(0); + expect(emittedValues).toHaveLength(1); + expect(emittedValues[1]).toEqual(undefined); const newDataSet: SimpleDataSet = { id: 'test_dataset', title: 'Test Dataset' }; service.setDataSet(newDataSet); - expect(emittedValues).toHaveLength(1); - expect(emittedValues[0]).toEqual(newDataSet); + expect(emittedValues).toHaveLength(2); + expect(emittedValues[1]).toEqual(newDataSet); service.setDataSet({ ...newDataSet }); - expect(emittedValues).toHaveLength(2); + expect(emittedValues).toHaveLength(3); }); }); diff --git a/src/plugins/data/public/query/dataset_manager/dataset_manager.ts b/src/plugins/data/public/query/dataset_manager/dataset_manager.ts index 018eba50ad73..4bf73d287ab1 100644 --- a/src/plugins/data/public/query/dataset_manager/dataset_manager.ts +++ b/src/plugins/data/public/query/dataset_manager/dataset_manager.ts @@ -26,7 +26,7 @@ export class DataSetManager { }; public getUpdates$ = () => { - return this.dataSet$.asObservable().pipe(skip(1)); + return this.dataSet$.asObservable(); }; public getDataSet = () => { diff --git a/src/plugins/data/public/query/mocks.ts b/src/plugins/data/public/query/mocks.ts index 3e47bc92752c..200ef46a5175 100644 --- a/src/plugins/data/public/query/mocks.ts +++ b/src/plugins/data/public/query/mocks.ts @@ -33,6 +33,7 @@ import { QueryService, QuerySetup, QueryStart } from '.'; import { timefilterServiceMock } from './timefilter/timefilter_service.mock'; import { createFilterManagerMock } from './filter_manager/filter_manager.mock'; import { queryStringManagerMock } from './query_string/query_string_manager.mock'; +import { dataSetManagerMock } from './dataset_manager/dataset_manager.mock'; type QueryServiceClientContract = PublicMethodsOf; @@ -41,6 +42,7 @@ const createSetupContractMock = () => { filterManager: createFilterManagerMock(), timefilter: timefilterServiceMock.createSetupContract(), queryString: queryStringManagerMock.createSetupContract(), + dataSet: dataSetManagerMock.createSetupContract(), state$: new Observable(), }; @@ -55,6 +57,7 @@ const createStartContractMock = () => { savedQueries: jest.fn() as any, state$: new Observable(), timefilter: timefilterServiceMock.createStartContract(), + dataSet: dataSetManagerMock.createStartContract(), getOpenSearchQuery: jest.fn(), }; diff --git a/src/plugins/query_enhancements/public/plugin.tsx b/src/plugins/query_enhancements/public/plugin.tsx index b74c00ced7e0..0fef52f0a7d7 100644 --- a/src/plugins/query_enhancements/public/plugin.tsx +++ b/src/plugins/query_enhancements/public/plugin.tsx @@ -109,11 +109,7 @@ export class QueryEnhancementsPlugin data.__enhance({ ui: { - queryEditorExtension: createQueryAssistExtension( - core.http, - this.connectionsService, - this.config.queryAssist - ), + queryEditorExtension: createQueryAssistExtension(core.http, data, this.config.queryAssist), }, }); diff --git a/src/plugins/query_enhancements/public/query_assist/components/index_selector.tsx b/src/plugins/query_enhancements/public/query_assist/components/index_selector.tsx deleted file mode 100644 index 4e591e3401c1..000000000000 --- a/src/plugins/query_enhancements/public/query_assist/components/index_selector.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { EuiComboBox, EuiComboBoxOptionOption, EuiText } from '@elastic/eui'; -import React from 'react'; -import { useIndexPatterns, useIndices } from '../hooks/use_indices'; - -interface IndexSelectorProps { - dataSourceId?: string; - selectedIndex?: string; - setSelectedIndex: React.Dispatch>; -} - -// TODO this is a temporary solution, there will be a dataset selector from discover -export const IndexSelector: React.FC = (props) => { - const { data: indices, loading: indicesLoading } = useIndices(props.dataSourceId); - const { data: indexPatterns, loading: indexPatternsLoading } = useIndexPatterns(); - const loading = indicesLoading || indexPatternsLoading; - const indicesAndIndexPatterns = - indexPatterns && indices - ? [...indexPatterns, ...indices].filter( - (v1, index, array) => array.findIndex((v2) => v1 === v2) === index - ) - : []; - const options: EuiComboBoxOptionOption[] = indicesAndIndexPatterns.map((index) => ({ - label: index, - })); - const selectedOptions = props.selectedIndex ? [{ label: props.selectedIndex }] : undefined; - - return ( - Index} - singleSelection={{ asPlainText: true }} - isLoading={loading} - options={options} - selectedOptions={selectedOptions} - onChange={(index) => { - props.setSelectedIndex(index[0].label); - }} - /> - ); -}; diff --git a/src/plugins/query_enhancements/public/query_assist/components/query_assist_bar.tsx b/src/plugins/query_enhancements/public/query_assist/components/query_assist_bar.tsx index c28c5cb8b0be..a24bec6a9f36 100644 --- a/src/plugins/query_enhancements/public/query_assist/components/query_assist_bar.tsx +++ b/src/plugins/query_enhancements/public/query_assist/components/query_assist_bar.tsx @@ -5,6 +5,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiForm, EuiFormRow } from '@elastic/eui'; import React, { SyntheticEvent, useEffect, useMemo, useRef, useState } from 'react'; +import { SimpleDataSet } from '../../../../data/common'; import { IDataPluginServices, PersistedLog, @@ -12,18 +13,15 @@ import { } from '../../../../data/public'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { QueryAssistParameters } from '../../../common/query_assist'; -import { ConnectionsService } from '../../services'; import { getStorage } from '../../services'; import { useGenerateQuery } from '../hooks'; import { getPersistedLog, ProhibitedQueryError } from '../utils'; import { QueryAssistCallOut, QueryAssistCallOutType } from './call_outs'; -import { IndexSelector } from './index_selector'; import { QueryAssistInput } from './query_assist_input'; import { QueryAssistSubmitButton } from './submit_button'; interface QueryAssistInputProps { dependencies: QueryEditorExtensionDependencies; - connectionsService: ConnectionsService; } export const QueryAssistBar: React.FC = (props) => { @@ -37,18 +35,16 @@ export const QueryAssistBar: React.FC = (props) => { const { generateQuery, loading } = useGenerateQuery(); const [callOutType, setCallOutType] = useState(); const dismissCallout = () => setCallOutType(undefined); - const [selectedIndex, setSelectedIndex] = useState(''); - const dataSourceIdRef = useRef(); + const [selectedDataSet, setSelectedDataSet] = useState(); + const selectedIndex = selectedDataSet?.title; const previousQuestionRef = useRef(); useEffect(() => { - const subscription = props.connectionsService - .getSelectedConnection$() - .subscribe((connection) => { - dataSourceIdRef.current = connection?.dataSource?.id; - }); + const subscription = services.data.query.dataSet.getUpdates$().subscribe((dataSet) => { + setSelectedDataSet(dataSet); + }); return () => subscription.unsubscribe(); - }, [props.connectionsService]); + }, [services.data.query.dataSet]); const onSubmit = async (e: SyntheticEvent) => { e.preventDefault(); @@ -67,7 +63,7 @@ export const QueryAssistBar: React.FC = (props) => { question: inputRef.current.value, index: selectedIndex, language: props.dependencies.language, - dataSourceId: dataSourceIdRef.current, + dataSourceId: selectedDataSet?.dataSourceRef?.id, }; const { response, error } = await generateQuery(params); if (error) { @@ -90,13 +86,6 @@ export const QueryAssistBar: React.FC = (props) => { - - - { - data?: T; - loading: boolean; - error?: Error; -} - -type Action = - | { type: 'request' } - | { type: 'success'; payload: State['data'] } - | { type: 'failure'; error: NonNullable['error']> }; - -// TODO use instantiation expressions when typescript is upgraded to >= 4.7 -type GenericReducer = Reducer, Action>; -export const genericReducer: GenericReducer = (state, action) => { - switch (action.type) { - case 'request': - return { data: state.data, loading: true }; - case 'success': - return { loading: false, data: action.payload }; - case 'failure': - return { loading: false, error: action.error }; - default: - return state; - } -}; - -export const useIndices = (dataSourceId: string | undefined) => { - const reducer: GenericReducer = genericReducer; - const [state, dispatch] = useReducer(reducer, { loading: false }); - const [refresh, setRefresh] = useState({}); - const { services } = useOpenSearchDashboards(); - - useEffect(() => { - const abortController = new AbortController(); - dispatch({ type: 'request' }); - services.http - .post('/api/console/proxy', { - query: { path: '_cat/indices?format=json', method: 'GET', dataSourceId }, - signal: abortController.signal, - }) - .then((payload: CatIndicesResponse) => - dispatch({ - type: 'success', - payload: payload - .filter((meta) => meta.index && !meta.index.startsWith('.')) - .map((meta) => meta.index!), - }) - ) - .catch((error) => dispatch({ type: 'failure', error })); - - return () => abortController.abort(); - }, [refresh, services.http, dataSourceId]); - - return { ...state, refresh: () => setRefresh({}) }; -}; - -export const useIndexPatterns = () => { - const reducer: GenericReducer = genericReducer; - const [state, dispatch] = useReducer(reducer, { loading: false }); - const [refresh, setRefresh] = useState({}); - const { services } = useOpenSearchDashboards(); - - useEffect(() => { - let abort = false; - dispatch({ type: 'request' }); - - services.data.indexPatterns - .getTitles() - .then((payload) => { - if (!abort) - dispatch({ - type: 'success', - // temporary solution does not support index patterns from other data sources - payload: payload.filter((title) => !title.includes('::')), - }); - }) - .catch((error) => { - if (!abort) dispatch({ type: 'failure', error }); - }); - - return () => { - abort = true; - }; - }, [refresh, services.data.indexPatterns]); - - return { ...state, refresh: () => setRefresh({}) }; -}; 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 41fc36dd71c7..9263c09662e6 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,13 @@ 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 { SimpleDataSet } from '../../../../data/common'; import { IIndexPattern } from '../../../../data/public'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { DataSetContract } from '../../../../data/public/query'; import { ConfigSchema } from '../../../common/config'; -import { ConnectionsService } from '../../data_source_connection'; -import { Connection } from '../../types'; import { createQueryAssistExtension } from './create_extension'; const coreSetupMock = coreMock.createSetup({ @@ -21,6 +23,18 @@ const coreSetupMock = coreMock.createSetup({ }, }); const httpMock = coreSetupMock.http; +const dataMock = dataPluginMock.createSetupContract(); +const dataSetMock = dataMock.query.dataSet as jest.Mocked; + +const mockSimpleDataSet = { + id: 'mock-data-set-id', + title: 'mock-title', + dataSourceRef: { + id: 'mock-data-source-id', + }, +} as SimpleDataSet; + +dataSetMock.getUpdates$.mockReturnValue(of(mockSimpleDataSet)); jest.mock('../components', () => ({ QueryAssistBar: jest.fn(() =>
QueryAssistBar
), @@ -35,19 +49,10 @@ describe('CreateExtension', () => { const config: ConfigSchema['queryAssist'] = { supportedLanguages: [{ language: 'PPL', agentConfig: 'os_query_assist_ppl' }], }; - const connectionsService = new ConnectionsService({ - startServices: coreSetupMock.getStartServices(), - http: httpMock, - }); - - // for these tests we only need id field in the connection - connectionsService.setSelectedConnection$({ - dataSource: { id: 'mock-data-source-id' }, - } as Connection); it('should be enabled if at least one language is configured', async () => { httpMock.get.mockResolvedValueOnce({ configuredLanguages: ['PPL'] }); - const extension = createQueryAssistExtension(httpMock, connectionsService, config); + const extension = createQueryAssistExtension(httpMock, dataMock, config); const isEnabled = await firstValueFrom(extension.isEnabled$({ language: 'PPL' })); expect(isEnabled).toBeTruthy(); expect(httpMock.get).toBeCalledWith('/api/enhancements/assist/languages', { @@ -57,7 +62,7 @@ describe('CreateExtension', () => { it('should be disabled for unsupported language', async () => { httpMock.get.mockRejectedValueOnce(new Error('network failure')); - const extension = createQueryAssistExtension(httpMock, connectionsService, config); + const extension = createQueryAssistExtension(httpMock, dataMock, config); const isEnabled = await firstValueFrom(extension.isEnabled$({ language: 'PPL' })); expect(isEnabled).toBeFalsy(); expect(httpMock.get).toBeCalledWith('/api/enhancements/assist/languages', { @@ -67,7 +72,7 @@ describe('CreateExtension', () => { it('should render the component if language is supported', async () => { httpMock.get.mockResolvedValueOnce({ configuredLanguages: ['PPL'] }); - const extension = createQueryAssistExtension(httpMock, connectionsService, config); + const extension = createQueryAssistExtension(httpMock, dataMock, config); const component = extension.getComponent?.({ language: 'PPL', indexPatterns: [{ id: 'test-pattern' }] as IIndexPattern[], @@ -84,7 +89,7 @@ describe('CreateExtension', () => { it('should render the banner if language is not supported', async () => { httpMock.get.mockResolvedValueOnce({ configuredLanguages: ['PPL'] }); - const extension = createQueryAssistExtension(httpMock, connectionsService, config); + const extension = createQueryAssistExtension(httpMock, dataMock, config); const banner = extension.getBanner?.({ language: 'DQL', indexPatterns: [{ id: 'test-pattern' }] as IIndexPattern[], 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 23611e39501e..5b7f8b55e842 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 @@ -6,15 +6,15 @@ import { HttpSetup } from 'opensearch-dashboards/public'; import React, { useEffect, useState } from 'react'; import { of } from 'rxjs'; -import { distinctUntilChanged, switchMap, map } from 'rxjs/operators'; +import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; import { + DataPublicPluginSetup, QueryEditorExtensionConfig, QueryEditorExtensionDependencies, } from '../../../../data/public'; import { API } from '../../../common'; import { ConfigSchema } from '../../../common/config'; -import { ConnectionsService } from '../../services'; -import { QueryAssistBar, QueryAssistBanner } from '../components'; +import { QueryAssistBanner, QueryAssistBar } from '../components'; /** * @returns observable list of query assist agent configured languages in the @@ -22,13 +22,13 @@ import { QueryAssistBar, QueryAssistBanner } from '../components'; */ const getAvailableLanguages$ = ( availableLanguagesByDataSource: Map, - connectionsService: ConnectionsService, - http: HttpSetup + http: HttpSetup, + data: DataPublicPluginSetup ) => - connectionsService.getSelectedConnection$().pipe( + data.query.dataSet.getUpdates$().pipe( distinctUntilChanged(), - switchMap(async (connection) => { - const dataSourceId = connection?.dataSource?.id; + switchMap(async (simpleDataSet) => { + const dataSourceId = simpleDataSet?.dataSourceRef?.id; const cached = availableLanguagesByDataSource.get(dataSourceId); if (cached !== undefined) return cached; const languages = await http @@ -44,7 +44,7 @@ const getAvailableLanguages$ = ( export const createQueryAssistExtension = ( http: HttpSetup, - connectionsService: ConnectionsService, + data: DataPublicPluginSetup, config: ConfigSchema['queryAssist'] ): QueryEditorExtensionConfig => { const availableLanguagesByDataSource: Map = new Map(); @@ -58,7 +58,7 @@ export const createQueryAssistExtension = ( if (dependencies.dataSource && dependencies.dataSource?.getType() !== 'default') return of(false); - return getAvailableLanguages$(availableLanguagesByDataSource, connectionsService, http).pipe( + return getAvailableLanguages$(availableLanguagesByDataSource, http, data).pipe( map((languages) => languages.length > 0) ); }, @@ -68,10 +68,10 @@ export const createQueryAssistExtension = ( - + ); }, @@ -81,8 +81,8 @@ export const createQueryAssistExtension = ( conf.language)} /> @@ -95,8 +95,8 @@ export const createQueryAssistExtension = ( interface QueryAssistWrapperProps { availableLanguagesByDataSource: Map; dependencies: QueryEditorExtensionDependencies; - connectionsService: ConnectionsService; http: HttpSetup; + data: DataPublicPluginSetup; invert?: boolean; } @@ -108,8 +108,8 @@ const QueryAssistWrapper: React.FC = (props) => { const subscription = getAvailableLanguages$( props.availableLanguagesByDataSource, - props.connectionsService, - props.http + props.http, + props.data ).subscribe((languages) => { const available = languages.includes(props.dependencies.language); if (mounted) setVisible(props.invert ? !available : available);