diff --git a/package.json b/package.json index 1c2a7f61d09b..4a9484a8d840 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "start": "scripts/use_node scripts/opensearch_dashboards --dev", "start:docker": "scripts/use_node scripts/opensearch_dashboards --dev --opensearch.hosts=$OPENSEARCH_HOSTS --opensearch.ignoreVersionMismatch=true --server.host=$SERVER_HOST", "start:security": "scripts/use_node scripts/opensearch_dashboards --dev --security", - "start:enhancements": "scripts/use_node scripts/opensearch_dashboards --dev --uiSettings.overrides['query:enhancements:enabled']=true --uiSettings.overrides['home:useNewHomePage']=true --uiSettings.overrides['state:storeInSessionStorage']=true", + "start:enhancements": "scripts/use_node scripts/opensearch_dashboards --dev --uiSettings.overrides['query:enhancements:enabled']=true --uiSettings.overrides['home:useNewHomePage']=true", "debug": "scripts/use_node --nolazy --inspect scripts/opensearch_dashboards --dev", "debug-break": "scripts/use_node --nolazy --inspect-brk scripts/opensearch_dashboards --dev", "lint": "yarn run lint:es && yarn run lint:style", diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index ad934e0a73ae..9dc6a84b484a 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -457,14 +457,14 @@ export class SavedObjectsClient { * { id: 'foo', type: 'index-pattern' } * ]) */ - public bulkGet = (objects: Array<{ id: string; type: string }> = []) => { + public bulkGet = (objects: Array<{ id: string; type: string }> = []) => { const filteredObjects = objects.map((obj) => pick(obj, ['id', 'type'])); return this.performBulkGet(filteredObjects).then((resp) => { resp.saved_objects = resp.saved_objects.map((d) => this.createSavedObject(d)); return renameKeys< PromiseType>, - SavedObjectsBatchResponse - >({ saved_objects: 'savedObjects' }, resp) as SavedObjectsBatchResponse; + SavedObjectsBatchResponse + >({ saved_objects: 'savedObjects' }, resp) as SavedObjectsBatchResponse; }); }; diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index c3328d0a1a50..f81f63066ea7 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -42,105 +42,6 @@ export const DEFAULT_DATA = { tooltip: 'Root Data Structure', }, } as DataStructure, - - NULL: { - id: 'NULL', - title: 'Empty', - type: 'NULL', - meta: { - type: DATA_STRUCTURE_META_TYPES.FEATURE, - icon: 'empty', - tooltip: 'Empty Data Structure', - }, - } as DataStructure, - - CATEGORY: { - id: 'CATEGORY', - title: 'Categories', - type: 'CATEGORY', - meta: { - type: DATA_STRUCTURE_META_TYPES.FEATURE, - icon: 'listAdd', - tooltip: 'Data Categories', - }, - } as DataStructure, - - DATA_SOURCE: { - id: 'DATA_SOURCE', - title: 'Clusters', - type: 'DATA_SOURCE', - meta: { - type: DATA_STRUCTURE_META_TYPES.FEATURE, - icon: 'cluster', - tooltip: 'Data Clusters', - }, - } as DataStructure, - - CONNECTION: { - id: 'CONNECTION', - title: 'Connections', - type: 'CONNECTION', - meta: { - type: DATA_STRUCTURE_META_TYPES.FEATURE, - icon: 'link', - tooltip: 'Data Connections', - }, - } as DataStructure, - - INDEX: { - id: 'INDEX', - title: 'Indexes', - type: 'INDEX', - meta: { - type: DATA_STRUCTURE_META_TYPES.FEATURE, - icon: 'indexOpen', - tooltip: 'Data Indexes', - }, - } as DataStructure, - - FIELD: { - id: 'FIELD', - title: 'Fields', - type: 'FIELD', - meta: { - type: DATA_STRUCTURE_META_TYPES.FEATURE, - icon: 'field', - tooltip: 'Data Fields', - }, - } as DataStructure, - - TIME_FIELD: { - id: 'TIME_FIELD', - title: 'Time fields', - type: 'TIME_FIELD', - meta: { - type: DATA_STRUCTURE_META_TYPES.FEATURE, - icon: 'clock', - tooltip: 'Time Fields', - }, - } as DataStructure, - - DATASET: { - id: 'DATASET', - title: 'Datasets', - type: 'DATASET', - meta: { - type: DATA_STRUCTURE_META_TYPES.FEATURE, - icon: 'document', - tooltip: 'Datasets', - }, - } as DataStructure, - - DATASET_CATEGORY: { - id: 'DATASET_CATEGORY', - title: 'Datasets', - type: 'DATASET_CATEGORY', - meta: { - type: DATA_STRUCTURE_META_TYPES.FEATURE, - icon: 'documents', - tooltip: 'Dataset Categories', - }, - } as DataStructure, }, SET_TYPES: { diff --git a/src/plugins/data/common/data_frames/utils.ts b/src/plugins/data/common/data_frames/utils.ts index c5303e0260b4..8c9f63b0f0d3 100644 --- a/src/plugins/data/common/data_frames/utils.ts +++ b/src/plugins/data/common/data_frames/utils.ts @@ -18,7 +18,7 @@ import { import { IFieldType } from './fields'; import { IndexPatternFieldMap, IndexPatternSpec } from '../index_patterns'; import { IOpenSearchDashboardsSearchRequest } from '../search'; -import { GetAggTypeFn, GetDataFrameAggQsFn, TimeRange } from '../types'; +import { GetAggTypeFn, GetDataFrameAggQsFn, Query, TimeRange } from '../types'; /** * Returns the raw data frame from the search request. @@ -380,25 +380,25 @@ export const createDataFrame = (partial: PartialDataFrame): IDataFrame | IDataFr */ export const updateDataFrameMeta = ({ dataFrame, - qs, + query, aggConfig, timeField, timeFilter, getAggQsFn, }: { dataFrame: IDataFrame; - qs: string; + query: Query; aggConfig: DataFrameAggConfig; timeField: any; timeFilter: string; getAggQsFn: GetDataFrameAggQsFn; }) => { dataFrame.meta = { - ...dataFrame.meta, + ...dataFrame?.meta, aggs: aggConfig, aggsQs: { [aggConfig.id]: getAggQsFn({ - qs, + query, aggConfig, timeField, timeFilter, @@ -411,7 +411,7 @@ export const updateDataFrameMeta = ({ for (const [key, subAgg] of Object.entries(subAggs)) { const subAggConfig: Record = { [key]: subAgg }; dataFrame.meta.aggsQs[subAgg.id] = getAggQsFn({ - qs, + query, aggConfig: subAggConfig as DataFrameAggConfig, timeField, timeFilter, diff --git a/src/plugins/data/common/datasets/types.ts b/src/plugins/data/common/datasets/types.ts index 5e2cfd4673c5..69549a631ab9 100644 --- a/src/plugins/data/common/datasets/types.ts +++ b/src/plugins/data/common/datasets/types.ts @@ -119,9 +119,7 @@ export interface DataStructure { parent?: DataStructure; /** Optional array of child data structures */ children?: DataStructure[]; - /** Optional array of data structures of ancestors */ - path?: DataStructure[]; - isLeaf?: boolean; + hasNext?: boolean; columnHeader?: string; /** Optional metadata for the data structure */ meta?: DataStructureMeta; @@ -152,7 +150,6 @@ export interface DataStructureDataTypeMeta { type: DATA_STRUCTURE_META_TYPES.TYPE; icon: EuiIconProps; tooltip: string; - isLeaf?: boolean; } /** @@ -203,7 +200,6 @@ export interface BaseDataset { type: string; /** Optional reference to the data source */ dataSource?: DataSource; - fields?: DatasetField[]; } /** @@ -220,10 +216,10 @@ export interface BaseDataset { * type: "INDEX_PATTERN", * timeFieldName: "@timestamp", * dataSource: { - * id: "main-cluster", - * title: "Main OpenSearch Cluster", - * type: "OPENSEARCH" - * }, + * id: "2e1b1b80-9c4d-11ee-8c90-0242ac120001", + * title: "Cluster1", + * type: "OpenSearch" + * } * }; * * @example 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 3d0dbe15dab7..ccda27870313 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 @@ -122,6 +122,7 @@ export class IndexPatternsService { this.savedObjectsCache = await Promise.all( this.savedObjectsCache.map(async (obj) => { + // TODO: This behaviour will cause the index pattern title to be resolved differently depending on how its fetched since the get method in this service will not append the datasource title if (obj.type === 'index-pattern') { const result = { ...obj }; result.attributes.title = await getIndexPatternTitle( diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index e6ce014a7835..84a094637481 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -320,16 +320,9 @@ export class SearchSource { * @return {undefined|IDataFrame} */ async createDataFrame(searchRequest: SearchRequest) { - const rawQueryString = this.getRawQueryStringFromRequest(searchRequest); const dataFrame = createDataFrame({ name: searchRequest.index.title || searchRequest.index, fields: [], - ...(rawQueryString && { - meta: { - queryConfig: { qs: rawQueryString }, - ...(searchRequest.dataSourceId && { dataSource: searchRequest.dataSourceId }), - }, - }), }); await this.setDataFrame(dataFrame); return this.getDataFrame(); diff --git a/src/plugins/data/common/types.ts b/src/plugins/data/common/types.ts index 57dfa2bde7a8..eb8b6500b6fa 100644 --- a/src/plugins/data/common/types.ts +++ b/src/plugins/data/common/types.ts @@ -29,6 +29,7 @@ */ import { DataFrameAggConfig, IDataFrame } from './data_frames'; +import { Query } from './query'; import { BucketAggType, MetricAggType } from './search'; export * from './query/types'; @@ -50,12 +51,12 @@ export * from './datasets/types'; export type GetConfigFn = (key: string, defaultOverride?: T) => T; export type GetDataFrameFn = () => IDataFrame | undefined; export type GetDataFrameAggQsFn = ({ - qs, + query, aggConfig, timeField, timeFilter, }: { - qs: string; + query: Query; aggConfig: DataFrameAggConfig; timeField: any; timeFilter: any; diff --git a/src/plugins/data/public/antlr/opensearch_sql/code_completion.ts b/src/plugins/data/public/antlr/opensearch_sql/code_completion.ts index 1e3ec7392bc9..bfd0d9d2da9c 100644 --- a/src/plugins/data/public/antlr/opensearch_sql/code_completion.ts +++ b/src/plugins/data/public/antlr/opensearch_sql/code_completion.ts @@ -47,7 +47,7 @@ export const getSuggestions = async ({ services, }: QuerySuggestionGetFnArgs): Promise => { const { api } = services.uiSettings; - const datasetManager = services.data.query.queryString.getDatasetManager(); + const queryString = services.data.query.queryString; const { lineNumber, column } = position || {}; const suggestions = getOpenSearchSqlAutoCompleteSuggestions(query, { line: lineNumber || selectionStart, @@ -60,7 +60,7 @@ export const getSuggestions = async ({ // Fetch columns and values if (suggestions.suggestColumns?.tables?.length) { const tableNames = suggestions.suggestColumns.tables.map((table) => table.name); - const schemas = await fetchTableSchemas(tableNames, api, datasetManager); + const schemas = await fetchTableSchemas(tableNames, api, queryString); (schemas as IDataFrameResponse[]).forEach((schema: IDataFrameResponse) => { if ('body' in schema && schema.body && 'fields' in schema.body) { diff --git a/src/plugins/data/public/antlr/shared/utils.test.ts b/src/plugins/data/public/antlr/shared/utils.test.ts index 7f8891a63e16..905634e0940d 100644 --- a/src/plugins/data/public/antlr/shared/utils.test.ts +++ b/src/plugins/data/public/antlr/shared/utils.test.ts @@ -5,7 +5,7 @@ import { of } from 'rxjs'; import { fetchData } from './utils'; -import { DatasetManager } from '../../query'; +import { QueryStringManager } from '../../query'; describe('fetchData', () => { it('should fetch data using the dataSourceRequestHandler', async () => { @@ -26,18 +26,20 @@ describe('fetchData', () => { fetch: jest.fn().mockResolvedValue('fetchedData'), }, }; - const mockDatasetManager: Partial = { + const mockQueryString: Partial = { getUpdates$: jest .fn() .mockReturnValue(of({ dataSourceRef: { id: 'testId', name: 'testTitle' } })), - getDataset: jest.fn().mockReturnValue({ dataSourceRef: { id: 'testId', name: 'testTitle' } }), + getDatasetService: jest + .fn() + .mockReturnValue({ dataSourceRef: { id: 'testId', name: 'testTitle' } }), }; const result = await fetchData( mockTables, mockQueryFormatter, mockApi, - mockDatasetManager as DatasetManager + mockQueryString as QueryStringManager ); expect(result).toEqual(['fetchedData', 'fetchedData']); expect(mockQueryFormatter).toHaveBeenCalledWith('table1', 'testId', 'testTitle'); @@ -59,16 +61,16 @@ describe('fetchData', () => { fetch: jest.fn().mockResolvedValue('fetchedData'), }, }; - const mockDatasetManager: Partial = { + const mockQueryString: Partial = { getUpdates$: jest.fn().mockReturnValue(of(undefined)), - getDataset: jest.fn().mockReturnValue(undefined), + getDatasetService: jest.fn().mockReturnValue(undefined), }; const result = await fetchData( mockTables, mockQueryFormatter, mockApi, - mockDatasetManager as DatasetManager + mockQueryString as QueryStringManager ); expect(result).toEqual(['fetchedData', 'fetchedData']); expect(mockQueryFormatter).toHaveBeenCalledWith('table1'); diff --git a/src/plugins/data/public/antlr/shared/utils.ts b/src/plugins/data/public/antlr/shared/utils.ts index 3c1ab4cbb938..7ae9dd18940b 100644 --- a/src/plugins/data/public/antlr/shared/utils.ts +++ b/src/plugins/data/public/antlr/shared/utils.ts @@ -5,7 +5,7 @@ import { from } from 'rxjs'; import { distinctUntilChanged, startWith, switchMap } from 'rxjs/operators'; -import { DatasetContract } from '../../query'; +import { QueryStringContract } from '../../query'; export interface IDataSourceRequestHandlerParams { dataSourceId: string; @@ -14,22 +14,22 @@ export interface IDataSourceRequestHandlerParams { // Function to get raw suggestion data export const getRawSuggestionData$ = ( - datasetManager: DatasetContract, + queryString: QueryStringContract, dataSourceRequestHandler: ({ dataSourceId, title, }: IDataSourceRequestHandlerParams) => Promise, defaultRequestHandler: () => Promise ) => - datasetManager.getUpdates$().pipe( - startWith(datasetManager.getDataset()), + queryString.getUpdates$().pipe( + startWith(queryString.getQuery()), distinctUntilChanged(), - switchMap((dataset) => { - if (!dataset) { + switchMap((query) => { + if (!query) { return from(defaultRequestHandler()); } - const dataSourceId = dataset?.dataSource?.id; - const title = dataset?.dataSource?.title; + const dataSourceId = query.dataset?.dataSource?.id; + const title = query.dataset?.dataSource?.title; return from(dataSourceRequestHandler({ dataSourceId, title })); }) ); @@ -52,11 +52,11 @@ export const fetchData = ( tables: string[], queryFormatter: (table: string, dataSourceId?: string, title?: string) => any, api: any, - datasetManager: DatasetContract + queryString: QueryStringContract ) => { return new Promise((resolve, reject) => { getRawSuggestionData$( - datasetManager, + queryString, ({ dataSourceId, title }) => { const requests = tables.map(async (table) => { const body = JSON.stringify(queryFormatter(table, dataSourceId, title)); @@ -82,11 +82,11 @@ export const fetchData = ( }; // Specific fetch function for table schemas -export const fetchTableSchemas = (tables: string[], api: any, datasetManager: DatasetContract) => { +export const fetchTableSchemas = (tables: string[], api: any, queryString: QueryStringContract) => { return fetchData( tables, (table, dataSourceId, title) => ({ - query: { qs: `DESCRIBE TABLES LIKE ${table}`, format: 'jdbc' }, + query: { query: `DESCRIBE TABLES LIKE ${table}`, format: 'jdbc' }, df: { meta: { queryConfig: { @@ -97,6 +97,6 @@ export const fetchTableSchemas = (tables: string[], api: any, datasetManager: Da }, }), api, - datasetManager + queryString ); }; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index e5cb26ceaf12..a4f93ff2fb41 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -445,7 +445,7 @@ export { QueryEditorTopRow, // for BWC, keeping the old name IUiStart as DataPublicPluginStartUi, - DataSetNavigator, + useQueryStringManager, } from './ui'; /** @@ -462,9 +462,14 @@ export { QueryState, getDefaultQuery, FilterManager, - DatasetContract, - DatasetManager, - DatasetHandlerConfig, + QueryStringContract, + QueryStringManager, + DatasetTypeConfig, + DatasetService, + DatasetServiceContract, + LanguageConfig, + LanguageService, + LanguageServiceContract, SavedQuery, SavedQueryService, SavedQueryTimeFilter, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 09bdd96d58e2..ec267a20eee1 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -134,9 +134,15 @@ export class DataPublicPlugin expressions.registerFunction(opensearchaggs); expressions.registerFunction(indexPatternLoad); + const searchService = this.searchService.setup(core, { + usageCollection, + expressions, + }); + const queryService = this.queryService.setup({ uiSettings: core.uiSettings, storage: this.storage, + defaultSearchInterceptor: searchService.getDefaultSearchInterceptor(), }); uiActions.registerAction( @@ -157,11 +163,6 @@ export class DataPublicPlugin })) ); - const searchService = this.searchService.setup(core, { - usageCollection, - expressions, - }); - const uiService = this.uiService.setup(core, {}); const ac = this.autocomplete.setup(core); diff --git a/src/plugins/data/public/query/index.tsx b/src/plugins/data/public/query/index.tsx index 44ee2e050ab0..d5dda197917a 100644 --- a/src/plugins/data/public/query/index.tsx +++ b/src/plugins/data/public/query/index.tsx @@ -29,11 +29,10 @@ */ export * from './lib'; - +export * from './types'; export * from './query_service'; export * from './filter_manager'; export * from './query_string'; -export * from './query_string/dataset_manager'; export * from './timefilter'; export * from './saved_query'; export * from './persisted_log'; diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index 3ef86b49c813..95d1bd136f53 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -29,39 +29,27 @@ */ import { share } from 'rxjs/operators'; -import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/public'; import { FilterManager } from './filter_manager'; import { createAddToQueryLog } from './lib'; import { TimefilterService, TimefilterSetup } from './timefilter'; import { createSavedQueryService } from './saved_query/saved_query_service'; import { createQueryStateObservable } from './state_sync/create_global_query_observable'; import { QueryStringManager, QueryStringContract } from './query_string'; -import { - buildOpenSearchQuery, - DataStorage, - getOpenSearchQueryConfig, - IndexPatternsService, -} from '../../common'; +import { buildOpenSearchQuery, getOpenSearchQueryConfig } from '../../common'; import { getUiSettings } from '../services'; import { IndexPattern } from '..'; +import { + IQuerySetup, + IQueryStart, + QueryServiceSetupDependencies, + QueryServiceStartDependencies, +} from './types'; /** * Query Service * @internal */ -interface QueryServiceSetupDependencies { - storage: DataStorage; - uiSettings: IUiSettingsClient; -} - -interface QueryServiceStartDependencies { - savedObjectsClient: SavedObjectsClientContract; - storage: DataStorage; - uiSettings: IUiSettingsClient; - indexPatterns: IndexPatternsService; -} - export class QueryService { filterManager!: FilterManager; timefilter!: TimefilterSetup; @@ -69,7 +57,11 @@ export class QueryService { state$!: ReturnType; - public setup({ storage, uiSettings }: QueryServiceSetupDependencies) { + public setup({ + storage, + uiSettings, + defaultSearchInterceptor, + }: QueryServiceSetupDependencies): IQuerySetup { this.filterManager = new FilterManager(uiSettings); const timefilterService = new TimefilterService(); @@ -78,7 +70,7 @@ export class QueryService { storage, }); - this.queryStringManager = new QueryStringManager(storage, uiSettings); + this.queryStringManager = new QueryStringManager(storage, uiSettings, defaultSearchInterceptor); this.state$ = createQueryStateObservable({ filterManager: this.filterManager, @@ -99,8 +91,8 @@ export class QueryService { storage, uiSettings, indexPatterns, - }: QueryServiceStartDependencies) { - this.queryStringManager.getDatasetManager().init(indexPatterns); + }: QueryServiceStartDependencies): IQueryStart { + this.queryStringManager.getDatasetService().init(); return { addToQueryLog: createAddToQueryLog({ storage, diff --git a/src/plugins/data/public/query/query_string/dataset_manager/dataset_manager.mock.ts b/src/plugins/data/public/query/query_string/dataset_manager/dataset_manager.mock.ts deleted file mode 100644 index 6b677958ec18..000000000000 --- a/src/plugins/data/public/query/query_string/dataset_manager/dataset_manager.mock.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { DatasetContract } from '.'; -import { Dataset, DataStructure, CachedDataStructure } from '../../../../common'; -import { DatasetHandlerConfig } from './types'; - -const createSetupContractMock = () => { - const datasetManagerMock: jest.Mocked = { - init: jest.fn(), - getDataset: jest.fn(), - setDataset: jest.fn(), - getUpdates$: jest.fn(), - getDefaultDataset: jest.fn(), - fetchDefaultDataset: jest.fn(), - initWithIndexPattern: jest.fn(), - registerDatasetHandler: jest.fn(), - fetchOptions: jest.fn(), - getCachedDataStructure: jest.fn(), - clearDataStructureCache: jest.fn(), - }; - return datasetManagerMock; -}; - -export const datasetManagerMock = { - createSetupContract: createSetupContractMock, - createStartContract: createSetupContractMock, -}; - -// Additional mock for DatasetHandlerConfig -export const createDatasetHandlerConfigMock = (): jest.Mocked => ({ - toDataset: jest.fn(), - toDataStructure: jest.fn(), - fetchOptions: jest.fn(), - isLeaf: jest.fn(), -}); - -// Mock for Dataset -export const createDatasetMock = (): jest.Mocked => ({ - id: 'mock-dataset-id', - title: 'Mock Dataset', - type: 'mock-type', - dataSource: { - id: 'mock-datasource-id', - title: 'Mock DataSource', - type: 'mock-datasource-type', - }, -}); - -// Mock for DataStructure -export const createDataStructureMock = (): jest.Mocked => ({ - id: 'mock-datastructure-id', - title: 'Mock DataStructure', - type: 'mock-type', - parent: undefined, - children: [], -}); - -// Mock for CachedDataStructure -export const createCachedDataStructureMock = (): jest.Mocked => ({ - id: 'mock-cached-datastructure-id', - title: 'Mock Cached DataStructure', - type: 'mock-type', - parent: 'mock-parent-id', - children: ['mock-child-id-1', 'mock-child-id-2'], -}); diff --git a/src/plugins/data/public/query/query_string/dataset_manager/dataset_manager.test.ts b/src/plugins/data/public/query/query_string/dataset_manager/dataset_manager.test.ts deleted file mode 100644 index 597f7418af37..000000000000 --- a/src/plugins/data/public/query/query_string/dataset_manager/dataset_manager.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { DatasetManager } from './dataset_manager'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { Dataset } from '../../../../common'; - -describe('DatasetManager', () => { - let service: DatasetManager; - - beforeEach(() => { - const uiSettingsMock = coreMock.createSetup().uiSettings; - uiSettingsMock.get.mockReturnValue(true); - service = new DatasetManager(uiSettingsMock); - }); - - test('getUpdates$ is a cold emits only after dataset changes', () => { - const obs$ = service.getUpdates$(); - const emittedValues: Dataset[] = []; - obs$.subscribe((v) => { - emittedValues.push(v!); - }); - expect(emittedValues).toHaveLength(0); - expect(emittedValues[0]).toEqual(undefined); - - const newDataset: Dataset = { - id: 'test_dataset', - title: 'Test Dataset', - type: 'INDEX_PATTERN', - }; - service.setDataset(newDataset); - expect(emittedValues).toHaveLength(1); - expect(emittedValues[0]).toEqual(newDataset); - - service.setDataset({ ...newDataset }); - expect(emittedValues).toHaveLength(2); - }); -}); diff --git a/src/plugins/data/public/query/query_string/dataset_manager/dataset_manager.ts b/src/plugins/data/public/query/query_string/dataset_manager/dataset_manager.ts deleted file mode 100644 index 217dd3d9f464..000000000000 --- a/src/plugins/data/public/query/query_string/dataset_manager/dataset_manager.ts +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { BehaviorSubject } from 'rxjs'; -import { CoreStart, SavedObjectsClientContract } from 'opensearch-dashboards/public'; -import { skip } from 'rxjs/operators'; -import { - Dataset, - DEFAULT_DATA, - DataStructure, - CachedDataStructure, - DataStructureCache, - toCachedDataStructure, - createDataStructureCache, - DatasetField, - IndexPatternSpec, -} from '../../../../common'; -import { IndexPatternsContract } from '../../../index_patterns'; -import { indexPatternHandlerConfig, indexHandlerConfig } from './lib'; -import { DatasetHandlerConfig } from './types'; - -/** - * Manages datasets and their associated handlers. - */ -export class DatasetManager { - private dataset$: BehaviorSubject; - private indexPatterns?: IndexPatternsContract; - private defaultDataset?: Dataset; - private dataStructureCache: DataStructureCache; - private datasetHandlers: Map; - private dataStructuresMap: Map; - - /** - * Creates an instance of DatasetManager. - * @param uiSettings - The CoreStart's uiSettings. - * @param savedObjects - The SavedObjectsClientContract. - */ - constructor(private readonly uiSettings: CoreStart['uiSettings']) { - this.dataset$ = new BehaviorSubject(undefined); - this.dataStructureCache = createDataStructureCache(); - this.datasetHandlers = new Map(); - this.dataStructuresMap = new Map(); - this.registerDefaultHandlers(); - } - - /** - * Registers default handlers for index patterns and indices. - */ - private registerDefaultHandlers() { - this.registerDatasetHandler(indexPatternHandlerConfig); - this.registerDatasetHandler(indexHandlerConfig); - } - - /** - * Registers a dataset handler. - * @param handler - The dataset handler configuration. - */ - public registerDatasetHandler(handler: DatasetHandlerConfig) { - this.datasetHandlers.set(handler.id, handler); - } - - /** - * Gets all registered dataset handlers. - * @returns An array of dataset handler configurations. - */ - public getDatasetHandlers(): DatasetHandlerConfig[] { - return Array.from(this.datasetHandlers.values()); - } - - /** - * Gets a dataset handler by its ID. - * @param id - The ID of the dataset handler. - * @returns The dataset handler configuration, or undefined if not found. - */ - public getDatasetHandlerById(id: string): DatasetHandlerConfig | undefined { - return this.datasetHandlers.get(id); - } - - /** - * Fetches options for a given data structure. - * @param dataStructure - The data structure to fetch options for. - * @returns A promise that resolves to a DatasetHandlerFetchResponse. - */ - public fetchOptions( - savedObjects: SavedObjectsClientContract, - path: DataStructure[] - ): Promise { - const dataStructure = path[path.length - 1]; - const handler = this.getHandlerForDataStructure(dataStructure); - return handler.fetch(savedObjects, path); - } - - /** - * Checks if a given data structure is a leaf node. - * @param dataStructure - The data structure to check. - * @returns True if the data structure is a leaf node, false otherwise. - */ - public isLeafDataStructure(dataStructure: DataStructure): boolean { - const handler = this.getHandlerForDataStructure(dataStructure); - return handler.id === dataStructure.type; - } - - /** - * Converts a data structure to a dataset. - * @param dataStructure - The data structure to convert. - * @returns A promise that resolves to a BaseDataset. - */ - public toDataset(path: DataStructure[]): Dataset { - const dataStructure = path[path.length - 1]; - const handler = this.getHandlerForDataStructure(dataStructure); - return handler.toDataset(path); - } - - /** - * Gets the appropriate handler for a given data structure. - * @param dataStructure - The data structure to get the handler for. - * @returns The dataset handler configuration. - * @throws Error if no handler is found. - */ - private getHandlerForDataStructure(dataStructure: DataStructure): DatasetHandlerConfig { - const handler = - this.datasetHandlers.get(dataStructure.type) || - this.datasetHandlers.get(DEFAULT_DATA.SET_TYPES.INDEX_PATTERN); - if (!handler) { - throw new Error(`No handler found for type: ${dataStructure.type}`); - } - return handler; - } - - /** - * Gets cached children for a given parent ID. - * @param parentId - The ID of the parent data structure. - * @returns An array of child data structures. - */ - private getCachedChildren(parentId: string): DataStructure[] { - const cachedStructure = this.dataStructureCache.get(parentId); - if (!cachedStructure) return []; - - return cachedStructure.children - .map((childId) => this.dataStructuresMap.get(childId)) - .filter((child): child is DataStructure => !!child); - } - - /** - * Caches an array of data structures. - * @param dataStructures - The data structures to cache. - */ - private cacheDataStructures(dataStructures: DataStructure[]) { - dataStructures.forEach((ds) => { - const fullId = this.getFullId(ds); - this.dataStructuresMap.set(fullId, ds); - const cachedDs = toCachedDataStructure(ds); - this.dataStructureCache.set(fullId, cachedDs); - }); - } - - /** - * Gets the full ID for a data structure, including its parent's ID. - * @param dataStructure - The data structure to get the full ID for. - * @returns The full ID of the data structure. - */ - private getFullId(dataStructure: DataStructure): string { - if (!dataStructure.parent) { - return dataStructure.id; - } - const parentId = this.getFullId(dataStructure.parent); - const separator = - dataStructure.parent.type === DEFAULT_DATA.STRUCTURES.DATA_SOURCE.type ? '::' : '.'; - return `${parentId}${separator}${dataStructure.id}`; - } - - /** - * Sets the current dataset. - * @param dataset - The dataset to set. - */ - public async setDataset(dataset: Dataset | undefined) { - if (dataset) { - const spec = { - ...dataset, - dataSourceRef: dataset.dataSource - ? { - id: dataset.dataSource.id!, - name: dataset.dataSource.title, - type: dataset.dataSource.type, - } - : undefined, - } as IndexPatternSpec; - const temporaryIndexPattern = await this.indexPatterns!.create(spec, true); - this.indexPatterns?.saveToCache(dataset.id, temporaryIndexPattern); - } - this.dataset$.next(dataset); - } - - /** - * Gets the current dataset. - * @returns The current dataset, or undefined if not set. - */ - public getDataset(): Dataset | undefined { - return this.dataset$.getValue(); - } - - /** - * Gets an observable of dataset updates. - * @returns An observable of dataset updates. - */ - public getUpdates$() { - return this.dataset$.asObservable().pipe(skip(1)); - } - - /** - * Gets a cached data structure by ID. - * @param id - The ID of the data structure to retrieve. - * @returns The cached data structure, if found. - */ - public getCachedDataStructure(id: string): CachedDataStructure | undefined { - return this.dataStructureCache.get(id); - } - - /** - * Clears the data structure cache. - * @param id - The ID of the specific data structure to clear. If not provided, clears all. - */ - public clearDataStructureCache(id?: string) { - if (id) { - this.dataStructureCache.clear(id); - this.dataStructuresMap.delete(id); - } else { - this.dataStructureCache.clearAll(); - this.dataStructuresMap.clear(); - } - } - - /** - * Initializes the DatasetManager with index patterns. - * @param indexPatterns - The index patterns contract. - */ - public init = async (indexPatterns: IndexPatternsContract) => { - this.indexPatterns = indexPatterns; - this.defaultDataset = await this.fetchDefaultDataset(); - }; - - /** - * Gets the default dataset. - * @returns The default dataset, or undefined if not set. - */ - public getDefaultDataset = () => { - return this.defaultDataset; - }; - - /** - * Fetches the default dataset. - * @returns A promise that resolves to the default dataset, or undefined if not found. - */ - public fetchDefaultDataset = async (): Promise => { - const defaultIndexPatternId = this.uiSettings.get('defaultIndex'); - if (!defaultIndexPatternId || !this.indexPatterns) { - return undefined; - } - - const indexPattern = await this.indexPatterns.get(defaultIndexPatternId); - if (!indexPattern || !indexPattern.id) { - return undefined; - } - - const handler = this.datasetHandlers.get(DEFAULT_DATA.SET_TYPES.INDEX_PATTERN); - if (handler) { - const dataset = handler.toDataset([ - { - id: indexPattern.id, - title: indexPattern.title, - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - }, - ]); - return { ...dataset, timeFieldName: indexPattern.timeFieldName }; - } - - return undefined; - }; -} - -export type DatasetContract = PublicMethodsOf; diff --git a/src/plugins/data/public/query/query_string/dataset_manager/lib/index_handler.test.ts b/src/plugins/data/public/query/query_string/dataset_manager/lib/index_handler.test.ts deleted file mode 100644 index f5f25ed890c6..000000000000 --- a/src/plugins/data/public/query/query_string/dataset_manager/lib/index_handler.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { indexHandlerConfig, indexToDataStructure, indexToDataset } from './index_handler'; -import { Dataset, DEFAULT_DATA, DataStructure } from '../../../../../common'; -import { IndexPatternsContract } from '../../../../index_patterns'; - -describe('Index Handler', () => { - describe('indexHandlerConfig.toDataset()', () => { - it('should convert a data structure to a dataset for index', () => { - const dataStructure: DataStructure = { - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - parent: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }; - - const result = indexHandlerConfig.toDataset(dataStructure); - - expect(result).toEqual({ - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - dataSource: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }); - }); - - it('should handle data structure without parent', () => { - const dataStructure: DataStructure = { - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - }; - - const result = indexHandlerConfig.toDataset(dataStructure); - - expect(result).toEqual({ - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - dataSource: undefined, - }); - }); - }); - - describe('indexHandlerConfig.toDataStructure()', () => { - it('should convert a dataset to a data structure for index', () => { - const dataset: Dataset = { - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - dataSource: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }; - - const result = indexHandlerConfig.toDataStructure(dataset); - - expect(result).toEqual({ - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - parent: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }); - }); - - it('should handle dataset without data source', () => { - const dataset: Dataset = { - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - }; - - const result = indexHandlerConfig.toDataStructure(dataset); - - expect(result).toEqual({ - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - parent: undefined, - }); - }); - }); - - describe('indexHandlerConfig.fetchOptions()', () => { - it('should fetch options for an index', async () => { - const dataStructure: DataStructure = { - id: 'test-index', - title: 'test-*', - type: DEFAULT_DATA.SET_TYPES.INDEX, - }; - - const mockIndexPatterns: Partial = { - getFieldsForWildcard: jest - .fn() - .mockResolvedValue([{ name: 'test-index-1' }, { name: 'test-index-2' }]), - }; - - const result = await indexHandlerConfig.fetchOptions( - dataStructure, - mockIndexPatterns as IndexPatternsContract - ); - - expect(result).toEqual([ - { - id: 'test-index-1', - title: 'test-index-1', - type: DEFAULT_DATA.SET_TYPES.INDEX, - parent: dataStructure, - }, - { - id: 'test-index-2', - title: 'test-index-2', - type: DEFAULT_DATA.SET_TYPES.INDEX, - parent: dataStructure, - }, - ]); - - expect(mockIndexPatterns.getFieldsForWildcard).toHaveBeenCalledWith({ - pattern: 'test-*', - }); - }); - }); - - describe('indexHandlerConfig.isLeaf()', () => { - it('should return true for index', () => { - const dataStructure: DataStructure = { - id: 'test-index', - title: 'test-*', - type: DEFAULT_DATA.SET_TYPES.INDEX, - }; - expect(indexHandlerConfig.isLeaf(dataStructure)).toBe(true); - }); - }); - - describe('indexToDataStructure()', () => { - it('should convert a dataset to a data structure for index', () => { - const dataset: Dataset = { - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - dataSource: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }; - - const result = indexToDataStructure(dataset); - - expect(result).toEqual({ - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - parent: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }); - }); - - it('should handle dataset without data source', () => { - const dataset: Dataset = { - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - }; - - const result = indexToDataStructure(dataset); - - expect(result).toEqual({ - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - parent: undefined, - }); - }); - }); - - describe('indexToDataset()', () => { - it('should convert a data structure to a dataset for index', () => { - const dataStructure: DataStructure = { - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - parent: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }; - - const result = indexToDataset(dataStructure); - - expect(result).toEqual({ - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - dataSource: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }); - }); - - it('should handle data structure without parent', () => { - const dataStructure: DataStructure = { - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - }; - - const result = indexToDataset(dataStructure); - - expect(result).toEqual({ - id: 'test-index', - title: 'Test Index', - type: DEFAULT_DATA.SET_TYPES.INDEX, - dataSource: undefined, - }); - }); - }); -}); diff --git a/src/plugins/data/public/query/query_string/dataset_manager/lib/index_pattern_handler.test.ts b/src/plugins/data/public/query/query_string/dataset_manager/lib/index_pattern_handler.test.ts deleted file mode 100644 index b60e16463bdd..000000000000 --- a/src/plugins/data/public/query/query_string/dataset_manager/lib/index_pattern_handler.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { indexPatternHandlerConfig, indexPatternToDataStructure } from './index_pattern_handler'; -import { Dataset, DEFAULT_DATA, DataStructure } from '../../../../../common'; -import { IndexPatternsContract } from '../../../../index_patterns'; - -describe('Index Pattern Handler', () => { - describe('indexPatternHandlerConfig.toDataset()', () => { - it('should convert a data structure to a dataset for index pattern', () => { - const dataStructure: DataStructure = { - id: 'test-pattern', - title: 'Test Pattern', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - parent: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }; - - const result = indexPatternHandlerConfig.toDataset(dataStructure); - - expect(result).toEqual({ - id: 'test-pattern', - title: 'Test Pattern', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - dataSource: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }); - }); - - it('should handle data structure without parent', () => { - const dataStructure: DataStructure = { - id: 'test-pattern', - title: 'Test Pattern', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - }; - - const result = indexPatternHandlerConfig.toDataset(dataStructure); - - expect(result).toEqual({ - id: 'test-pattern', - title: 'Test Pattern', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - dataSource: undefined, - }); - }); - }); - - describe('indexPatternHandlerConfig.toDataStructure()', () => { - it('should convert a dataset to a data structure for index pattern', () => { - const dataset: Dataset = { - id: 'test-pattern', - title: 'Test Pattern', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - dataSource: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }; - - const result = indexPatternHandlerConfig.toDataStructure(dataset); - - expect(result).toEqual({ - id: 'test-pattern', - title: 'Test Pattern', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - parent: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }); - }); - - it('should handle dataset without data source', () => { - const dataset: Dataset = { - id: 'test-pattern', - title: 'Test Pattern', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - }; - - const result = indexPatternHandlerConfig.toDataStructure(dataset); - - expect(result).toEqual({ - id: 'test-pattern', - title: 'Test Pattern', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - parent: undefined, - }); - }); - }); - - describe('indexPatternHandlerConfig.fetchOptions()', () => { - it('should fetch options for an index pattern', async () => { - const dataStructure: DataStructure = { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }; - - const mockIndexPatterns: Partial = { - getIdsWithTitle: jest.fn().mockResolvedValue([ - { id: 'pattern-1', title: 'Pattern 1' }, - { id: 'pattern-2', title: 'Pattern 2' }, - ]), - }; - - const result = await indexPatternHandlerConfig.fetchOptions( - dataStructure, - mockIndexPatterns as IndexPatternsContract - ); - - expect(result).toEqual([ - { - id: 'pattern-1', - title: 'Pattern 1', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - parent: dataStructure, - }, - { - id: 'pattern-2', - title: 'Pattern 2', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - parent: dataStructure, - }, - ]); - - expect(mockIndexPatterns.getIdsWithTitle).toHaveBeenCalled(); - }); - }); - - describe('indexPatternHandlerConfig.isLeaf()', () => { - it('should return true for index pattern', () => { - const dataStructure: DataStructure = { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }; - - expect(indexPatternHandlerConfig.isLeaf(dataStructure)).toBe(true); - }); - }); - - describe('indexPatternToDataStructure()', () => { - it('should convert a dataset to a data structure for index pattern', () => { - const dataset: Dataset = { - id: 'test-pattern', - title: 'Test Pattern', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - dataSource: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }; - - const result = indexPatternToDataStructure(dataset); - - expect(result).toEqual({ - id: 'test-pattern', - title: 'Test Pattern', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - parent: { - id: 'test-source', - title: 'Test Source', - type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH, - }, - }); - }); - - it('should handle dataset without data source', () => { - const dataset: Dataset = { - id: 'test-pattern', - title: 'Test Pattern', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - }; - - const result = indexPatternToDataStructure(dataset); - - expect(result).toEqual({ - id: 'test-pattern', - title: 'Test Pattern', - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - parent: undefined, - }); - }); - }); -}); diff --git a/src/plugins/data/public/query/query_string/dataset_manager/lib/index_pattern_handler.ts b/src/plugins/data/public/query/query_string/dataset_manager/lib/index_pattern_handler.ts deleted file mode 100644 index ebcd822070ad..000000000000 --- a/src/plugins/data/public/query/query_string/dataset_manager/lib/index_pattern_handler.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; -import { DEFAULT_DATA, DataStructure, DatasetField, Dataset } from '../../../../../common'; -import { DatasetHandlerConfig } from '../types'; -import { getIndexPatterns } from '../../../../services'; - -export const indexPatternHandlerConfig: DatasetHandlerConfig = { - id: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - title: 'Index Patterns', - meta: { - icon: { type: 'indexPatternApp' }, - tooltip: 'OpenSearch Index Patterns', - }, - - toDataset: (path: DataStructure[]): Dataset => { - const pattern = path[path.length - 1]; - return { - id: pattern.id, - title: pattern.title, - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - dataSource: pattern.parent - ? { - id: pattern.parent.id, - title: pattern.parent.title, - type: pattern.parent.type, - } - : undefined, - }; - }, - - fetch: async ( - savedObjects: SavedObjectsClientContract, - path: DataStructure[] - ): Promise => { - const dataStructure = path[path.length - 1]; - const indexPatterns = await fetchIndexPatterns(savedObjects); - return { - ...dataStructure, - columnHeader: 'Index patterns', - children: indexPatterns, - isLeaf: false, - }; - }, - - fetchFields: async (dataset: Dataset): Promise => { - const indexPattern = await getIndexPatterns().get(dataset.id); - return indexPattern.fields.map((field: any) => ({ - name: field.name, - type: field.type, - })); - }, - - supportedLanguages: async (): Promise => { - return ['SQL', 'PPL', 'KQL', 'Lucene']; - }, -}; - -const fetchIndexPatterns = async (client: SavedObjectsClientContract): Promise => { - const resp = await client.find({ - type: 'index-pattern', - fields: ['title'], - perPage: 10000, - }); - return resp.savedObjects.map((savedObject) => ({ - id: savedObject.id, - title: savedObject.attributes.title, - type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, - isLeaf: true, - })); -}; diff --git a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts new file mode 100644 index 000000000000..7dd10c2e5245 --- /dev/null +++ b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts @@ -0,0 +1,117 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CoreStart, SavedObjectsClientContract } from 'opensearch-dashboards/public'; +import { + Dataset, + DataStructure, + IndexPatternSpec, + DEFAULT_DATA, + IFieldType, +} from '../../../../common'; +import { getIndexPatterns } from '../../../services'; +import { DatasetTypeConfig } from './types'; +import { indexPatternTypeConfig, indexTypeConfig } from './lib'; + +export class DatasetService { + private defaultDataset?: Dataset; + private typesRegistry: Map = new Map(); + + constructor(private readonly uiSettings: CoreStart['uiSettings']) { + this.registerDefaultTypes(); + } + + /** + * Registers default handlers for index patterns and indices. + */ + private registerDefaultTypes() { + this.registerType(indexPatternTypeConfig); + this.registerType(indexTypeConfig); + } + + public async init(): Promise { + this.defaultDataset = await this.fetchDefaultDataset(); + } + + public registerType(handlerConfig: DatasetTypeConfig): void { + this.typesRegistry.set(handlerConfig.id, handlerConfig); + } + + public getType(type: string): DatasetTypeConfig | undefined { + return this.typesRegistry.get(type); + } + + public getTypes(): DatasetTypeConfig[] { + return Array.from(this.typesRegistry.values()); + } + + public getDefault(): Dataset | undefined { + return this.defaultDataset; + } + + public async cacheDataset(dataset: Dataset): Promise { + const type = this.getType(dataset.type); + if (dataset) { + const spec = { + id: dataset.id, + title: dataset.title, + timeFieldName: { + name: dataset.timeFieldName, + type: 'date', + } as Partial, + fields: await type?.fetchFields(dataset), + dataSourceRef: dataset.dataSource + ? { + id: dataset.dataSource.id!, + name: dataset.dataSource.title, + type: dataset.dataSource.type, + } + : undefined, + } as IndexPatternSpec; + const temporaryIndexPattern = await getIndexPatterns().create(spec); + getIndexPatterns().saveToCache(dataset.id, temporaryIndexPattern); + } + } + + public fetchOptions( + savedObjects: SavedObjectsClientContract, + path: DataStructure[], + dataType: string + ): Promise { + const type = this.typesRegistry.get(dataType); + if (!type) { + throw new Error(`No handler found for type: ${path[0]}`); + } + return type.fetch(savedObjects, path); + } + + private async fetchDefaultDataset(): Promise { + const defaultIndexPatternId = this.uiSettings.get('defaultIndex'); + if (!defaultIndexPatternId) { + return undefined; + } + + const indexPattern = await getIndexPatterns().get(defaultIndexPatternId); + if (!indexPattern || !indexPattern.id) { + return undefined; + } + + const dataType = this.typesRegistry.get(DEFAULT_DATA.SET_TYPES.INDEX_PATTERN); + if (dataType) { + const dataset = dataType.toDataset([ + { + id: indexPattern.id, + title: indexPattern.title, + type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, + }, + ]); + return { ...dataset, timeFieldName: indexPattern.timeFieldName }; + } + + return undefined; + } +} + +export type DatasetServiceContract = PublicMethodsOf; diff --git a/src/plugins/data/public/query/query_string/dataset_manager/index.ts b/src/plugins/data/public/query/query_string/dataset_service/index.ts similarity index 58% rename from src/plugins/data/public/query/query_string/dataset_manager/index.ts rename to src/plugins/data/public/query/query_string/dataset_service/index.ts index c80f62eb39cd..8becfe4126ac 100644 --- a/src/plugins/data/public/query/query_string/dataset_manager/index.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/index.ts @@ -4,4 +4,4 @@ */ export * from './types'; -export { DatasetContract, DatasetManager } from './dataset_manager'; +export { DatasetServiceContract, DatasetService } from './dataset_service'; diff --git a/src/plugins/data/public/query/query_string/dataset_manager/lib/index.ts b/src/plugins/data/public/query/query_string/dataset_service/lib/index.ts similarity index 53% rename from src/plugins/data/public/query/query_string/dataset_manager/lib/index.ts rename to src/plugins/data/public/query/query_string/dataset_service/lib/index.ts index 061b77244211..68ba7d2d09b8 100644 --- a/src/plugins/data/public/query/query_string/dataset_manager/lib/index.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './index_handler'; -export * from './index_pattern_handler'; +export * from './index_type'; +export * from './index_pattern_type'; diff --git a/src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.ts b/src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.ts new file mode 100644 index 000000000000..eeb17cb7a146 --- /dev/null +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.ts @@ -0,0 +1,125 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; +import { DataSourceAttributes } from '../../../../../../data_source/common/data_sources'; +import { + DEFAULT_DATA, + DataStructure, + DatasetField, + Dataset, + IIndexPattern, + DATA_STRUCTURE_META_TYPES, +} from '../../../../../common'; +import { DatasetTypeConfig } from '../types'; +import { getIndexPatterns } from '../../../../services'; + +export const indexPatternTypeConfig: DatasetTypeConfig = { + id: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, + title: 'Index Patterns', + meta: { + icon: { type: 'indexPatternApp' }, + tooltip: 'OpenSearch Index Patterns', + }, + + toDataset: (path) => { + const pattern = path[path.length - 1]; + return { + id: pattern.id, + title: pattern.title, + type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, + dataSource: pattern.parent + ? { + id: pattern.parent.id, + title: pattern.parent.title, + type: pattern.parent.type, + } + : undefined, + } as Dataset; + }, + + fetch: async (savedObjects, path) => { + const dataStructure = path[path.length - 1]; + const indexPatterns = await fetchIndexPatterns(savedObjects); + return { + ...dataStructure, + columnHeader: 'Index patterns', + children: indexPatterns, + hasNext: false, + }; + }, + + fetchFields: async (dataset: Dataset): Promise => { + const indexPattern = await getIndexPatterns().get(dataset.id); + return indexPattern.fields.map((field: any) => ({ + name: field.name, + type: field.type, + })); + }, + + supportedLanguages: (dataset): string[] => { + if (dataset.dataSource?.type === 'OpenSearch Serverless') { + return ['DQL', 'Lucene']; + } + return ['DQL', 'Lucene', 'PPL', 'SQL']; + }, +}; + +const fetchIndexPatterns = async (client: SavedObjectsClientContract): Promise => { + const resp = await client.find({ + type: 'index-pattern', + fields: ['title', 'timeFieldName', 'references'], + search: `*`, + searchFields: ['title'], + perPage: 100, + }); + + // Get all unique data source ids + const datasourceIds = Array.from( + new Set( + resp.savedObjects + .filter((savedObject) => savedObject.references.length > 0) + .map((savedObject) => savedObject.references.find((ref) => ref.type === 'data-source')?.id) + .filter(Boolean) + ) + ) as string[]; + + const dataSourceMap: Record = {}; + if (datasourceIds.length > 0) { + const dataSourceResp = await client.bulkGet( + datasourceIds.map((id) => ({ id, type: 'data-source' })) + ); + + dataSourceResp.savedObjects.forEach((savedObject) => { + dataSourceMap[savedObject.id] = savedObject.attributes; + }); + } + + return resp.savedObjects.map( + (savedObject): DataStructure => { + const dataSourceId = savedObject.references.find((ref) => ref.type === 'data-source')?.id; + const dataSource = dataSourceId ? dataSourceMap[dataSourceId] : undefined; + + const indexPatternDataStructure: DataStructure = { + id: savedObject.id, + title: savedObject.attributes.title, + type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, + meta: { + type: DATA_STRUCTURE_META_TYPES.CUSTOM, + timeFieldName: savedObject.attributes.timeFieldName, + }, + }; + + if (dataSource) { + indexPatternDataStructure.parent = { + id: dataSourceId!, // Since we know it exists + title: dataSource.title, + type: dataSource.dataSourceEngineType ?? 'OpenSearch', + }; + } + return indexPatternDataStructure; + } + ); +}; diff --git a/src/plugins/data/public/query/query_string/dataset_manager/lib/index_handler.ts b/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts similarity index 76% rename from src/plugins/data/public/query/query_string/dataset_manager/lib/index_handler.ts rename to src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts index 3b88fed76977..fcc531c56f80 100644 --- a/src/plugins/data/public/query/query_string/dataset_manager/lib/index_handler.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts @@ -5,21 +5,11 @@ import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; import { map } from 'rxjs/operators'; -import { - DEFAULT_DATA, - DataStructure, - DATA_STRUCTURE_META_TYPES, - DataStructureFeatureMeta, - DatasetField, - Dataset, -} from '../../../../../common'; -import { DatasetHandlerConfig } from '../types'; +import { DEFAULT_DATA, DataStructure, Dataset } from '../../../../../common'; +import { DatasetTypeConfig } from '../types'; import { getSearchService, getIndexPatterns } from '../../../../services'; const INDEX_INFO = { - ID: DEFAULT_DATA.SET_TYPES.INDEX, - TITLE: 'Indexes', - ICON: 'logoOpenSearch', LOCAL_DATASOURCE: { id: '', title: 'Local Cluster', @@ -27,13 +17,7 @@ const INDEX_INFO = { }, }; -const meta = { - type: DATA_STRUCTURE_META_TYPES.FEATURE, - icon: INDEX_INFO.ICON, - tooltip: INDEX_INFO.TITLE, -} as DataStructureFeatureMeta; - -export const indexHandlerConfig: DatasetHandlerConfig = { +export const indexTypeConfig: DatasetTypeConfig = { id: DEFAULT_DATA.SET_TYPES.INDEX, title: 'Indexes', meta: { @@ -41,7 +25,7 @@ export const indexHandlerConfig: DatasetHandlerConfig = { tooltip: 'OpenSearch Indexes', }, - toDataset: (path: DataStructure[]): Dataset => { + toDataset: (path) => { const index = path[path.length - 1]; const dataSource = path.find((ds) => ds.type === 'DATA_SOURCE'); @@ -59,22 +43,19 @@ export const indexHandlerConfig: DatasetHandlerConfig = { }; }, - fetch: async ( - savedObjects: SavedObjectsClientContract, - path: DataStructure[] - ): Promise => { + fetch: async (savedObjects, path) => { const dataStructure = path[path.length - 1]; switch (dataStructure.type) { case 'DATA_SOURCE': { const indices = await fetchIndices(dataStructure); return { ...dataStructure, - isLeaf: true, - columnHeader: 'Indices', + hasNext: false, + columnHeader: 'Indexes', children: indices.map((indexName) => ({ id: `${dataStructure.id}::${indexName}`, title: indexName, - type: DEFAULT_DATA.STRUCTURES.INDEX.type, + type: 'INDEX', })), }; } @@ -84,14 +65,14 @@ export const indexHandlerConfig: DatasetHandlerConfig = { return { ...dataStructure, columnHeader: 'Cluster', - isLeaf: false, + hasNext: true, children: dataSources, }; } } }, - fetchFields: async (dataset: Dataset): Promise => { + fetchFields: async (dataset) => { const fields = await getIndexPatterns().getFieldsForWildcard({ pattern: dataset.title, dataSourceId: dataset.dataSource?.id, @@ -102,8 +83,8 @@ export const indexHandlerConfig: DatasetHandlerConfig = { })); }, - supportedLanguages: async (): Promise => { - return ['SQL', 'PPL']; + supportedLanguages: (dataset: Dataset): string[] => { + return ['SQL', 'PPL', 'DQL', 'Lucene']; }, }; diff --git a/src/plugins/data/public/query/query_string/dataset_manager/types.ts b/src/plugins/data/public/query/query_string/dataset_service/types.ts similarity index 90% rename from src/plugins/data/public/query/query_string/dataset_manager/types.ts rename to src/plugins/data/public/query/query_string/dataset_service/types.ts index 5ad7f2935cd1..8d597694ac49 100644 --- a/src/plugins/data/public/query/query_string/dataset_manager/types.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/types.ts @@ -4,12 +4,12 @@ */ import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; import { EuiIconProps } from '@elastic/eui'; -import { BaseDataset, Dataset, DatasetField, DataStructure } from '../../../../common'; +import { Dataset, DatasetField, DataStructure } from '../../../../common'; /** * Configuration for handling dataset operations. */ -export interface DatasetHandlerConfig { +export interface DatasetTypeConfig { /** Unique identifier for the dataset handler */ id: string; /** Human-readable title for the dataset type */ @@ -43,5 +43,5 @@ export interface DatasetHandlerConfig { * Retrieves the supported query languages for this dataset type. * @returns {Promise} A promise that resolves to an array of supported language names. */ - supportedLanguages: () => Promise; + supportedLanguages: (dataset: Dataset) => string[]; } diff --git a/src/plugins/data/public/query/query_string/index.ts b/src/plugins/data/public/query/query_string/index.ts index 84b15ba62ccc..61d25349fbcd 100644 --- a/src/plugins/data/public/query/query_string/index.ts +++ b/src/plugins/data/public/query/query_string/index.ts @@ -29,4 +29,5 @@ */ export { QueryStringContract, QueryStringManager } from './query_string_manager'; -export { DatasetHandlerConfig } from './dataset_manager'; +export { DatasetServiceContract, DatasetService, DatasetTypeConfig } from './dataset_service'; +export { LanguageServiceContract, LanguageService, LanguageConfig } from './language_service'; diff --git a/src/plugins/data/public/query/query_string/language_service/index.ts b/src/plugins/data/public/query/query_string/language_service/index.ts new file mode 100644 index 000000000000..79aea071de3f --- /dev/null +++ b/src/plugins/data/public/query/query_string/language_service/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './types'; +export { LanguageServiceContract, LanguageService } from './language_service'; diff --git a/src/plugins/data/public/query/query_string/language_service/language_service.ts b/src/plugins/data/public/query/query_string/language_service/language_service.ts new file mode 100644 index 000000000000..c4d2d85b8482 --- /dev/null +++ b/src/plugins/data/public/query/query_string/language_service/language_service.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LanguageConfig } from './types'; +import { getDQLLanguageConfig, getLuceneLanguageConfig } from './lib'; +import { ISearchInterceptor } from '../../../search'; + +export class LanguageService { + private languages: Map = new Map(); + + constructor(private readonly defaultSearchInterceptor: ISearchInterceptor) { + this.registerDefaultLanguages(); + } + + /** + * Registers default handlers for index patterns and indices. + */ + private registerDefaultLanguages() { + this.registerLanguage(getDQLLanguageConfig(this.defaultSearchInterceptor)); + this.registerLanguage(getLuceneLanguageConfig(this.defaultSearchInterceptor)); + } + + public registerLanguage(config: LanguageConfig): void { + this.languages.set(config.id, config); + } + + public getLanguage(language: string): LanguageConfig | undefined { + return this.languages.get(language); + } + + public getLanguages(): LanguageConfig[] { + return Array.from(this.languages.values()); + } + + public getDefaultLanguage(): LanguageConfig { + return this.languages.get('kuery') || this.languages.values().next().value; + } +} + +export type LanguageServiceContract = PublicMethodsOf; diff --git a/src/plugins/data/public/query/query_string/language_service/lib/dql_language.ts b/src/plugins/data/public/query/query_string/language_service/lib/dql_language.ts new file mode 100644 index 000000000000..865c6aae84f5 --- /dev/null +++ b/src/plugins/data/public/query/query_string/language_service/lib/dql_language.ts @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LanguageConfig } from '../types'; +import { ISearchInterceptor } from '../../../../search'; + +export const getDQLLanguageConfig = (search: ISearchInterceptor): LanguageConfig => { + return { + id: 'kuery', + title: 'DQL', + search, + getQueryString(_) { + return ''; + }, + searchBar: { + showQueryInput: true, + showFilterBar: true, + showDatePicker: true, + showAutoRefreshOnly: false, + }, + fields: { + filterable: true, + visualizable: true, + }, + showDocLinks: true, + supportedAppNames: ['discover'], + }; +}; diff --git a/src/plugins/data/public/query/query_string/language_service/lib/index.ts b/src/plugins/data/public/query/query_string/language_service/lib/index.ts new file mode 100644 index 000000000000..42b8f7f2fb18 --- /dev/null +++ b/src/plugins/data/public/query/query_string/language_service/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './dql_language'; +export * from './lucene_language'; diff --git a/src/plugins/data/public/query/query_string/language_service/lib/lucene_language.ts b/src/plugins/data/public/query/query_string/language_service/lib/lucene_language.ts new file mode 100644 index 000000000000..3b9643e92739 --- /dev/null +++ b/src/plugins/data/public/query/query_string/language_service/lib/lucene_language.ts @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LanguageConfig } from '../types'; +import { ISearchInterceptor } from '../../../../search'; + +export const getLuceneLanguageConfig = (search: ISearchInterceptor): LanguageConfig => { + return { + id: 'lucene', + title: 'Lucene', + search, + getQueryString(_) { + return ''; + }, + searchBar: { + showQueryInput: true, + showFilterBar: true, + showDatePicker: true, + showAutoRefreshOnly: false, + }, + fields: { + filterable: true, + visualizable: true, + }, + showDocLinks: true, + supportedAppNames: ['discover'], + }; +}; diff --git a/src/plugins/data/public/query/query_string/language_service/types.ts b/src/plugins/data/public/query/query_string/language_service/types.ts new file mode 100644 index 000000000000..53bf397da3fa --- /dev/null +++ b/src/plugins/data/public/query/query_string/language_service/types.ts @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ISearchInterceptor } from '../../../search'; +import { Query } from '../../../../public'; + +export interface LanguageConfig { + id: string; + title: string; + search: ISearchInterceptor; + getQueryString: (query: Query) => string; + searchBar?: { + showQueryInput?: boolean; + showFilterBar?: boolean; + showDatePicker?: boolean; + showAutoRefreshOnly?: boolean; + dateRange?: { + initialFrom?: string; + initialTo?: string; + }; + }; + fields?: { + filterable?: boolean; + visualizable?: boolean; + }; + showDocLinks?: boolean; + supportedAppNames: string[]; +} diff --git a/src/plugins/data/public/query/query_string/query_history.ts b/src/plugins/data/public/query/query_string/query_history.ts index 17e5f2cd1494..80dfa4b5d560 100644 --- a/src/plugins/data/public/query/query_string/query_history.ts +++ b/src/plugins/data/public/query/query_string/query_history.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DataStorage, SimpleDataSet } from 'src/plugins/data/common'; import { BehaviorSubject } from 'rxjs'; +import { DataStorage, Dataset } from '../../../common'; import { Query, TimeRange } from '../..'; // Todo: Implement a more advanced QueryHistory class when needed for recent query history @@ -35,7 +35,7 @@ export class QueryHistory { return () => subscription.unsubscribe(); } - addQueryToHistory(dataSet: SimpleDataSet, query: Query, dateRange?: TimeRange) { + addQueryToHistory(dataset: Dataset, query: Query, dateRange?: TimeRange) { const keys = this.getHistoryKeys(); keys.splice(0, 500); // only maintain most recent X; keys.forEach((key) => { @@ -45,7 +45,7 @@ export class QueryHistory { const timestamp = new Date().getTime(); const k = 'query_' + timestamp; this.storage.set(k, { - dataSet, + dataset, time: timestamp, query, dateRange, diff --git a/src/plugins/data/public/query/query_string/query_string_manager.mock.ts b/src/plugins/data/public/query/query_string/query_string_manager.mock.ts index 0142b5037dd2..4d2f389a7f71 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.mock.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.mock.ts @@ -29,7 +29,6 @@ */ import { QueryStringContract } from '.'; -// import { datasetManagerMock } from './dataset_manager/dataset_manager.mock'; const createSetupContractMock = () => { const queryStringManagerMock: jest.Mocked = { @@ -39,8 +38,15 @@ const createSetupContractMock = () => { getDefaultQuery: jest.fn(), formatQuery: jest.fn(), clearQuery: jest.fn(), - // getDatasetManager: () => datasetManagerMock.createSetupContract(), - getDatasetManager: jest.fn(), + addToQueryHistory: jest.fn(), + getQueryHistory: jest.fn(), + clearQueryHistory: jest.fn(), + changeQueryHistory: jest.fn(), + getInitialQuery: jest.fn(), + getInitialQueryByLanguage: jest.fn(), + getDatasetService: jest.fn(), + getLanguageService: jest.fn(), + getInitialQueryByDataset: jest.fn(), }; return queryStringManagerMock; }; diff --git a/src/plugins/data/public/query/query_string/query_string_manager.ts b/src/plugins/data/public/query/query_string/query_string_manager.ts index 68b1ce8b540c..38bab9a8132c 100644 --- a/src/plugins/data/public/query/query_string/query_string_manager.ts +++ b/src/plugins/data/public/query/query_string/query_string_manager.ts @@ -33,42 +33,39 @@ import { skip } from 'rxjs/operators'; import { CoreStart } from 'opensearch-dashboards/public'; import { Dataset, DataStorage, Query, TimeRange, UI_SETTINGS } from '../../../common'; import { createHistory, QueryHistory } from './query_history'; -import { DatasetContract, DatasetManager } from './dataset_manager'; +import { DatasetService, DatasetServiceContract } from './dataset_service'; +import { LanguageService, LanguageServiceContract } from './language_service'; +import { ISearchInterceptor } from '../../search'; export class QueryStringManager { private query$: BehaviorSubject; private queryHistory: QueryHistory; - private datasetManager!: DatasetContract; + private datasetService!: DatasetServiceContract; + private languageService!: LanguageServiceContract; constructor( private readonly storage: DataStorage, - private readonly uiSettings: CoreStart['uiSettings'] + private readonly uiSettings: CoreStart['uiSettings'], + private readonly defaultSearchInterceptor: ISearchInterceptor ) { this.query$ = new BehaviorSubject(this.getDefaultQuery()); this.queryHistory = createHistory({ storage }); - this.datasetManager = new DatasetManager(uiSettings); + this.datasetService = new DatasetService(uiSettings); + this.languageService = new LanguageService(this.defaultSearchInterceptor); } private getDefaultQueryString() { return this.storage.get('userQueryString') || ''; } - private getDefaultLanguage() { - return ( - this.storage.get('userQueryLanguage') || - this.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) - ); - } - public getDefaultQuery() { return { query: this.getDefaultQueryString(), language: this.getDefaultLanguage(), - // ...(this.uiSettings && - // this.uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED) && { - // dataset: this.getDatasetManager().getDefaultDataset(), - // }), - // }; + ...(this.uiSettings && + this.uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED) && { + dataset: this.datasetService?.getDefault(), + }), }; } @@ -79,6 +76,7 @@ export class QueryStringManager { return { query, language: this.getDefaultLanguage(), + dataset: this.datasetService?.getDefault(), }; } else { return query; @@ -97,11 +95,10 @@ export class QueryStringManager { * Updates the query. * @param {Query} query */ - public setQuery = (query: Query) => { + public setQuery = (query: Partial) => { const curQuery = this.query$.getValue(); - if (query?.language !== curQuery.language || query?.query !== curQuery.query) { - this.query$.next(query); - } + const newQuery = { ...curQuery, ...query }; + this.query$.next(newQuery); }; /** @@ -129,12 +126,51 @@ export class QueryStringManager { public changeQueryHistory(listener: (reqs: any[]) => void) { return this.queryHistory.change(listener); } - /** - * TODO: verify if we want to just access the dataset manager directly or access each function - */ - public getDatasetManager = () => { - return this.datasetManager; + + public getDatasetService = () => { + return this.datasetService; + }; + + public getLanguageService = () => { + return this.languageService; + }; + + public getInitialQuery = () => { + return this.getInitialQueryByLanguage(this.query$.getValue().language); + }; + + public getInitialQueryByLanguage = (languageId: string) => { + const curQuery = this.query$.getValue(); + const language = this.languageService.getLanguage(languageId); + const dataset = curQuery.dataset; + const input = language?.getQueryString(curQuery) || ''; + + return { + query: input, + language: languageId, + dataset, + }; }; + + public getInitialQueryByDataset = (newDataset: Dataset) => { + const curQuery = this.query$.getValue(); + const languageId = curQuery.language; + const language = this.languageService.getLanguage(languageId); + const newQuery = { ...curQuery, dataset: newDataset }; + const input = language?.getQueryString(newQuery) || ''; + + return { + ...newQuery, + query: input, + }; + }; + + private getDefaultLanguage() { + return ( + this.storage.get('userQueryLanguage') || + this.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) + ); + } } export type QueryStringContract = PublicMethodsOf; diff --git a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts index 04f52daad90b..8abcb3ece18d 100644 --- a/src/plugins/data/public/query/state_sync/create_global_query_observable.ts +++ b/src/plugins/data/public/query/state_sync/create_global_query_observable.ts @@ -52,7 +52,6 @@ export function createQueryStateObservable({ refreshInterval: timefilter.getRefreshInterval(), filters: filterManager.getFilters(), query: queryString.getQuery(), - dataset: queryString.getDatasetManager().getDataset(), }); let currentChange: QueryStateChange = {}; @@ -61,13 +60,6 @@ export function createQueryStateObservable({ currentChange.query = true; state.set({ ...state.get(), query: queryString.getQuery() }); }), - queryString - .getDatasetManager() - .getUpdates$() - .subscribe(() => { - currentChange.dataset = true; - state.set({ ...state.get(), dataset: queryString.getDatasetManager().getDataset() }); - }), timefilter.getTimeUpdate$().subscribe(() => { currentChange.time = true; state.set({ ...state.get(), time: timefilter.getTime() }); diff --git a/src/plugins/data/public/query/types.ts b/src/plugins/data/public/query/types.ts new file mode 100644 index 000000000000..ee359f0939a2 --- /dev/null +++ b/src/plugins/data/public/query/types.ts @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IUiSettingsClient, SavedObjectsClientContract } from 'opensearch-dashboards/public'; +import { Observable } from 'rxjs'; +import { DataStorage } from '../../common'; +import { IndexPattern, IndexPatternsService } from '../index_patterns'; +import { ISearchInterceptor } from '../search'; +import { FilterManager } from './filter_manager'; +import { QueryStringContract } from './query_string'; +import { QueryStateChange, QueryState } from './state_sync'; +import { createAddToQueryLog } from './lib'; +import { createSavedQueryService } from './saved_query'; +import { TimefilterSetup } from './timefilter'; + +export interface IQuerySetup { + filterManager: FilterManager; + timefilter: TimefilterSetup; + queryString: QueryStringContract; + state$: Observable<{ changes: QueryStateChange; state: QueryState }>; +} + +export interface IQueryStart { + addToQueryLog: ReturnType; + filterManager: FilterManager; + queryString: QueryStringContract; + savedQueries: ReturnType; + state$: Observable<{ changes: QueryStateChange; state: QueryState }>; + timefilter: TimefilterSetup; + getOpenSearchQuery: (indexPattern: IndexPattern) => any; +} + +/** @internal */ +export interface QueryServiceSetupDependencies { + storage: DataStorage; + uiSettings: IUiSettingsClient; + defaultSearchInterceptor: ISearchInterceptor; +} + +/** @internal */ +export interface QueryServiceStartDependencies { + savedObjectsClient: SavedObjectsClientContract; + storage: DataStorage; + uiSettings: IUiSettingsClient; + indexPatterns: IndexPatternsService; +} diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index bfd0bc1ed780..f5ff39c40c4f 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -56,5 +56,5 @@ export { export { getOpenSearchPreference } from './opensearch_search'; -export { SearchInterceptor, SearchInterceptorDeps } from './search_interceptor'; +export { SearchInterceptor, SearchInterceptorDeps, ISearchInterceptor } from './search_interceptor'; export * from './errors'; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 5516b4abc79d..297cb3342c5c 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -62,7 +62,6 @@ import { IDataFrame, IDataFrameResponse, createDataFrameCache, - dataFrameToSpec, } from '../../common/data_frames'; import { getQueryService, getUiService } from '../services'; import { UI_SETTINGS } from '../../common'; @@ -127,6 +126,7 @@ export class SearchService implements Plugin { __enhance: (enhancements: SearchEnhancements) => { this.searchInterceptor = enhancements.searchInterceptor; }, + getDefaultSearchInterceptor: () => this.defaultSearchInterceptor, }; } @@ -135,19 +135,19 @@ export class SearchService implements Plugin { { fieldFormats, indexPatterns }: SearchServiceStartDependencies ): ISearchStart { const search = ((request, options) => { - const selectedLanguage = getQueryService().queryString.getQuery().language; - const uiService = getUiService(); - const enhancement = uiService.Settings.getQueryEnhancements(selectedLanguage); - uiService.Settings.setUiOverridesByUserQueryLanguage(selectedLanguage); + const queryStringManager = getQueryService().queryString; + const language = queryStringManager.getQuery().language; + const languageConfig = queryStringManager.getLanguageService().getLanguage(language); + getUiService().Settings.setUiOverridesByUserQueryLanguage(language); const isEnhancedEnabled = uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED); - if (enhancement) { + if (languageConfig) { if (!isEnhancedEnabled) { notifications.toasts.addWarning( - `Query enhancements are disabled. Please enable to use: ${selectedLanguage}.` + `Query enhancements are disabled. Please enable to use: ${languageConfig.id}.` ); } - return enhancement.search.search(request, options); + return languageConfig.search.search(request, options); } return this.defaultSearchInterceptor.search(request, options); }) as ISearchGeneric; @@ -158,28 +158,7 @@ export class SearchService implements Plugin { const dfService: DataFrameService = { get: () => this.dfCache.get(), set: async (dataFrame: IDataFrame) => { - if (this.dfCache.get() && this.dfCache.get()?.name !== dataFrame.name) { - indexPatterns.clearCache(this.dfCache.get()!.name, false); - } - - if ( - dataFrame.meta && - dataFrame.meta.queryConfig && - 'dataSource' in dataFrame.meta.queryConfig - ) { - const dataSource = await indexPatterns.findDataSourceByTitle( - dataFrame.meta.queryConfig.dataSource - ); - dataFrame.meta.queryConfig.dataSourceId = dataSource?.id; - } this.dfCache.set(dataFrame); - const dataSetName = `${dataFrame.meta?.queryConfig?.dataSourceId ?? ''}.${dataFrame.name}`; - const existingIndexPattern = await indexPatterns.get(dataSetName, true); - const dataSet = await indexPatterns.create( - dataFrameToSpec(dataFrame, existingIndexPattern?.id ?? dataSetName), - !existingIndexPattern?.id - ); - indexPatterns.saveToCache(dataSetName, dataSet); }, clear: () => { if (this.dfCache.get() === undefined) return; diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 29dc37b41c91..d34271ef7568 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -54,6 +54,7 @@ export interface ISearchSetup { * @internal */ __enhance: (enhancements: SearchEnhancements) => void; + getDefaultSearchInterceptor: () => ISearchInterceptor; } /** diff --git a/src/plugins/data/public/ui/dataset_selector/__mocks__/utils.ts b/src/plugins/data/public/ui/dataset_selector/__mocks__/utils.ts deleted file mode 100644 index 2dfdfe235cef..000000000000 --- a/src/plugins/data/public/ui/dataset_selector/__mocks__/utils.ts +++ /dev/null @@ -1,349 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - Dataset, - DatasetManager, - DatasetTypeConfig, - DataSource, - DataStructure, -} from '../../../../common/data_sets'; - -interface DatasetVariations { - dataSourceTypes: string[]; - dataSetTypes: string[]; - timeFields: string[]; -} - -export function generateRandomDatasets(count: number, variations: DatasetVariations): Dataset[] { - const datasets: Dataset[] = []; - - for (let i = 0; i < count; i++) { - const dataSource: DataSource = { - id: `ds-${Math.random().toString(36).substr(2, 9)}`, - title: `Data Source ${i + 1}`, - type: - variations.dataSourceTypes[Math.floor(Math.random() * variations.dataSourceTypes.length)], - }; - - const dataset: Dataset = { - id: `dataset-${Math.random().toString(36).substr(2, 9)}`, - title: `Dataset ${i + 1}`, - type: variations.dataSetTypes[Math.floor(Math.random() * variations.dataSetTypes.length)], - timeFieldName: - variations.timeFields[Math.floor(Math.random() * variations.timeFields.length)], - dataSource, - }; - - datasets.push(dataset); - } - - return datasets; -} - -class MockDatasetManager implements DatasetManager { - private types: { - [key: string]: DatasetTypeConfig; - } = {}; - - private dataset?: Dataset; - private recentDatasets: Dataset[] = []; - - constructor() { - this.registerType({ - id: 'index-pattern', - title: 'Index Pattern', - getOptions: this.mockGetIndexPatternOptions, - getDataset: this.mockGetIndexPatternDataset, - getFields: this.mockGetIndexPatternFields, - config: { - supportedLanguages: ['DQL', 'lucene'], - icon: { - type: 'indexPatternApp', - }, - }, - }); - - this.registerType({ - id: 'indices', - title: 'Indices', - getOptions: this.mockGetIndicesOptions, - getDataset: this.mockGetIndicesDataset, - getFields: this.mockGetIndicesFields, - config: { - supportedLanguages: ['DQL', 'lucene'], - icon: { - type: 'logoOpenSearch', - }, - hasTimeField: true, - }, - }); - - this.registerType({ - id: 's3', - title: 'S3', - getOptions: this.mockGetS3Options, - getDataset: this.mockGetS3Dataset, - getFields: this.mockGetS3Fields, - config: { - supportedLanguages: ['SQL'], - icon: { - type: 'logoAWS', - }, - hasTimeField: false, - }, - }); - } - - registerType = (props: DatasetTypeConfig) => { - this.types[props.id] = props; - }; - - getTypes = () => { - return Object.values(this.types); - }; - - getType = (type: string) => { - return this.types[type]; - }; - - getDataSet = () => { - return this.dataset; - }; - - setDataSet = (dataset: Dataset) => { - this.dataset = dataset; - // Store the top 10 recently used - this.recentDatasets = [dataset, ...this.recentDatasets].slice(0, 10); - }; - - getRecentlyUsed = () => { - return this.recentDatasets; - }; - - private mockGetIndexPatternOptions = async (path?: DataStructure[]) => { - const lastOption = path?.[path.length - 1]; - - if (lastOption?.id !== 'index-pattern') { - return { - options: [], - columnHeader: 'Index patterns', - }; - } - const mockData: DataStructure[] = [ - { id: 'pattern1', title: 'logs-*', type: 'index-pattern' }, - { id: 'pattern2', title: 'metrics-*', type: 'index-pattern' }, - { id: 'pattern3', title: 'events-*', type: 'index-pattern' }, - ]; - - return { - options: mockData, - columnHeader: 'Index patterns', - }; - }; - - private mockGetIndexPatternDataset = (path: DataStructure[]) => { - const lastOption = path?.[path.length - 1]; - - return { - id: lastOption?.id, - title: lastOption?.title, - type: 'index-pattern', - timeFieldName: 'timestamp', - dataSource: { - id: 'main-cluster', - title: 'Main OpenSearch Cluster', - type: 'OPENSEARCH', - }, - }; - }; - - private mockGetIndexPatternFields = async (dataset: Dataset) => { - return [ - { - name: 'timestamp', - type: 'date', - }, - { - name: 'bytes', - type: 'number', - }, - { - name: 'cpu', - type: 'number', - }, - { - name: 'memory', - type: 'number', - }, - { - name: 'log', - type: 'string', - }, - ]; - }; - - private mockGetIndicesOptions = async (path?: DataStructure[]) => { - const option = path?.[path.length - 1]; - if (option?.type === 'indices') { - // First level: List of clusters - const mockClusters: DataStructure[] = [ - { id: 'cluster1', title: 'Production Cluster', type: 'cluster' }, - { id: 'cluster2', title: 'Development Cluster', type: 'cluster' }, - { id: 'cluster3', title: 'Testing Cluster', type: 'cluster' }, - ]; - - return { - options: mockClusters, - isLoadable: true, - columnHeader: 'Clusters', - }; - } else if (option?.type === 'cluster') { - // Second level: List of indices within the selected cluster - const mockIndices: DataStructure[] = [ - { id: `${option.id}-index1`, title: `${option.title}-logs-2023.05`, type: 'index' }, - { id: `${option.id}-index2`, title: `${option.title}-metrics-2023.05`, type: 'index' }, - { id: `${option.id}-index3`, title: `${option.title}-events-2023.05`, type: 'index' }, - ]; - - return { - options: mockIndices, - columnHeader: 'Indices', - }; - } - - // If we reach here, it's an unexpected state - return { - options: [], - columnHeader: 'Indices', - }; - }; - - private mockGetIndicesDataset = (path: DataStructure[]) => { - const option = path?.[path.length - 1]; - - return { - id: option?.id, - title: option?.title, - type: 'indices', - timeFieldName: 'timestamp', - dataSource: { - id: 'main-cluster', - title: 'Main OpenSearch Cluster', - type: 'OPENSEARCH', - }, - }; - }; - - private mockGetIndicesFields = async (dataset: Dataset) => { - return [ - { - name: 'timestamp', - type: 'date', - }, - { - name: 'bytes', - type: 'number', - }, - { - name: 'cpu', - type: 'number', - }, - { - name: 'memory', - type: 'number', - }, - { - name: 'log', - type: 'string', - }, - ]; - }; - - private mockGetS3Options = async (path?: DataStructure[]) => { - const lastOption = path?.[path.length - 1]; - - if (!lastOption || lastOption.type === 's3') { - // First level: Connections (instantaneous) - const mockConnections: DataStructure[] = [ - { id: 'conn1', title: 'Production S3', type: 'connection' }, - { id: 'conn2', title: 'Development S3', type: 'connection' }, - { id: 'conn3', title: 'Testing S3', type: 'connection' }, - ]; - - return { - options: mockConnections, - columnHeader: 'S3 Connections', - isLoadable: true, - }; - } else if (lastOption.type === 'connection') { - // Second level: Databases (10s delay) - await new Promise((resolve) => setTimeout(resolve, 3000)); - - const mockDatabases: DataStructure[] = [ - { id: `${lastOption.id}-db1`, title: 'Customer Data', type: 'database' }, - { id: `${lastOption.id}-db2`, title: 'Product Catalog', type: 'database' }, - { id: `${lastOption.id}-db3`, title: 'Sales Records', type: 'database' }, - ]; - - return { - options: mockDatabases, - columnHeader: 'S3 Databases', - isLoadable: true, - }; - } else if (lastOption.type === 'database') { - // Third level: Tables (10s delay) - await new Promise((resolve) => setTimeout(resolve, 3000)); - - const mockTables: DataStructure[] = [ - { id: `${lastOption.id}-table1`, title: 'Users', type: 'table' }, - { id: `${lastOption.id}-table2`, title: 'Orders', type: 'table' }, - { id: `${lastOption.id}-table3`, title: 'Products', type: 'table' }, - ]; - - return { - options: mockTables, - columnHeader: 'S3 Tables', - isLoadable: false, - }; - } - - return { - options: [], - columnHeader: 'S3', - }; - }; - - private mockGetS3Dataset = (path: DataStructure[]) => { - const lastOption = path[path.length - 1]; - - return { - id: lastOption.id, - title: lastOption.title, - type: 's3', - dataSource: { - id: path[1].id, // Connection ID - title: path[1].title, // Connection Title - type: 'S3', - }, - }; - }; - - private mockGetS3Fields = async (dataset: Dataset) => { - // Simulate a delay for field fetching - await new Promise((resolve) => setTimeout(resolve, 5000)); - - return [ - { name: 'id', type: 'string' }, - { name: 'name', type: 'string' }, - { name: 'created_at', type: 'date' }, - { name: 'updated_at', type: 'date' }, - { name: 'value', type: 'number' }, - ]; - }; -} - -// Export an instance of the mock manager -export const mockDatasetManager = new MockDatasetManager(); diff --git a/src/plugins/data/public/ui/dataset_selector/_dataset_explorer.scss b/src/plugins/data/public/ui/dataset_selector/_dataset_explorer.scss index b75f0f8b9ff2..0011db04fa29 100644 --- a/src/plugins/data/public/ui/dataset_selector/_dataset_explorer.scss +++ b/src/plugins/data/public/ui/dataset_selector/_dataset_explorer.scss @@ -26,7 +26,3 @@ border-bottom: $euiBorderThin; } } - -.datasetSelector__selectable { - padding: $euiSizeXS; -} diff --git a/src/plugins/data/public/ui/dataset_selector/_dataset_selector.scss b/src/plugins/data/public/ui/dataset_selector/_dataset_selector.scss new file mode 100644 index 000000000000..9ec6ca531cf8 --- /dev/null +++ b/src/plugins/data/public/ui/dataset_selector/_dataset_selector.scss @@ -0,0 +1,14 @@ +.datasetSelector { + &__icon { + margin-right: 4px; + } + + &__advancedButton { + width: 100%; + } + + &__selectable { + width: 365px; + padding: $euiSizeXS; + } +} diff --git a/src/plugins/data/public/ui/dataset_selector/_index.scss b/src/plugins/data/public/ui/dataset_selector/_index.scss index 8057fc32b961..b1d4dbe34c68 100644 --- a/src/plugins/data/public/ui/dataset_selector/_index.scss +++ b/src/plugins/data/public/ui/dataset_selector/_index.scss @@ -1 +1,2 @@ @import "./dataset_explorer"; +@import "./dataset_selector"; diff --git a/src/plugins/data/public/ui/dataset_selector/advanced_selector.tsx b/src/plugins/data/public/ui/dataset_selector/advanced_selector.tsx index c50efc51868b..a319c9a376bd 100644 --- a/src/plugins/data/public/ui/dataset_selector/advanced_selector.tsx +++ b/src/plugins/data/public/ui/dataset_selector/advanced_selector.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; import { BaseDataset, @@ -25,26 +25,27 @@ export const AdvancedSelector = ({ onSelect: (dataset: Dataset) => void; onCancel: () => void; }) => { - const queryService = getQueryService(); - const datasetManager = queryService.queryString.getDatasetManager(); + const queryString = getQueryService().queryString; const [path, setPath] = useState([ { ...DEFAULT_DATA.STRUCTURES.ROOT, - columnHeader: 'Select Data', - isLeaf: false, - children: datasetManager.getDatasetHandlers().map( - (handler) => - ({ - id: handler.id, - title: handler.title, - type: handler.id, + columnHeader: 'Select data', + hasNext: true, + children: queryString + .getDatasetService() + .getTypes() + .map((type) => { + return { + id: type!.id, + title: type!.title, + type: type!.id, meta: { - ...handler.meta, + ...type!.meta, type: DATA_STRUCTURE_META_TYPES.TYPE, }, - } as DataStructure) - ), + } as DataStructure; + }), }, ]); const [selectedDataset, setSelectedDataset] = useState(); @@ -59,7 +60,7 @@ export const AdvancedSelector = ({ ) : ( setSelectedDataset(dataset)} diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.tsx index d4651179429d..32e48691aa87 100644 --- a/src/plugins/data/public/ui/dataset_selector/configurator.tsx +++ b/src/plugins/data/public/ui/dataset_selector/configurator.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; -import { BaseDataset, Dataset, DatasetField, DataStructure } from '../../../common'; +import { BaseDataset, Dataset, DatasetField } from '../../../common'; import { getQueryService, getIndexPatterns } from '../../services'; export const Configurator = ({ @@ -34,33 +34,29 @@ export const Configurator = ({ onPrevious: () => void; }) => { const queryService = getQueryService(); - const datasetManager = queryService.queryString.getDatasetManager(); - // const languageManager = queryService.queryString.getLanguageManager(); + const queryString = queryService.queryString; const indexPatternsService = getIndexPatterns(); + const type = queryString.getDatasetService().getType(baseDataset.type); + const languages = type?.supportedLanguages(baseDataset) || []; const [dataset, setDataset] = useState(baseDataset); - const [fields, setFields] = useState(); const [timeFields, setTimeFields] = useState(); const [timeField, setTimeField] = useState(dataset.timeFieldName); - // const [selectedLanguage, setSelectedLanguage] = useState( - // languageManager.getDefaultLanguage(dataset.type) - // ); + const [language, setLanguage] = useState(languages[0]); useEffect(() => { const fetchFields = async () => { - const datasetFields = await datasetManager - .getDatasetHandlerById(baseDataset.type) + const datasetFields = await queryString + .getDatasetService() + .getType(baseDataset.type) ?.fetchFields(baseDataset); - setFields(datasetFields); const dateFields = datasetFields?.filter((field) => field.type === 'date'); setTimeFields(dateFields || []); }; fetchFields(); - }, [baseDataset, datasetManager, indexPatternsService]); - - // const supportedLanguages = languageManager.getSupportedLanguages(dataset.type); + }, [baseDataset, indexPatternsService, queryString]); return ( <> @@ -121,7 +117,7 @@ export const Configurator = ({ /> )} - {/* ({ - text: language, - value: language, + options={languages.map((languageId) => ({ + text: languageId, + value: languageId, }))} - value={selectedLanguage} - onChange={(e) => setSelectedLanguage(e.target.value)} + value={language} + onChange={(e) => setLanguage(e.target.value)} /> - */} + @@ -153,7 +149,7 @@ export const Configurator = ({ defaultMessage="Previous" /> - onConfirm({ ...dataset, fields })} fill> + onConfirm(dataset)} fill> void; onNext: (dataset: BaseDataset) => void; @@ -51,23 +45,23 @@ export const DatasetExplorer = ({ const lastPathItem = newPath[newPath.length - 1]; const nextPath = [...newPath, item]; - const handler = datasetManager.getDatasetHandlerById(nextPath[1].id); - if (!handler) return; + const typeConfig = queryString.getDatasetService().getType(nextPath[1].id); + if (!typeConfig) return; - if (lastPathItem.isLeaf) { - const dataset = handler!.toDataset(nextPath); + if (!lastPathItem.hasNext) { + const dataset = typeConfig!.toDataset(nextPath); setExplorerDataset(dataset as BaseDataset); return; } setLoading(true); - const nextDataStructure = await handler.fetch(savedObjects, nextPath); + const nextDataStructure = await typeConfig.fetch(savedObjects, nextPath); setLoading(false); setPath([...newPath, nextDataStructure]); }; - const columnCount = !path[path.length - 1]?.isLeaf ? path.length + 1 : path.length; + const columnCount = path[path.length - 1]?.hasNext ? path.length + 1 : path.length; return ( <> @@ -103,7 +97,7 @@ export const DatasetExplorer = ({ > {path.map((current, index) => { const isLast = index === path.length - 1; - const isFinal = isLast && current.isLeaf; + const isFinal = isLast && !current.hasNext; return (
({ - label: child.title, + label: child.parent ? `${child.parent.title}::${child.title}` : child.title, value: child.id, prepend: child.meta?.type === DATA_STRUCTURE_META_TYPES.TYPE && child.meta?.icon && , @@ -139,7 +133,7 @@ export const DatasetExplorer = ({ }, searchable: true, })} - className="datasetSelector__selectable" + className="datasetExplorer__selectable" > {(list, search) => ( <> @@ -151,7 +145,7 @@ export const DatasetExplorer = ({
); })} - {!!!path[path.length - 1]?.isLeaf && } + {!!path[path.length - 1]?.hasNext && } diff --git a/src/plugins/data/public/ui/dataset_selector/dataset_selector.tsx b/src/plugins/data/public/ui/dataset_selector/dataset_selector.tsx index c04c8c9c06e3..01441f736c79 100644 --- a/src/plugins/data/public/ui/dataset_selector/dataset_selector.tsx +++ b/src/plugins/data/public/ui/dataset_selector/dataset_selector.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { EuiButtonEmpty, EuiIcon, @@ -22,7 +22,7 @@ import { getQueryService } from '../../services'; interface DatasetSelectorProps { selectedDataset?: Dataset; - setSelectedDataset: (dataset?: Dataset) => void; + setSelectedDataset: (dataset: Dataset) => void; services: IDataPluginServices; } @@ -33,64 +33,92 @@ export const DatasetSelector = ({ }: DatasetSelectorProps) => { const [isOpen, setIsOpen] = useState(false); const [datasets, setDatasets] = useState([]); - const togglePopover = () => setIsOpen(!isOpen); - const closePopover = () => setIsOpen(false); const { overlays, savedObjects } = services; - const queryService = getQueryService(); - const datasetManager = queryService.queryString.getDatasetManager(); + const datasetService = getQueryService().queryString.getDatasetService(); const datasetIcon = - datasetManager.getDatasetHandlerById(selectedDataset?.type || '')?.meta.icon.type || 'database'; + datasetService.getType(selectedDataset?.type || '')?.meta.icon.type || 'database'; + + const fetchDatasets = useCallback(async () => { + const typeConfig = datasetService.getType(selectedDataset?.type || ''); + if (!typeConfig || typeConfig.id !== DEFAULT_DATA.SET_TYPES.INDEX_PATTERN) { + return; + } + + const fetchedIndexPatternDataStructures = await typeConfig.fetch(savedObjects.client, []); + + const fetchedDatasets = + fetchedIndexPatternDataStructures.children?.map((pattern) => + typeConfig.toDataset([pattern]) + ) ?? []; + setDatasets(fetchedDatasets); + + // If no dataset is selected, select the first one + if (!selectedDataset && fetchedDatasets.length > 0) { + setSelectedDataset(fetchedDatasets[0]); + } + }, [datasetService, savedObjects.client, selectedDataset, setSelectedDataset]); useEffect(() => { - const init = async () => { - setDatasets( - ( - await datasetManager - .getDatasetHandlerById(DEFAULT_DATA.SET_TYPES.INDEX_PATTERN) - ?.fetch(savedObjects.client, [])! - ).children || [] - ); - }; - - init(); - }, [datasetManager, savedObjects.client]); + fetchDatasets(); + }, [fetchDatasets]); + + const togglePopover = useCallback(async () => { + if (!isOpen) { + await fetchDatasets(); + } + setIsOpen(!isOpen); + }, [isOpen, fetchDatasets]); + + const closePopover = useCallback(() => setIsOpen(false), []); const options = useMemo(() => { - const newOptions: EuiSelectableOption[] = []; - // Add index pattern datasets - newOptions.push({ - label: 'Index patterns', - isGroupLabel: true, - }); + const newOptions: EuiSelectableOption[] = [ + { + label: 'Index patterns', + isGroupLabel: true, + }, + ]; - datasets.forEach(({ id, title, type }) => { + datasets.forEach(({ id, title, type, dataSource }) => { + const label = dataSource ? `${dataSource.title}::${title}` : title; newOptions.push({ - label: title, + label, checked: id === selectedDataset?.id ? 'on' : undefined, key: id, - prepend: , + prepend: , }); }); return newOptions; - }, [datasetManager, datasets, selectedDataset?.id]); + }, [datasets, selectedDataset?.id, datasetService]); - // Handle option change - const handleOptionChange = (newOptions: EuiSelectableOption[]) => { - const selectedOption = newOptions.find((option) => option.checked === 'on'); + const handleOptionChange = useCallback( + (newOptions: EuiSelectableOption[]) => { + const selectedOption = newOptions.find((option) => option.checked === 'on'); + if (selectedOption) { + const foundDataset = datasets.find((dataset) => dataset.id === selectedOption.key); + if (foundDataset) { + closePopover(); + setSelectedDataset(foundDataset); + } + } + }, + [datasets, setSelectedDataset, closePopover] + ); - if (!selectedOption) { - setSelectedDataset(undefined); - return; + const datasetTitle = useMemo(() => { + if (!selectedDataset) { + return 'Select data'; } - const foundDataset = datasets.find((dataset) => dataset.id === selectedOption.key); + if (selectedDataset.dataSource) { + return `${selectedDataset.dataSource.title}::${selectedDataset.title}`; + } - closePopover(); - setSelectedDataset(foundDataset || undefined); - }; + return selectedDataset.title; + }, [selectedDataset]); return ( - - {selectedDataset?.title ?? 'Select data'} + + {datasetTitle} } @@ -129,7 +157,9 @@ export const DatasetSelector = ({ savedObjects={savedObjects.client} onSelect={(dataset?: Dataset) => { overlay?.close(); - setSelectedDataset(dataset); + if (dataset) { + setSelectedDataset(dataset); + } }} onCancel={() => overlay?.close()} /> @@ -146,7 +176,7 @@ export const DatasetSelector = ({ { - const [selectedDataset, setSelectedDataset] = useState(); +interface ConnectedDatasetSelectorProps { + onSubmit: ((query: Query, dateRange?: TimeRange | undefined) => void) | undefined; +} + +const ConnectedDatasetSelector = ({ onSubmit }: ConnectedDatasetSelectorProps) => { const { services } = useOpenSearchDashboards(); - const datasetManager = services.data.query.queryString.getDatasetManager(); + const queryString = services.data.query.queryString; + const initialDataset = queryString.getQuery().dataset || queryString.getDefaultQuery().dataset; + const [selectedDataset, setSelectedDataset] = useState(initialDataset); useEffect(() => { - const initialDataset = datasetManager.getDataset() || datasetManager.getDefaultDataset(); - setSelectedDataset(initialDataset); - - const subscription = datasetManager.getUpdates$().subscribe((updatedDataset) => { - setSelectedDataset(updatedDataset); + const subscription = queryString.getUpdates$().subscribe((query) => { + setSelectedDataset(query.dataset); }); return () => subscription.unsubscribe(); - }, [datasetManager]); + }, [queryString]); const handleDatasetChange = (dataset?: Dataset) => { setSelectedDataset(dataset); if (dataset) { - datasetManager.setDataset(dataset); + const query = queryString.getInitialQueryByDataset(dataset); + queryString.setQuery(query); + onSubmit!(queryString.getQuery()); } }; diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 5483b540d5bf..c1e29b57e86a 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -47,5 +47,10 @@ export { QueryEditorExtensionDependencies, QueryEditorExtensionConfig, } from './query_editor'; -export { SearchBar, SearchBarProps, StatefulSearchBarProps } from './search_bar'; +export { + SearchBar, + SearchBarProps, + StatefulSearchBarProps, + useQueryStringManager, +} from './search_bar'; export { SuggestionsComponent } from './typeahead'; diff --git a/src/plugins/data/public/ui/mocks.ts b/src/plugins/data/public/ui/mocks.ts index dc70e5cac1d4..bbb3a118b9a9 100644 --- a/src/plugins/data/public/ui/mocks.ts +++ b/src/plugins/data/public/ui/mocks.ts @@ -38,7 +38,6 @@ function createStartContract( const queryEnhancements = new Map(); return { IndexPatternSelect: jest.fn(), - DataSetNavigator: jest.fn(), // Add the missing property SearchBar: jest.fn(), SuggestionsComponent: jest.fn(), // Add the missing property Settings: new SettingsMock( diff --git a/src/plugins/data/public/ui/query_editor/_query_editor.scss b/src/plugins/data/public/ui/query_editor/_query_editor.scss index 88ba526abcd5..82c71a199b59 100644 --- a/src/plugins/data/public/ui/query_editor/_query_editor.scss +++ b/src/plugins/data/public/ui/query_editor/_query_editor.scss @@ -102,7 +102,7 @@ } } -.osdQueryEditor__dataSetNavigatorWrapper { +.osdQueryEditor__datasetSelectorWrapper { :first-child { border-bottom: $euiBorderThin !important; } @@ -155,6 +155,11 @@ align-items: center; padding: $euiSizeXS; + > * { + flex: 0 1 auto; + min-width: 0; + } + .osdQueryEditor__collapseBtn { padding-right: $euiSizeXS; border-right: $euiBorderThin; diff --git a/src/plugins/data/public/ui/query_editor/language_selector.tsx b/src/plugins/data/public/ui/query_editor/language_selector.tsx index 455540d28df2..acfa03c34d3f 100644 --- a/src/plugins/data/public/ui/query_editor/language_selector.tsx +++ b/src/plugins/data/public/ui/query_editor/language_selector.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import React, { useState, useEffect } from 'react'; import { PopoverAnchorPosition, EuiContextMenuPanel, @@ -10,78 +11,72 @@ import { EuiButtonEmpty, EuiContextMenuItem, } from '@elastic/eui'; -import { i18n } from '@osd/i18n'; -import React, { useState } from 'react'; -import { getUiService } from '../../services'; +import { getQueryService, getUiService } from '../../services'; +import { LanguageConfig } from '../../query'; +import { Query } from '../..'; export interface QueryLanguageSelectorProps { - language: string; + query: Query; onSelectLanguage: (newLanguage: string) => void; anchorPosition?: PopoverAnchorPosition; appName?: string; } -const mapExternalLanguageToOptions = (language: string) => { +const mapExternalLanguageToOptions = (language: LanguageConfig) => { return { - label: language, - value: language, + label: language.title, + value: language.id, }; }; export const QueryLanguageSelector = (props: QueryLanguageSelectorProps) => { const [isPopoverOpen, setPopover] = useState(false); + const [currentLanguage, setCurrentLanguage] = useState(props.query.language); + + const uiService = getUiService(); + const queryString = getQueryService().queryString; + const languageService = queryString.getLanguageService(); + + useEffect(() => { + const subscription = queryString.getUpdates$().subscribe((query: Query) => { + if (query.language !== currentLanguage) { + setCurrentLanguage(query.language); + } + }); + + return () => { + subscription.unsubscribe(); + }; + }, [queryString, currentLanguage, props]); const onButtonClick = () => { setPopover(!isPopoverOpen); }; - const dqlLabel = i18n.translate('data.query.queryEditor.dqlLanguageName', { - defaultMessage: 'DQL', - }); - const luceneLabel = i18n.translate('data.query.queryEditor.luceneLanguageName', { - defaultMessage: 'Lucene', - }); - - const languageOptions = [ - { - label: dqlLabel, - value: 'kuery', - }, - { - label: luceneLabel, - value: 'lucene', - }, - ]; + const languageOptions: Array<{ label: string; value: string }> = []; - const uiService = getUiService(); - - const queryEnhancements = uiService.Settings.getAllQueryEnhancements(); - queryEnhancements.forEach((enhancement) => { + languageService.getLanguages().forEach((language) => { if ( - (enhancement.supportedAppNames && - props.appName && - !enhancement.supportedAppNames.includes(props.appName)) || - uiService.Settings.getUserQueryLanguageBlocklist().includes( - enhancement.language.toLowerCase() - ) + (language && props.appName && !language.supportedAppNames.includes(props.appName)) || + uiService.Settings.getUserQueryLanguageBlocklist().includes(language?.id) ) return; - languageOptions.unshift(mapExternalLanguageToOptions(enhancement.language)); + languageOptions.unshift(mapExternalLanguageToOptions(language!)); }); const selectedLanguage = { label: (languageOptions.find( - (option) => (option.value as string).toLowerCase() === props.language.toLowerCase() + (option) => (option.value as string).toLowerCase() === currentLanguage.toLowerCase() )?.label as string) ?? languageOptions[0].label, }; const handleLanguageChange = (newLanguage: string) => { + setCurrentLanguage(newLanguage); props.onSelectLanguage(newLanguage); - uiService.Settings.setUserQueryLanguage(newLanguage); }; - uiService.Settings.setUserQueryLanguage(props.language); + uiService.Settings.setUserQueryLanguage(currentLanguage); const languageOptionsMenu = languageOptions .sort((a, b) => { @@ -102,6 +97,7 @@ export const QueryLanguageSelector = (props: QueryLanguageSelectorProps) => { ); }); + return ( void; onChangeQueryEditorFocus?: (isFocused: boolean) => void; onSubmit?: (query: Query, dateRange?: TimeRange) => void; - getQueryStringInitialValue?: (language: string) => string; dataTestSubj?: string; size?: SuggestionsListSize; className?: string; isInvalid?: boolean; - queryLanguage?: string; headerClassName?: string; bannerClassName?: string; footerClassName?: string; @@ -86,7 +82,7 @@ export default class QueryEditorUI extends Component { public inputRef: monaco.editor.IStandaloneCodeEditor | null = null; - private queryService = getQueryService(); + private queryString = getQueryService().queryString; private persistedLog: PersistedLog | undefined; private abortController?: AbortController; @@ -96,9 +92,6 @@ export default class QueryEditorUI extends Component { private extensionMap = this.props.settings?.getQueryEditorExtensionMap(); private getQueryString = () => { - if (!this.props.query.query) { - return this.props.getQueryStringInitialValue?.(this.props.query.language) ?? ''; - } return toUser(this.props.query.query); }; @@ -111,7 +104,7 @@ export default class QueryEditorUI extends Component { !( this.headerRef.current && this.bannerRef.current && - this.props.queryLanguage && + this.props.query.language && this.extensionMap && Object.keys(this.extensionMap).length > 0 ) @@ -120,7 +113,7 @@ export default class QueryEditorUI extends Component { } return ( { this.persistedLog.add(query.query); } - this.props.onSubmit({ query: fromUser(query.query), language: query.language }); + this.props.onSubmit({ + query: fromUser(query.query), + language: query.language, + dataset: query.dataset, + }); } }; private onChange = (query: Query, dateRange?: TimeRange) => { if (this.props.onChange) { - this.props.onChange({ query: fromUser(query.query), language: query.language }, dateRange); + this.props.onChange( + { query: fromUser(query.query), language: query.language, dataset: query.dataset }, + dateRange + ); } }; @@ -153,7 +153,11 @@ export default class QueryEditorUI extends Component { index: null, }); - this.onChange({ query: value, language: this.props.query.language }); + this.onChange({ + query: value, + language: this.props.query.language, + dataset: this.props.query.dataset, + }); }; private onInputChange = (value: string) => { @@ -173,21 +177,18 @@ export default class QueryEditorUI extends Component { }; // TODO: MQL consider moving language select language of setting search source here - private onSelectLanguage = (language: string) => { + private onSelectLanguage = (languageId: string) => { // Send telemetry info every time the user opts in or out of kuery // As a result it is important this function only ever gets called in the // UI component's change handler. this.services.http.post('/api/opensearch-dashboards/dql_opt_in_stats', { - body: JSON.stringify({ opt_in: language === 'kuery' }), + body: JSON.stringify({ opt_in: languageId === 'kuery' }), }); - const newQuery = { - query: this.props.getQueryStringInitialValue?.(language) ?? '', - language, - }; + const languageConfig = this.queryString.getLanguageService().getLanguage(languageId); + const newQuery = this.queryString.getInitialQueryByLanguage(languageId); - const enhancement = this.props.settings.getQueryEnhancements(newQuery.language); - const fields = enhancement?.fields; + const fields = languageConfig?.fields; const newSettings: DataSettings = { userQueryLanguage: newQuery.language, userQueryString: newQuery.query, @@ -195,7 +196,7 @@ export default class QueryEditorUI extends Component { }; this.props.settings?.updateSettings(newSettings); - const dateRangeEnhancement = enhancement?.searchBar?.dateRange; + const dateRangeEnhancement = languageConfig?.searchBar?.dateRange; const dateRange = dateRangeEnhancement ? { from: dateRangeEnhancement.initialFrom!, @@ -227,6 +228,19 @@ export default class QueryEditorUI extends Component { } public componentDidUpdate(prevProps: Props) { + const prevQuery = prevProps.query; + + if (!isEqual(this.props.query.dataset, prevQuery.dataset)) { + if (this.inputRef) { + const newQuery = this.queryString.getInitialQuery(); + const newQueryString = newQuery.query; + if (this.inputRef.getValue() !== newQueryString) { + this.inputRef.setValue(newQueryString); + this.onSubmit(newQuery); + } + } + } + const parsedQuery = fromUser(toUser(this.props.query.query)); if (!isEqual(this.props.query.query, parsedQuery)) { this.onChange({ ...this.props.query, query: parsedQuery }); @@ -246,9 +260,10 @@ export default class QueryEditorUI extends Component { }; private fetchIndexPattern = async () => { - const dataSetTitle = this.queryService.queryString.getDatasetManager().getDataset()?.title; - if (!dataSetTitle) return undefined; - return getIndexPatterns().getByTitle(dataSetTitle); + const dataset = this.queryString.getQuery().dataset; + if (!dataset) return undefined; + const indexPattern = await getIndexPatterns().get(dataset.id); + return indexPattern; }; provideCompletionItems = async ( @@ -297,7 +312,7 @@ export default class QueryEditorUI extends Component { const languageSelector = ( { footerItems: { start: [ `${this.state.lineCount} ${this.state.lineCount === 1 ? 'line' : 'lines'}`, - this.props.dataset?.timeFieldName || '', + this.props.query.dataset?.timeFieldName || '', ], }, provideCompletionItems: this.provideCompletionItems, @@ -331,7 +346,6 @@ export default class QueryEditorUI extends Component { onChange: (value: string) => { // Replace new lines with an empty string to prevent multi-line input this.onQueryStringChange(value.replace(/[\r\n]+/gm, '')); - this.setState({ lineCount: undefined }); }, editorDidMount: (editor: monaco.editor.IStandaloneCodeEditor) => { @@ -381,7 +395,7 @@ export default class QueryEditorUI extends Component { onClick={() => this.setState({ isCollapsed: !this.state.isCollapsed })} isCollapsed={!this.state.isCollapsed} /> - +
{this.state.isCollapsed ? languageEditor.TopBar.Collapsed() diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx index b6a35565b42e..da3ba70b3837 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx @@ -13,7 +13,6 @@ import { prettyDuration, } from '@elastic/eui'; import classNames from 'classnames'; -import { isEqual } from 'lodash'; import React, { useState } from 'react'; import { createPortal } from 'react-dom'; import { IDataPluginServices, IIndexPattern, Query, TimeHistoryContract, TimeRange } from '../..'; @@ -26,7 +25,6 @@ import { getQueryLog, PersistedLog } from '../../query'; import { Settings } from '../types'; import { NoDataPopover } from './no_data_popover'; import QueryEditorUI from './query_editor'; -import { useDatasetManager } from '../search_bar/lib/use_dataset_manager'; const QueryEditor = withOpenSearchDashboards(QueryEditorUI); @@ -73,29 +71,14 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { query: { queryString }, }, } = opensearchDashboards.services; - const datasetManager = queryString.getDatasetManager(); - const { dataset } = useDatasetManager({ datasetManager }); const queryLanguage = props.query && props.query.language; - const queryUiEnhancement = - (queryLanguage && - props.settings && - props.settings.getQueryEnhancements(queryLanguage)?.searchBar) || - null; - const parsedQuery = - !queryUiEnhancement || isValidQuery(props.query) - ? props.query! - : { query: getQueryStringInitialValue(queryLanguage!), language: queryLanguage! }; - if (!isEqual(parsedQuery?.query, props.query?.query)) { - onQueryChange(parsedQuery); - onSubmit({ query: parsedQuery, dateRange: getDateRange() }); - } const persistedLog: PersistedLog | undefined = React.useMemo( () => queryLanguage && uiSettings && storage && appName ? getQueryLog(uiSettings!, storage, appName, queryLanguage) : undefined, - [appName, queryLanguage, uiSettings, storage] + [queryLanguage, uiSettings, storage, appName] ); function onClickSubmitButton(event: React.MouseEvent) { @@ -108,17 +91,20 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { function getDateRange() { const defaultTimeSetting = uiSettings!.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS); + const languageConfig = queryString.getLanguageService().getLanguage(queryLanguage!); return { from: props.dateRangeFrom || - queryUiEnhancement?.dateRange?.initialFrom || + languageConfig?.searchBar?.dateRange?.initialFrom || defaultTimeSetting.from, - to: props.dateRangeTo || queryUiEnhancement?.dateRange?.initialTo || defaultTimeSetting.to, + to: + props.dateRangeTo || + languageConfig?.searchBar?.dateRange?.initialTo || + defaultTimeSetting.to, }; } function onQueryChange(query: Query, dateRange?: TimeRange) { - if (queryUiEnhancement && !isValidQuery(query)) return; props.onChange({ query, dateRange: dateRange ?? getDateRange(), @@ -191,39 +177,22 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { return valueAsMoment.toISOString(); } - function isValidQuery(query: Query | undefined) { - if (query && query.query) return true; - } - - function getQueryStringInitialValue(language: string) { - const { settings } = props; - const input = settings?.getQueryEnhancements(language)?.searchBar?.queryStringInput - ?.initialValue; - - if (!input) return ''; - - return input.replace('', dataset?.title ?? dataset?.title ?? ''); - } - function renderQueryEditor() { if (!shouldRenderQueryEditor()) return; return ( @@ -247,9 +216,10 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { } function shouldRenderDatePicker(): boolean { + const languageConfig = queryString.getLanguageService().getLanguage(queryLanguage!); return Boolean( - (props.showDatePicker && (queryUiEnhancement?.showDatePicker ?? true)) ?? - (props.showAutoRefreshOnly && (queryUiEnhancement?.showAutoRefreshOnly ?? true)) + (props.showDatePicker && (languageConfig?.searchBar?.showDatePicker ?? true)) ?? + (props.showAutoRefreshOnly && (languageConfig?.searchBar?.showAutoRefreshOnly ?? true)) ); } diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 675f6cdc5791..a310d6e67c21 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -154,8 +154,7 @@ export function createSearchBar({ filterManager: data.query.filterManager, }); const { query } = useQueryStringManager({ - query: props.query, - queryStringManager: data.query.queryString, + queryString: data.query.queryString, }); const { timeRange, refreshInterval } = useTimefilter({ diff --git a/src/plugins/data/public/ui/search_bar/index.tsx b/src/plugins/data/public/ui/search_bar/index.tsx index 41cc1c2a992b..6fffe20199e2 100644 --- a/src/plugins/data/public/ui/search_bar/index.tsx +++ b/src/plugins/data/public/ui/search_bar/index.tsx @@ -45,3 +45,4 @@ const WrappedSearchBar = (props: SearchBarProps) => ( export const SearchBar = injectI18n(withOpenSearchDashboards(WrappedSearchBar)); export { StatefulSearchBarProps } from './create_search_bar'; export type { SearchBarProps, SearchBarOwnProps } from './search_bar'; +export { useQueryStringManager } from './lib/use_query_string_manager'; diff --git a/src/plugins/data/public/ui/search_bar/lib/use_dataset_manager.ts b/src/plugins/data/public/ui/search_bar/lib/use_dataset_manager.ts deleted file mode 100644 index 430a34227c83..000000000000 --- a/src/plugins/data/public/ui/search_bar/lib/use_dataset_manager.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useState, useEffect } from 'react'; -import { Subscription } from 'rxjs'; -import { Dataset } from '../../../../../data/common'; -import { DatasetContract } from '../../../query'; - -interface UseDatasetManagerProps { - dataset?: Dataset; - datasetManager: DatasetContract; -} - -export const useDatasetManager = (props: UseDatasetManagerProps) => { - const [dataset, setDataset] = useState( - props.dataset || props.datasetManager.getDataset() - ); - - useEffect(() => { - const subscriptions = new Subscription(); - - subscriptions.add( - props.datasetManager.getUpdates$().subscribe({ - next: () => { - const newDataset = props.datasetManager.getDataset(); - setDataset(newDataset); - }, - }) - ); - - return () => { - subscriptions.unsubscribe(); - }; - }, [dataset, props.dataset, props.datasetManager]); - - return { dataset }; -}; diff --git a/src/plugins/data/public/ui/search_bar/lib/use_query_string_manager.ts b/src/plugins/data/public/ui/search_bar/lib/use_query_string_manager.ts index b88bd282d442..8f9d49f80fef 100644 --- a/src/plugins/data/public/ui/search_bar/lib/use_query_string_manager.ts +++ b/src/plugins/data/public/ui/search_bar/lib/use_query_string_manager.ts @@ -28,35 +28,50 @@ * under the License. */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Subscription } from 'rxjs'; import { Query } from '../../..'; import { QueryStringContract } from '../../../query/query_string'; interface UseQueryStringProps { query?: Query; - queryStringManager: QueryStringContract; + queryString: QueryStringContract; } export const useQueryStringManager = (props: UseQueryStringProps) => { // Filters should be either what's passed in the initial state or the current state of the filter manager - const [query, setQuery] = useState(props.query || props.queryStringManager.getQuery()); + const [query, setQuery] = useState(() => props.query || props.queryString.getQuery()); + useEffect(() => { const subscriptions = new Subscription(); - subscriptions.add( - props.queryStringManager.getUpdates$().subscribe({ + props.queryString.getUpdates$().subscribe({ next: () => { - const newQuery = props.queryStringManager.getQuery(); - setQuery(newQuery); + setQuery((prevQuery) => { + const newQuery = props.queryString.getQuery(); + // Only update if the query has actually changed + return JSON.stringify(prevQuery) !== JSON.stringify(newQuery) ? newQuery : prevQuery; + }); }, }) ); - return () => { subscriptions.unsubscribe(); }; - }, [props.queryStringManager]); + }, [props.queryString]); + + // Use callback to memoize the function + const updateQuery = useCallback( + (newQueryPartial: Partial) => { + const updatedQuery = { ...query, ...newQueryPartial }; + props.queryString.setQuery(updatedQuery); + setQuery(updatedQuery); + }, + [query, props.queryString] + ); - return { query }; + return { + query, + updateQuery, + }; }; diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index e6f1c74b6bb0..666a54946f57 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -119,9 +119,8 @@ class SearchBarUI extends Component { }; private services = this.props.opensearchDashboards.services; - private dataSetService = this.services.data.query.dataSetManager; - private queryStringService = this.services.data.query.queryString; - private savedQueryService = this.services.data.query.savedQueries; + private queryService = this.services.data.query; + private savedQueryService = this.queryService.savedQueries; public filterBarRef: Element | null = null; public filterBarWrapperRef: Element | null = null; private useNewHeader = Boolean(this.services.uiSettings.get(UI_SETTINGS.NEW_HOME_PAGE)); @@ -136,6 +135,7 @@ class SearchBarUI extends Component { nextQuery = { query: nextProps.query.query, language: nextProps.query.language, + dataset: nextProps.query.dataset, }; } else if ( nextProps.query && @@ -145,6 +145,17 @@ class SearchBarUI extends Component { nextQuery = { query: '', language: nextProps.query.language, + dataset: nextProps.query.dataset, + }; + } else if ( + nextProps.query && + prevState.query && + nextProps.query.dataset !== prevState.query.dataset + ) { + nextQuery = { + query: nextProps.query.query, + language: nextProps.query.language, + dataset: nextProps.query.dataset, }; } @@ -238,8 +249,8 @@ class SearchBarUI extends Component { (!this.useNewHeader || this.props.filters.length > 0) && this.props.indexPatterns && compact(this.props.indexPatterns).length > 0 && - (this.props.settings?.getQueryEnhancements(this.state.query?.language!)?.searchBar - ?.showFilterBar ?? + (this.queryService.queryString.getLanguageService().getLanguage(this.state.query?.language!) + ?.searchBar?.showFilterBar ?? true) ); } @@ -374,10 +385,10 @@ class SearchBarUI extends Component { } } ); - const dataSet = this.dataSetService.getDataSet(); - if (dataSet && queryAndDateRange.query) { - this.queryStringService.addToQueryHistory( - dataSet, + const dataset = this.queryService.queryString.getQuery().dataset; + if (dataset && queryAndDateRange.query) { + this.queryService.queryString.addToQueryHistory( + dataset, queryAndDateRange.query, queryAndDateRange.dateRange ); diff --git a/src/plugins/data/public/ui/settings/settings.ts b/src/plugins/data/public/ui/settings/settings.ts index 7fc758712ad2..f5dd8f247f84 100644 --- a/src/plugins/data/public/ui/settings/settings.ts +++ b/src/plugins/data/public/ui/settings/settings.ts @@ -8,7 +8,7 @@ import { DataStorage, setOverrides as setFieldOverrides } from '../../../common' import { ConfigSchema } from '../../../config'; import { ISearchStart } from '../../search'; import { QueryEditorExtensionConfig } from '../query_editor/query_editor_extensions'; -import { QueryEnhancement } from '../types'; +import { IQueryStart } from '../../query'; export interface DataSettings { userQueryLanguage: string; @@ -30,8 +30,8 @@ export class Settings { constructor( private readonly config: ConfigSchema['enhancements'], private readonly search: ISearchStart, + private readonly query: IQueryStart, private readonly storage: DataStorage, - private readonly queryEnhancements: Map, private readonly queryEditorExtensionMap: Record ) { this.isEnabled = true; @@ -58,14 +58,6 @@ export class Settings { return true; } - getAllQueryEnhancements() { - return this.queryEnhancements; - } - - getQueryEnhancements(language: string) { - return this.queryEnhancements.get(language); - } - getQueryEditorExtensionMap() { return this.queryEditorExtensionMap; } @@ -86,18 +78,12 @@ export class Settings { return this.storage.get('userQueryLanguage') || 'kuery'; } - setUserQueryLanguage(language: string) { - if (language !== this.getUserQueryLanguage()) { + setUserQueryLanguage(languageId: string) { + if (languageId !== this.getUserQueryLanguage()) { this.search.df.clear(); } - this.storage.set('userQueryLanguage', language); - const queryEnhancement = this.queryEnhancements.get(language); - this.search.__enhance({ - searchInterceptor: queryEnhancement - ? queryEnhancement.search - : this.search.getDefaultSearchInterceptor(), - }); - this.setUiOverridesByUserQueryLanguage(language); + this.storage.set('userQueryLanguage', languageId); + this.setUiOverridesByUserQueryLanguage(languageId); return true; } @@ -126,10 +112,10 @@ export class Settings { return true; } - setUiOverridesByUserQueryLanguage(language: string) { - const queryEnhancement = this.queryEnhancements.get(language); - if (queryEnhancement) { - const { fields = {}, showDocLinks } = queryEnhancement; + setUiOverridesByUserQueryLanguage(languageId: string) { + const language = this.query.queryString.getLanguageService().getLanguage(languageId); + if (language) { + const { fields = {}, showDocLinks } = language; this.setUiOverrides({ fields, showDocLinks }); } else { this.setUiOverrides({ fields: undefined, showDocLinks: undefined }); @@ -170,17 +156,11 @@ export class Settings { interface Deps { config: ConfigSchema['enhancements']; search: ISearchStart; + query: IQueryStart; storage: DataStorage; - queryEnhancements: Map; queryEditorExtensionMap: Record; } -export function createSettings({ - config, - search, - storage, - queryEnhancements, - queryEditorExtensionMap, -}: Deps) { - return new Settings(config, search, storage, queryEnhancements, queryEditorExtensionMap); +export function createSettings({ config, search, query, storage, queryEditorExtensionMap }: Deps) { + return new Settings(config, search, query, storage, queryEditorExtensionMap); } diff --git a/src/plugins/data/public/ui/types.ts b/src/plugins/data/public/ui/types.ts index 0890ea1c13bd..1d13a13c6040 100644 --- a/src/plugins/data/public/ui/types.ts +++ b/src/plugins/data/public/ui/types.ts @@ -4,7 +4,6 @@ */ import { Observable } from 'rxjs'; -import { SearchInterceptor } from '../search'; import { IndexPatternSelectProps } from './index_pattern_select'; import { StatefulSearchBarProps } from './search_bar'; import { QueryEditorExtensionConfig } from './query_editor/query_editor_extensions'; @@ -13,41 +12,7 @@ import { SuggestionsComponentProps } from './typeahead/suggestions_component'; export * from './settings'; -export interface QueryEnhancement { - // TODO: MQL do want to default have supported all data_sources? - // or should data connect have a record of query enhancements that are supported - language: string; - search: SearchInterceptor; - // Leave blank to support all data sources - // supportedDataSourceTypes?: Record; - searchBar?: { - showDataSetsSelector?: boolean; - showDataSourcesSelector?: boolean; - showQueryInput?: boolean; - showFilterBar?: boolean; - showDatePicker?: boolean; - showAutoRefreshOnly?: boolean; - queryStringInput?: { - // will replace '' with the data source name - initialValue?: string; - }; - dateRange?: { - initialFrom?: string; - initialTo?: string; - }; - }; - fields?: { - filterable?: boolean; - visualizable?: boolean; - }; - showDocLinks?: boolean; - // List of supported app names that this enhancement should be enabled for, - // if not provided it will be enabled for all apps - supportedAppNames?: string[]; -} - export interface UiEnhancements { - query?: QueryEnhancement; queryEditorExtension?: QueryEditorExtensionConfig; } diff --git a/src/plugins/data/public/ui/ui_service.ts b/src/plugins/data/public/ui/ui_service.ts index ff7d32bb2467..faea0b738571 100644 --- a/src/plugins/data/public/ui/ui_service.ts +++ b/src/plugins/data/public/ui/ui_service.ts @@ -12,7 +12,7 @@ import { QueryEditorExtensionConfig } from './query_editor'; import { createSearchBar } from './search_bar/create_search_bar'; import { createSettings } from './settings'; import { SuggestionsComponent } from './typeahead'; -import { IUiSetup, IUiStart, QueryEnhancement, UiEnhancements } from './types'; +import { IUiSetup, IUiStart, UiEnhancements } from './types'; import { DataStorage } from '../../common'; /** @internal */ @@ -27,7 +27,6 @@ export interface UiServiceStartDependencies { export class UiService implements Plugin { enhancementsConfig: ConfigSchema['enhancements']; - private queryEnhancements: Map = new Map(); private queryEditorExtensionMap: Record = {}; private dataSetContainer$ = new BehaviorSubject(null); @@ -41,9 +40,6 @@ export class UiService implements Plugin { return { __enhance: (enhancements?: UiEnhancements) => { if (!enhancements) return; - if (enhancements.query && enhancements.query.language) { - this.queryEnhancements.set(enhancements.query.language, enhancements.query); - } if (enhancements.queryEditorExtension) { this.queryEditorExtensionMap[enhancements.queryEditorExtension.id] = enhancements.queryEditorExtension; @@ -56,8 +52,8 @@ export class UiService implements Plugin { const Settings = createSettings({ config: this.enhancementsConfig, search: dataServices.search, + query: dataServices.query, storage, - queryEnhancements: this.queryEnhancements, queryEditorExtensionMap: this.queryEditorExtensionMap, }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index deb277087fcf..98ef84e20008 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -78,7 +78,6 @@ import { IDataFrame, IDataFrameResponse, createDataFrameCache, - dataFrameToSpec, } from '../../common'; type StrategyMap = Record>; @@ -215,29 +214,7 @@ export class SearchService implements Plugin { const dfService: DataFrameService = { get: () => this.dfCache.get(), set: async (dataFrame: IDataFrame) => { - if (this.dfCache.get() && this.dfCache.get()?.name !== dataFrame.name) { - scopedIndexPatterns.clearCache(this.dfCache.get()!.name, false); - } - if ( - dataFrame.meta && - dataFrame.meta.queryConfig && - 'dataSource' in dataFrame.meta.queryConfig - ) { - const dataSource = await scopedIndexPatterns.findDataSourceByTitle( - dataFrame.meta.queryConfig.dataSource - ); - dataFrame.meta.queryConfig.dataSourceId = dataSource?.id; - } this.dfCache.set(dataFrame); - const dataSetName = `${dataFrame.meta?.queryConfig?.dataSourceId ?? ''}.${ - dataFrame.name - }`; - const existingIndexPattern = await scopedIndexPatterns.get(dataSetName, true); - const dataSet = await scopedIndexPatterns.create( - dataFrameToSpec(dataFrame, existingIndexPattern?.id ?? dataSetName), - !existingIndexPattern?.id - ); - scopedIndexPatterns.saveToCache(dataSetName, dataSet); }, clear: () => { if (this.dfCache.get() === undefined) return; diff --git a/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts b/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts index 70fb0b77c47c..1fca4a659244 100644 --- a/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts +++ b/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts @@ -5,13 +5,12 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { DataExplorerServices } from '../../types'; -import { Dataset } from '../../../../data/common'; +import { QUERY_ENHANCEMENT_ENABLED_SETTING } from '../../components/constants'; export interface MetadataState { indexPattern?: string; originatingApp?: string; view?: string; - dataSet?: Omit; } const initialState: MetadataState = {}; @@ -20,12 +19,16 @@ export const getPreloadedState = async ({ embeddable, scopedHistory, data, + uiSettings, }: DataExplorerServices): Promise => { const { originatingApp } = embeddable .getStateTransfer(scopedHistory) .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; - const defaultIndexPattern = await data.indexPatterns.getDefault(); + const isQueryEnhancementEnabled = uiSettings.get(QUERY_ENHANCEMENT_ENABLED_SETTING); + const defaultIndexPattern = isQueryEnhancementEnabled + ? undefined + : await data.indexPatterns.getDefault(); const preloadedState: MetadataState = { ...initialState, originatingApp, @@ -39,12 +42,9 @@ export const slice = createSlice({ name: 'metadata', initialState, reducers: { - setIndexPattern: (state, action: PayloadAction) => { + setIndexPattern: (state, action: PayloadAction) => { state.indexPattern = action.payload; }, - setDataSet: (state, action: PayloadAction>) => { - state.dataSet = action.payload; - }, setOriginatingApp: (state, action: PayloadAction) => { state.originatingApp = action.payload; }, @@ -58,4 +58,4 @@ export const slice = createSlice({ }); export const { reducer } = slice; -export const { setIndexPattern, setDataSet, setOriginatingApp, setView, setState } = slice.actions; +export const { setIndexPattern, setOriginatingApp, setView, setState } = slice.actions; diff --git a/src/plugins/data_explorer/public/utils/state_management/redux_persistence.ts b/src/plugins/data_explorer/public/utils/state_management/redux_persistence.ts index 81517f3e9f4f..7eb715433f01 100644 --- a/src/plugins/data_explorer/public/utils/state_management/redux_persistence.ts +++ b/src/plugins/data_explorer/public/utils/state_management/redux_persistence.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { Dataset, DEFAULT_DATA } from '../../../../data/common'; +import { QUERY_ENHANCEMENT_ENABLED_SETTING } from '../../components/constants'; import { DataExplorerServices } from '../../types'; import { getPreloadedState } from './preload'; import { RootState } from './store'; @@ -10,12 +12,49 @@ import { RootState } from './store'; export const loadReduxState = async (services: DataExplorerServices) => { try { const serializedState = services.osdUrlStateStorage.get('_a'); - if (serializedState !== null) return serializedState; + if (serializedState !== null) { + const isQueryEnhancementEnabled = services.uiSettings.get(QUERY_ENHANCEMENT_ENABLED_SETTING); + + // Migrate index pattern to query state + if (isQueryEnhancementEnabled && serializedState.metadata.indexPattern) { + const indexPattern = await services.data.indexPatterns.get( + serializedState.metadata.indexPattern + ); + + const dataset: Dataset = { + id: serializedState.metadata.indexPattern, + title: indexPattern.title, + type: DEFAULT_DATA.SET_TYPES.INDEX_PATTERN, + }; + + if (indexPattern.dataSourceRef) { + const dataSource = await services.data.indexPatterns.getDataSource( + indexPattern.dataSourceRef.id + ); + + if (dataSource) { + dataset.dataSource = { + id: dataSource.id, + title: dataSource.attributes.title, + type: dataSource.attributes.dataSourceEngineType || '', + }; + } + } + services.data.query.queryString.setQuery({ + dataset, + }); + + delete serializedState.metadata.indexPattern; + } + + return serializedState; + } } catch (err) { // eslint-disable-next-line no-console console.error(err); } + // If state is not found, load the default state return await getPreloadedState(services); }; diff --git a/src/plugins/data_explorer/public/utils/state_management/store.ts b/src/plugins/data_explorer/public/utils/state_management/store.ts index 9d320de4b54b..daf0b3d7e369 100644 --- a/src/plugins/data_explorer/public/utils/state_management/store.ts +++ b/src/plugins/data_explorer/public/utils/state_management/store.ts @@ -116,4 +116,4 @@ export type RenderState = Omit; // Remaining state after export type Store = ReturnType; export type AppDispatch = Store['dispatch']; -export { MetadataState, setIndexPattern, setDataSet, setOriginatingApp } from './metadata_slice'; +export { MetadataState, setIndexPattern, setOriginatingApp } from './metadata_slice'; diff --git a/src/plugins/discover/public/application/utils/state_management/index.ts b/src/plugins/discover/public/application/utils/state_management/index.ts index e6df7e4774b8..989b2662f0d4 100644 --- a/src/plugins/discover/public/application/utils/state_management/index.ts +++ b/src/plugins/discover/public/application/utils/state_management/index.ts @@ -7,7 +7,6 @@ import { TypedUseSelectorHook } from 'react-redux'; import { RootState, setIndexPattern as updateIndexPattern, - setDataSet as updateDataSet, useTypedDispatch, useTypedSelector, } from '../../../../../data_explorer/public'; @@ -21,4 +20,4 @@ export interface DiscoverRootState extends RootState { export const useSelector: TypedUseSelectorHook = useTypedSelector; export const useDispatch = useTypedDispatch; -export { updateIndexPattern, updateDataSet }; +export { updateIndexPattern }; diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx index 500c8ebdc80c..a44ac89c5d62 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -3,15 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { i18n } from '@osd/i18n'; import React, { useEffect, useState, useRef, useCallback } from 'react'; -import { - EuiButtonIcon, - EuiContextMenu, - EuiPanel, - EuiPopover, - EuiCompressedSwitch, -} from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; import { TopNav } from './top_nav'; import { ViewProps } from '../../../../../data_explorer/public'; import { DiscoverTable } from './discover_table'; @@ -33,7 +26,6 @@ import { import { OpenSearchSearchHit } from '../../../application/doc_views/doc_views_types'; import { buildColumns } from '../../utils/columns'; import './discover_canvas.scss'; -import { getNewDiscoverSetting, setNewDiscoverSetting } from '../../components/utils/local_storage'; import { HeaderVariant } from '../../../../../../core/public'; // eslint-disable-next-line import/no-default-export diff --git a/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx index e38545d965d4..34cf98e12496 100644 --- a/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx @@ -20,7 +20,6 @@ import { useDiscoverContext } from '../context'; import { useDispatch, setSavedQuery, useSelector } from '../../utils/state_management'; import './discover_canvas.scss'; -import { useDatasetManager } from '../utils/use_dataset_manager'; import { TopNavMenuItemRenderType } from '../../../../../navigation/public'; export interface TopNavProps { @@ -75,16 +74,16 @@ export const TopNav = ({ opts, showSaveQuery, isEnhancementsEnabled }: TopNavPro const initializeDataset = async () => { await data.indexPatterns.ensureDefaultIndexPattern(); const defaultIndexPattern = await data.indexPatterns.getDefault(); - const datasetManager = data.query.queryString.getDatasetManager(); - datasetManager.fetchDefaultDataset(); - const defaultDataset = datasetManager.getDefaultDataset(); + // TODO: ROCKY do we need this? + // const queryString = data.query.queryString; + // const defaultDataset = queryString.getDatasetService().getDefault(); if (!isMounted) return; setIndexPatterns(defaultIndexPattern ? [defaultIndexPattern] : undefined); - if (defaultDataset) { - datasetManager.setDataset(defaultDataset); - } + // if (defaultDataset) { + // datasetManager.setDataset(defaultDataset); + // } }; initializeDataset(); diff --git a/src/plugins/discover/public/application/view_components/utils/update_search_source.ts b/src/plugins/discover/public/application/view_components/utils/update_search_source.ts index d5e6771679f9..4d837a632520 100644 --- a/src/plugins/discover/public/application/view_components/utils/update_search_source.ts +++ b/src/plugins/discover/public/application/view_components/utils/update_search_source.ts @@ -30,23 +30,12 @@ export const updateSearchSource = async ({ histogramConfigs, }: Props) => { const { uiSettings, data } = services; - const queryDataset = data.query.queryString.getDatasetManager().getDataset(); + const queryDataset = data.query.queryString.getQuery().dataset; - let dataset = + const dataset = indexPattern.id === queryDataset?.id ? await data.indexPatterns.get(queryDataset?.id!, true) : indexPattern; - const dataFrame = searchSource?.getDataFrame(); - if ( - searchSource && - dataFrame && - dataFrame.name && - dataFrame.name !== '' && - dataset.title !== dataFrame.name - ) { - dataset = data.indexPatterns.getByTitle(dataFrame.name, true) ?? dataset; - searchSource.setField('index', dataset); - } const sortForSearchSource = getSortForSearchSource( sort, diff --git a/src/plugins/discover/public/application/view_components/utils/use_dataset_manager.ts b/src/plugins/discover/public/application/view_components/utils/use_dataset_manager.ts deleted file mode 100644 index abc84fa943f1..000000000000 --- a/src/plugins/discover/public/application/view_components/utils/use_dataset_manager.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useState, useEffect } from 'react'; -import { Subscription } from 'rxjs'; -import { Dataset } from '../../../../../data/common'; -import { DatasetContract } from '../../../../../data/public'; - -interface UseDatasetManagerProps { - dataset?: Dataset; - datasetManager: DatasetContract; -} - -export const useDatasetManager = (props: UseDatasetManagerProps) => { - const [dataset, setDataset] = useState( - props.dataset || props.datasetManager.getDataset() - ); - - useEffect(() => { - const subscriptions = new Subscription(); - - subscriptions.add( - props.datasetManager.getUpdates$().subscribe({ - next: () => { - const newDataset = props.datasetManager.getDataset(); - setDataset(newDataset); - }, - }) - ); - - return () => { - subscriptions.unsubscribe(); - }; - }, [dataset, props.dataset, props.datasetManager]); - - return { dataset }; -}; diff --git a/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts b/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts index c46d73d854ab..2f8bd6fbebf6 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts @@ -5,12 +5,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { i18n } from '@osd/i18n'; -import { DEFAULT_QUERY, Dataset, SavedObjectReference } from '../../../../../data/common'; -import { IndexPattern } from '../../../../../data/public'; +import { IndexPattern, useQueryStringManager } from '../../../../../data/public'; import { useSelector, updateIndexPattern } from '../../utils/state_management'; import { DiscoverViewServices } from '../../../build_services'; import { getIndexPatternId } from '../../helpers/get_index_pattern_id'; -import { useDatasetManager } from './use_dataset_manager'; import { QUERY_ENHANCEMENT_ENABLED_SETTING } from '../../../../common'; /** @@ -29,8 +27,8 @@ import { QUERY_ENHANCEMENT_ENABLED_SETTING } from '../../../../common'; */ export const useIndexPattern = (services: DiscoverViewServices) => { const { data, toastNotifications, uiSettings, store } = services; - const { dataset } = useDatasetManager({ - datasetManager: data.query.queryString.getDatasetManager(), + const { query } = useQueryStringManager({ + queryString: data.query.queryString, }); const indexPatternIdFromState = useSelector((state) => state.metadata.indexPattern); const [indexPattern, setIndexPattern] = useState(undefined); @@ -47,8 +45,8 @@ export const useIndexPattern = (services: DiscoverViewServices) => { let isMounted = true; const handleIndexPattern = async () => { - if (isQueryEnhancementEnabled && dataset) { - const pattern = await data.indexPatterns.get(dataset.id, true); + if (isQueryEnhancementEnabled && query?.dataset) { + const pattern = await data.indexPatterns.get(query.dataset.id); if (isMounted && pattern) { setIndexPattern(pattern); @@ -56,7 +54,11 @@ export const useIndexPattern = (services: DiscoverViewServices) => { } else if (!isQueryEnhancementEnabled) { if (!indexPatternIdFromState) { const indexPatternList = await data.indexPatterns.getCache(); - const newId = getIndexPatternId('', indexPatternList, uiSettings.get('defaultIndex')); + const newId = getIndexPatternId( + '', + indexPatternList || [], + uiSettings.get('defaultIndex') + ); if (isMounted) { store!.dispatch(updateIndexPattern(newId)); handleIndexPattern(); @@ -93,7 +95,7 @@ export const useIndexPattern = (services: DiscoverViewServices) => { store, toastNotifications, uiSettings, - dataset, + query?.dataset, ]); return indexPattern; 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 5e0fd293b541..b6c13d4982f2 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 @@ -251,8 +251,7 @@ export const useSearch = (services: DiscoverViewServices) => { timefilter.getFetch$(), timefilter.getTimeUpdate$(), timefilter.getAutoRefreshFetch$(), - data.query.queryString.getUpdates$(), - data.query.queryString.getDatasetManager().getUpdates$() + data.query.queryString.getUpdates$() ).pipe(debounceTime(100)); const subscription = fetch$.subscribe(() => { diff --git a/src/plugins/query_enhancements/common/utils.ts b/src/plugins/query_enhancements/common/utils.ts index df300e92a413..208256bd4dd4 100644 --- a/src/plugins/query_enhancements/common/utils.ts +++ b/src/plugins/query_enhancements/common/utils.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { IDataFrame } from 'src/plugins/data/common'; +import { IDataFrame, Query } from 'src/plugins/data/common'; import { Observable, Subscription, from, throwError, timer } from 'rxjs'; import { catchError, concatMap, last, takeWhile, tap } from 'rxjs/operators'; import { FetchDataFrameContext, FetchFunction } from './types'; @@ -133,13 +133,9 @@ export const handleDataFrameError = (response: any) => { } }; -export const fetchDataFrame = ( - context: FetchDataFrameContext, - queryString: string, - df: IDataFrame -) => { +export const fetchDataFrame = (context: FetchDataFrameContext, query: Query, df: IDataFrame) => { const { http, path, signal } = context; - const body = JSON.stringify({ query: { qs: queryString, format: 'jdbc' }, df }); + const body = JSON.stringify({ query: { ...query, format: 'jdbc' }, df }); return from( http.fetch({ method: 'POST', diff --git a/src/plugins/query_enhancements/public/datasets/index.ts b/src/plugins/query_enhancements/public/datasets/index.ts index c6970576a6bc..ddd7bf74995c 100644 --- a/src/plugins/query_enhancements/public/datasets/index.ts +++ b/src/plugins/query_enhancements/public/datasets/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -// export * from './s3_handler'; +export * from './s3_handler'; diff --git a/src/plugins/query_enhancements/public/datasets/s3_handler.ts b/src/plugins/query_enhancements/public/datasets/s3_handler.ts index 5a7d5d59f726..ce18662146bc 100644 --- a/src/plugins/query_enhancements/public/datasets/s3_handler.ts +++ b/src/plugins/query_enhancements/public/datasets/s3_handler.ts @@ -3,146 +3,111 @@ * SPDX-License-Identifier: Apache-2.0 */ -// import { Dataset, DataStructure, DEFAULT_DATA, DataStructureFeatureMeta } from '../../../../common'; -// import { IndexPatternsContract } from '../../../index_patterns'; -// import { DatasetHandlerConfig } from '../types'; +import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; +import { DataStructure, Dataset, DatasetField } from 'src/plugins/data/common'; +import { DatasetTypeConfig } from 'src/plugins/data/public'; -// const S3_ICON = './assets/s3_mark.svg'; +const S3_ICON = 'visTable'; +const S3_ID = 'S3'; -// export const s3HandlerConfig: DatasetHandlerConfig = { -// toDataset: (dataStructure: DataStructure): Dataset => ({ -// id: dataStructure.id, -// title: dataStructure.title, -// type: DEFAULT_DATA.SET_TYPES.S3, -// dataSource: dataStructure.parent -// ? { -// id: dataStructure.parent.id, -// title: dataStructure.parent.title, -// type: dataStructure.parent.type, -// } -// : undefined, -// }), +export const s3TypeConfig: DatasetTypeConfig = { + id: S3_ID, + title: S3_ID, + meta: { + icon: { type: S3_ICON }, + tooltip: 'S3 Data Source', + }, -// toDataStructure: (dataset: Dataset): DataStructure => ({ -// id: dataset.id, -// title: dataset.title, -// type: DEFAULT_DATA.SET_TYPES.S3, -// parent: dataset.dataSource -// ? { -// id: dataset.dataSource.id!, -// title: dataset.dataSource.title, -// type: dataset.dataSource.type, -// } -// : undefined, -// meta: { -// type: DEFAULT_DATA.STRUCTURE_META_TYPES.FEATURE, -// icon: S3_ICON, -// tooltip: 'S3 Data Source', -// } as DataStructureFeatureMeta, -// }), + toDataset: (path: DataStructure[]): Dataset => { + const s3 = path[path.length - 1]; + const dataSource = path.find((ds) => ds.type === S3_ID); -// fetchOptions: async ( -// dataStructure: DataStructure, -// indexPatterns: IndexPatternsContract -// ): Promise => { -// // This is a placeholder implementation. You'll need to implement -// // the actual logic to fetch S3 data structures. -// if (dataStructure.type === DEFAULT_DATA.SOURCE_TYPES.S3) { -// // Fetch connections -// return [ -// { -// id: `${dataStructure.id}::mys3`, -// title: 'mys3', -// type: DEFAULT_DATA.STRUCTURE_TYPES.CONNECTION, -// parent: dataStructure, -// meta: { -// type: DEFAULT_DATA.STRUCTURE_META_TYPES.FEATURE, -// icon: S3_ICON, -// tooltip: 'S3 Connection', -// } as DataStructureFeatureMeta, -// }, -// ]; -// } else if (dataStructure.type === DEFAULT_DATA.STRUCTURE_TYPES.CONNECTION) { -// // Fetch databases -// return [ -// { -// id: `${dataStructure.id}.defaultDb`, -// title: 'defaultDb', -// type: DEFAULT_DATA.STRUCTURE_TYPES.DATABASE, -// parent: dataStructure, -// meta: { -// type: DEFAULT_DATA.STRUCTURE_META_TYPES.FEATURE, -// icon: S3_ICON, -// tooltip: 'S3 Connections', -// } as DataStructureFeatureMeta, -// }, -// ]; -// } else if (dataStructure.type === DEFAULT_DATA.STRUCTURE_TYPES.DATABASE) { -// // Fetch tables -// return [ -// { -// id: `${dataStructure.id}.table1`, -// title: 'table1', -// type: DEFAULT_DATA.STRUCTURE_TYPES.TABLE, -// parent: dataStructure, -// meta: { -// type: DEFAULT_DATA.STRUCTURE_META_TYPES.FEATURE, -// icon: S3_ICON, -// tooltip: 'S3 Table', -// } as DataStructureFeatureMeta, -// }, -// { -// id: `${dataStructure.id}.table2`, -// title: 'table2', -// type: DEFAULT_DATA.STRUCTURE_TYPES.TABLE, -// parent: dataStructure, -// meta: { -// type: DEFAULT_DATA.STRUCTURE_META_TYPES.FEATURE, -// icon: S3_ICON, -// tooltip: 'S3 Table', -// } as DataStructureFeatureMeta, -// }, -// ]; -// } -// return []; -// }, + return { + id: s3.id, + title: s3.title, + type: S3_ID, + dataSource: dataSource + ? { + id: dataSource.id, + title: dataSource.title, + type: dataSource.type, + } + : undefined, + }; + }, -// isLeaf: (dataStructure: DataStructure): boolean => { -// return dataStructure.type === DEFAULT_DATA.STRUCTURE_TYPES.TABLE; -// }, -// }; + fetch: async ( + savedObjects: SavedObjectsClientContract, + path: DataStructure[] + ): Promise => { + const dataStructure = path[path.length - 1]; + switch (dataStructure.type) { + case S3_ID: + return { + ...dataStructure, + columnHeader: 'Connections', + hasNext: true, + children: [ + { + id: `${dataStructure.id}::mys3`, + title: 'mys3', + type: 'CONNECTION', + }, + ], + }; + case 'CONNECTION': + return { + ...dataStructure, + columnHeader: 'Databases', + hasNext: true, + children: [ + { + id: `${dataStructure.id}.defaultDb`, + title: 'defaultDb', + type: 'DATABASE', + }, + ], + }; + case 'DATABASE': + return { + ...dataStructure, + columnHeader: 'Tables', + hasNext: false, + children: [ + { + id: `${dataStructure.id}.table1`, + title: 'table1', + type: 'TABLE', + }, + { + id: `${dataStructure.id}.table2`, + title: 'table2', + type: 'TABLE', + }, + ], + }; + default: + const s3DataSources = await fetchS3DataSources(savedObjects); + return { + ...dataStructure, + columnHeader: 'S3 Data Sources', + hasNext: false, + children: s3DataSources, + }; + } + }, -// export function s3ToDataStructure(dataset: Dataset): DataStructure { -// return { -// id: dataset.id, -// title: dataset.title, -// type: DEFAULT_DATA.SET_TYPES.S3, -// parent: dataset.dataSource -// ? { -// id: dataset.dataSource.id!, -// title: dataset.dataSource.title!, -// type: DEFAULT_DATA.SOURCE_TYPES.S3, -// } -// : undefined, -// meta: { -// type: DEFAULT_DATA.STRUCTURE_META_TYPES.FEATURE, -// icon: S3_ICON, -// tooltip: 'S3 Data Source', -// } as DataStructureFeatureMeta, -// }; -// } + fetchFields: async (dataset: Dataset): Promise => { + // This is a placeholder. You'll need to implement the actual logic to fetch S3 fields. + // For now, we'll return an empty array. + return []; + }, -// export function s3ToDataset(s3: DataStructure): Dataset { -// return { -// id: s3.id, -// title: s3.title, -// type: DEFAULT_DATA.SET_TYPES.S3, -// dataSource: s3.parent -// ? { -// id: s3.parent.id, -// title: s3.parent.title, -// type: s3.parent.type, -// } -// : undefined, -// }; -// } + supportedLanguages: (): string[] => { + return ['sql']; // Assuming S3 only supports SQL queries + }, +}; + +const fetchS3DataSources = async (client: SavedObjectsClientContract): Promise => { + return []; +}; diff --git a/src/plugins/query_enhancements/public/plugin.tsx b/src/plugins/query_enhancements/public/plugin.tsx index 2d69115a622b..d09189eda3b9 100644 --- a/src/plugins/query_enhancements/public/plugin.tsx +++ b/src/plugins/query_enhancements/public/plugin.tsx @@ -16,7 +16,8 @@ import { QueryEnhancementsPluginStart, QueryEnhancementsPluginStartDependencies, } from './types'; -import { UI_SETTINGS } from '../common'; +import { LanguageConfig, Query } from '../../data/public'; +import { s3TypeConfig } from './datasets'; export class QueryEnhancementsPlugin implements @@ -38,19 +39,7 @@ export class QueryEnhancementsPlugin core: CoreSetup, { data }: QueryEnhancementsPluginSetupDependencies ): QueryEnhancementsPluginSetup { - core.uiSettings.getUpdate$().subscribe(({ key, newValue }) => { - if (key === UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED) { - if (newValue) { - core.uiSettings.set(UI_SETTINGS.STATE_STORE_IN_SESSION_STORAGE, true); - } - } - if (key === UI_SETTINGS.STATE_STORE_IN_SESSION_STORAGE) { - if (!newValue) { - core.uiSettings.set(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED, false); - } - } - }); - + const { queryString } = data.query; const pplSearchInterceptor = new PPLSearchInterceptor({ toasts: core.notifications.toasts, http: core.http, @@ -67,55 +56,50 @@ export class QueryEnhancementsPlugin usageCollector: data.search.usageCollector, }); - // TODO: SEAN - register DATASETS HERE - // data.query.dataSetManager.registerDatasetHandler('S3', s3DatasetHandler); - - data.__enhance({ - ui: { - query: { - language: 'PPL', - search: pplSearchInterceptor, - searchBar: { - queryStringInput: { initialValue: 'source=' }, - dateRange: { - initialFrom: moment().subtract(2, 'days').toISOString(), - initialTo: moment().add(2, 'days').toISOString(), - }, - showFilterBar: false, - showDataSetsSelector: true, - showDataSourcesSelector: true, - }, - fields: { - filterable: false, - visualizable: false, - }, - showDocLinks: false, - supportedAppNames: ['discover'], + // Register PPL language + const pplLanguageConfig: LanguageConfig = { + id: 'PPL', + title: 'PPL', + search: pplSearchInterceptor, + getQueryString: (query: Query) => { + return `source = ${query.dataset?.title}`; + }, + searchBar: { + dateRange: { + initialFrom: moment().subtract(2, 'days').toISOString(), + initialTo: moment().add(2, 'days').toISOString(), }, + showFilterBar: false, }, - }); + fields: { + filterable: false, + visualizable: false, + }, + showDocLinks: false, + supportedAppNames: ['discover'], + }; + queryString.getLanguageService().registerLanguage(pplLanguageConfig); - data.__enhance({ - ui: { - query: { - language: 'SQL', - search: sqlSearchInterceptor, - searchBar: { - showDatePicker: false, - showFilterBar: false, - showDataSetsSelector: true, - showDataSourcesSelector: true, - queryStringInput: { initialValue: 'SELECT * FROM LIMIT 10' }, - }, - fields: { - filterable: false, - visualizable: false, - }, - showDocLinks: false, - supportedAppNames: ['discover'], - }, + // Register SQL language + const sqlLanguageConfig: LanguageConfig = { + id: 'SQL', + title: 'SQL', + search: sqlSearchInterceptor, + getQueryString: (query: Query) => { + return `SELECT * FROM ${queryString.getQuery().dataset?.title} LIMIT 10`; }, - }); + searchBar: { + showDatePicker: false, + showFilterBar: false, + }, + fields: { + filterable: false, + visualizable: false, + }, + showDocLinks: false, + supportedAppNames: ['discover'], + }; + queryString.getLanguageService().registerLanguage(sqlLanguageConfig); data.__enhance({ ui: { @@ -123,6 +107,8 @@ export class QueryEnhancementsPlugin }, }); + queryString.getDatasetService().registerType(s3TypeConfig); + return {}; } 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 589fac44b228..85f951fc7515 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 @@ -26,7 +26,7 @@ interface QueryAssistInputProps { export const QueryAssistBar: React.FC = (props) => { const { services } = useOpenSearchDashboards(); - const datasetManager = services.data.query.queryString.getDatasetManager(); + const queryString = services.data.query.queryString; const inputRef = useRef(null); const storage = getStorage(); const persistedLog: PersistedLog = useMemo( @@ -37,17 +37,17 @@ export const QueryAssistBar: React.FC = (props) => { const [callOutType, setCallOutType] = useState(); const dismissCallout = () => setCallOutType(undefined); const [selectedDataset, setSelectedDataset] = useState( - datasetManager.getDataset() + queryString.getQuery().dataset ); const selectedIndex = selectedDataset?.title; const previousQuestionRef = useRef(); useEffect(() => { - const subscription = datasetManager.getUpdates$().subscribe((dataset) => { - setSelectedDataset(dataset); + const subscription = queryString.getUpdates$().subscribe((query) => { + setSelectedDataset(query.dataset); }); return () => subscription.unsubscribe(); - }, [datasetManager]); + }, [queryString]); const onSubmit = async (e: SyntheticEvent) => { e.preventDefault(); @@ -79,6 +79,7 @@ export const QueryAssistBar: React.FC = (props) => { services.data.query.queryString.setQuery({ query: response.query, language: params.language, + dataset: selectedDataset, }); if (response.timeRange) services.data.query.timefilter.timefilter.setTime(response.timeRange); setCallOutType('query_generated'); 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 c717f2b65671..3afb986b259f 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,12 +6,9 @@ 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 { Dataset } from '../../../../data/common'; import { QueryEditorExtensionDependencies } from '../../../../data/public'; import { dataPluginMock } from '../../../../data/public/mocks'; -import { DatasetContract } from '../../../../data/public/query'; import { ConfigSchema } from '../../../common/config'; import { createQueryAssistExtension } from './create_extension'; @@ -24,20 +21,6 @@ const coreSetupMock = coreMock.createSetup({ }); const httpMock = coreSetupMock.http; const dataMock = dataPluginMock.createSetupContract(); -const datasetMock = (dataMock.query.queryString.getDatasetManager() as unknown) as jest.Mocked< - DatasetContract ->; - -const mockDataset = { - id: 'mock-data-set-id', - title: 'mock-title', - dataSource: { - id: 'mock-data-source-id', - }, -} as Dataset; - -datasetMock.getDataset.mockReturnValue(mockDataset); -datasetMock.getUpdates$.mockReturnValue(of(mockDataset)); jest.mock('../components', () => ({ QueryAssistBar: jest.fn(() =>
QueryAssistBar
), 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 3d377f4a5d1f..e1c8d56908df 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 @@ -25,30 +25,27 @@ const getAvailableLanguages$ = ( http: HttpSetup, data: DataPublicPluginSetup ) => - data.query.queryString - .getDatasetManager() - .getUpdates$() - .pipe( - startWith(data.query.queryString.getDatasetManager().getDataset()), - distinctUntilChanged(), - switchMap(async (dataset) => { - // currently query assist tool relies on opensearch API to get index - // mappings, external data source types (e.g. s3) are not supported - if (dataset?.dataSource?.type !== DEFAULT_QUERY.DATASET.DATASOURCE.TYPE) return []; + data.query.queryString.getUpdates$().pipe( + startWith(data.query.queryString.getQuery()), + distinctUntilChanged(), + switchMap(async (query) => { + // currently query assist tool relies on opensearch API to get index + // mappings, external data source types (e.g. s3) are not supported + if (query.dataset?.dataSource?.type !== DEFAULT_QUERY.DATASET.DATASOURCE.TYPE) return []; - const dataSourceId = dataset?.dataSource?.id; - const cached = availableLanguagesByDataSource.get(dataSourceId); - if (cached !== undefined) return cached; - const languages = await http - .get<{ configuredLanguages: string[] }>(API.QUERY_ASSIST.LANGUAGES, { - query: { dataSourceId }, - }) - .then((response) => response.configuredLanguages) - .catch(() => []); - availableLanguagesByDataSource.set(dataSourceId, languages); - return languages; - }) - ); + const dataSourceId = query.dataset?.dataSource?.id; + const cached = availableLanguagesByDataSource.get(dataSourceId); + if (cached !== undefined) return cached; + const languages = await http + .get<{ configuredLanguages: string[] }>(API.QUERY_ASSIST.LANGUAGES, { + query: { dataSourceId }, + }) + .then((response) => response.configuredLanguages) + .catch(() => []); + availableLanguagesByDataSource.set(dataSourceId, languages); + return languages; + }) + ); export const createQueryAssistExtension = ( http: HttpSetup, diff --git a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts index b73b3bfc4893..3a5edd67ca7f 100644 --- a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts +++ b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts @@ -5,16 +5,14 @@ import { trimEnd } from 'lodash'; import { Observable, throwError } from 'rxjs'; -import { catchError, concatMap } from 'rxjs/operators'; +import { catchError } from 'rxjs/operators'; import { DataFrameAggConfig, - getAggConfig, getRawDataFrame, - getRawQueryString, formatTimePickerDate, getUniqueValuesForRawAggs, updateDataFrameMeta, - getRawAggs, + Query, } from '../../../data/common'; import { DataPublicPluginStart, @@ -73,17 +71,17 @@ export class PPLSearchInterceptor extends SearchInterceptor { }; const getAggQsFn = ({ - qs, + query, aggConfig, timeField, timeFilter, }: { - qs: string; + query: Query; aggConfig: DataFrameAggConfig; timeField: any; timeFilter: string; }) => { - return removeKeyword(`${qs} ${getAggString(timeField, aggConfig)} ${timeFilter}`); + return removeKeyword(`${query.query} ${getAggString(timeField, aggConfig)} ${timeFilter}`); }; const getAggString = (timeField: any, aggsConfig?: DataFrameAggConfig) => { @@ -138,80 +136,24 @@ export class PPLSearchInterceptor extends SearchInterceptor { }; const dataFrame = getRawDataFrame(searchRequest); - - let queryString = dataFrame.meta?.queryConfig?.qs ?? getRawQueryString(searchRequest) ?? ''; - - dataFrame.meta = { - ...dataFrame.meta, - aggConfig: { - ...dataFrame.meta.aggConfig, - ...(getRawAggs(searchRequest) && - this.aggsService.types.get.bind(this) && - getAggConfig(searchRequest, {}, this.aggsService.types.get.bind(this))), - }, - queryConfig: { - ...dataFrame.meta.queryConfig, - ...(this.queryService.queryString.getDatasetManager().getDataset() && { - dataSourceId: this.queryService.queryString.getDatasetManager().getDataset()?.dataSource - ?.id, - dataSourceName: this.queryService.queryString.getDatasetManager().getDataset()?.dataSource - ?.title, - timeFieldName: this.queryService.queryString.getDatasetManager().getDataset() - ?.timeFieldName, - }), - }, - }; - - if (!dataFrame.schema) { - return fetchDataFrame(dfContext, queryString, dataFrame).pipe( - concatMap((response) => { - const df = response.body; - if (df.error) { - const jsError = new Error(df.error.response); - return throwError(jsError); - } - const timeField = dataFrame.meta?.queryConfig?.timeFieldName; - const aggConfig = dataFrame.meta?.aggConfig; - if (timeField && aggConfig) { - const timeFilter = getTimeFilter(timeField); - const newQuery = insertTimeFilter(queryString, timeFilter); - updateDataFrameMeta({ - dataFrame: df, - qs: newQuery, - aggConfig, - timeField, - timeFilter, - getAggQsFn: getAggQsFn.bind(this), - }); - return fetchDataFrame(dfContext, newQuery, df); - } - return fetchDataFrame(dfContext, queryString, df); - }), - catchError((error) => { - return throwError(error); - }) - ); - } - - if (dataFrame.schema) { - const timeField = dataFrame.meta?.queryConfig?.timeFieldName; - const aggConfig = dataFrame.meta?.aggConfig; - if (timeField && aggConfig) { - const timeFilter = getTimeFilter(timeField); - const newQuery = insertTimeFilter(queryString, timeFilter); - updateDataFrameMeta({ - dataFrame, - qs: newQuery, - aggConfig: dataFrame.meta?.aggConfig, - timeField, - timeFilter, - getAggQsFn: getAggQsFn.bind(this), - }); - queryString += timeFilter; - } + const query = this.queryService.queryString.getQuery(); + const timeField = query.dataset?.timeFieldName; + const aggConfig = dataFrame?.meta?.aggConfig; + if (timeField && aggConfig) { + const timeFilter = getTimeFilter(timeField); + const newQuery = insertTimeFilter(query.query as string, timeFilter); + updateDataFrameMeta({ + dataFrame, + query: { ...query, query: newQuery }, + aggConfig: dataFrame?.meta?.aggConfig, + timeField, + timeFilter, + getAggQsFn: getAggQsFn.bind(this), + }); + query.query += timeFilter; } - return fetchDataFrame(dfContext, queryString, dataFrame).pipe( + return fetchDataFrame(dfContext, query, dataFrame).pipe( catchError((error) => { return throwError(error); }) diff --git a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts index 040fde48e92b..3aba5cca7fad 100644 --- a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts +++ b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts @@ -7,7 +7,7 @@ import { trimEnd } from 'lodash'; import { Observable, throwError } from 'rxjs'; import { i18n } from '@osd/i18n'; import { concatMap, map } from 'rxjs/operators'; -import { DATA_FRAME_TYPES, getRawDataFrame, getRawQueryString } from '../../../data/common'; +import { DATA_FRAME_TYPES, getRawDataFrame } from '../../../data/common'; import { DataPublicPluginStart, IOpenSearchDashboardsSearchRequest, @@ -55,40 +55,7 @@ export class SQLSearchInterceptor extends SearchInterceptor { const dataFrame = getRawDataFrame(searchRequest); - const queryString = dataFrame.meta?.queryConfig?.qs ?? getRawQueryString(searchRequest) ?? ''; - - dataFrame.meta = { - ...dataFrame.meta, - queryConfig: { - ...dataFrame.meta.queryConfig, - queryConfig: { - ...dataFrame.meta.queryConfig, - ...(this.queryService.queryString.getDatasetManager().getDataset() && { - dataSourceId: this.queryService.queryString.getDatasetManager().getDataset()?.dataSource - ?.id, - dataSourceName: this.queryService.queryString.getDatasetManager().getDataset() - ?.dataSource?.title, - timeFieldName: this.queryService.queryString.getDatasetManager().getDataset() - ?.timeFieldName, - }), - }, - }, - }; - - if (!dataFrame.schema) { - return fetchDataFrame(dfContext, queryString, dataFrame).pipe( - concatMap((response) => { - const df = response.body; - if (df.error) { - const jsError = new Error(df.error.response); - return throwError(jsError); - } - return fetchDataFrame(dfContext, queryString, df); - }) - ); - } - - return fetchDataFrame(dfContext, queryString, dataFrame); + return fetchDataFrame(dfContext, this.queryService.queryString.getQuery(), dataFrame); } protected runSearchAsync( @@ -105,24 +72,19 @@ export class SQLSearchInterceptor extends SearchInterceptor { }; const dataFrame = getRawDataFrame(searchRequest); - if (!dataFrame) { - return throwError(this.handleSearchError('DataFrame is not defined', request, signal!)); - } + const query = this.queryService.queryString.getQuery(); - const queryString = getRawQueryString(searchRequest) ?? ''; - const dataSourceRef = this.queryService.queryString.getDatasetManager().getDataset() + const dataSourceRef = query.dataset ? { - dataSourceId: this.queryService.queryString.getDatasetManager().getDataset()?.dataSource - ?.id, - dataSourceName: this.queryService.queryString.getDatasetManager().getDataset()?.dataSource - ?.title, + dataSourceId: query.dataset.dataSource?.id, + dataSourceName: query.dataset.dataSource?.title, } : {}; dataFrame.meta = { - ...dataFrame.meta, + ...dataFrame?.meta, queryConfig: { - ...dataFrame.meta.queryConfig, + ...dataFrame?.meta.queryConfig, ...dataSourceRef, }, sessionId: dataSourceRef @@ -163,7 +125,7 @@ export class SQLSearchInterceptor extends SearchInterceptor { defaultMessage: 'Starting query job...', }), }); - return fetchDataFrame(dfContext, queryString, dataFrame).pipe( + return fetchDataFrame(dfContext, query, dataFrame).pipe( concatMap((jobResponse) => { const df = jobResponse.body; if (dataSourceRef?.dataSourceName && df?.meta?.sessionId) { @@ -190,8 +152,8 @@ export class SQLSearchInterceptor extends SearchInterceptor { } public search(request: IOpenSearchDashboardsSearchRequest, options: ISearchOptions) { - const dataSet = this.queryService.queryString.getDatasetManager().getDataset(); - if (dataSet?.type === 'TEMPORARY_ASYNC') { + const dataset = this.queryService.queryString.getQuery().dataset; + if (dataset?.type === 'S3') { return this.runSearchAsync(request, options.abortSignal, SEARCH_STRATEGY.SQL_ASYNC); } return this.runSearch(request, options.abortSignal, SEARCH_STRATEGY.SQL); diff --git a/src/plugins/query_enhancements/server/routes/index.ts b/src/plugins/query_enhancements/server/routes/index.ts index 3c23db3c87b9..a17c4f2294bd 100644 --- a/src/plugins/query_enhancements/server/routes/index.ts +++ b/src/plugins/query_enhancements/server/routes/index.ts @@ -32,7 +32,9 @@ function defineRoute( validate: { body: schema.object({ query: schema.object({ - qs: schema.string(), + query: schema.string(), + language: schema.string(), + dataset: schema.nullable(schema.object({}, { unknowns: 'allow' })), format: schema.string(), }), df: schema.nullable(schema.object({}, { unknowns: 'allow' })), diff --git a/src/plugins/query_enhancements/server/search/ppl_search_strategy.ts b/src/plugins/query_enhancements/server/search/ppl_search_strategy.ts index f0b01cd51da4..f426b2d9980f 100644 --- a/src/plugins/query_enhancements/server/search/ppl_search_strategy.ts +++ b/src/plugins/query_enhancements/server/search/ppl_search_strategy.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { first } from 'rxjs/operators'; import { SharedGlobalConfig, Logger, ILegacyClusterClient } from 'opensearch-dashboards/server'; import { Observable } from 'rxjs'; import { ISearchStrategy, getDefaultSearchParams, SearchUsage } from '../../../data/server'; @@ -13,6 +12,7 @@ import { IDataFrameResponse, IDataFrameWithAggs, IOpenSearchDashboardsSearchRequest, + Query, createDataFrame, } from '../../../data/common'; import { getFields } from '../../common/utils'; @@ -45,8 +45,6 @@ export const pplSearchStrategyProvider = ( const source = pipeMap.get('source'); - const searchQuery = query; - const filters = pipeMap.get('where'); const stats = pipeMap.get('stats'); @@ -55,15 +53,12 @@ export const pplSearchStrategyProvider = ( : undefined; return { - map: pipeMap, - search: searchQuery, aggs: aggsQuery, }; }; return { search: async (context, request: any, options) => { - const config = await config$.pipe(first()).toPromise(); const uiSettingsClient = await context.core.uiSettings.client; const { dataFrameHydrationStrategy, ...defaultParams } = await getDefaultSearchParams( @@ -71,14 +66,9 @@ export const pplSearchStrategyProvider = ( ); try { - const requestParams = parseRequest(request.body.query.qs); - const source = requestParams?.map.get('source'); - const { schema, meta } = request.body.df; - - request.body.query = - !schema || dataFrameHydrationStrategy === 'perQuery' - ? `source=${source} | head` - : requestParams.search; + const query: Query = request.body.query; + const { df } = request.body; + const rawResponse: any = await pplFacet.describeQuery(context, request); if (!rawResponse.success) { @@ -90,9 +80,9 @@ export const pplSearchStrategyProvider = ( } const dataFrame = createDataFrame({ - name: source, - schema: schema ?? rawResponse.data.schema, - meta, + name: query.dataset?.id, + schema: rawResponse.data.schema, + meta: df?.meta, fields: getFields(rawResponse), }); @@ -100,11 +90,10 @@ export const pplSearchStrategyProvider = ( if (usage) usage.trackSuccess(rawResponse.took); - if (dataFrame.meta?.aggsQs) { - for (const [key, aggQueryString] of Object.entries(dataFrame.meta.aggsQs)) { + if (dataFrame?.meta?.aggsQs) { + for (const [key, aggQueryString] of Object.entries(dataFrame?.meta?.aggsQs)) { const aggRequest = parseRequest(aggQueryString as string); - const query = aggRequest.aggs; - request.body.query = query; + request.body.query = aggRequest.aggs; const rawAggs: any = await pplFacet.describeQuery(context, request); (dataFrame as IDataFrameWithAggs).aggs = {}; (dataFrame as IDataFrameWithAggs).aggs[key] = rawAggs.data.datarows?.map((hit: any) => { diff --git a/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts b/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts index 3c3802eae261..c32159e5b968 100644 --- a/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts +++ b/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts @@ -12,6 +12,7 @@ import { IDataFrameResponse, IOpenSearchDashboardsSearchRequest, PartialDataFrame, + Query, createDataFrame, } from '../../../data/common'; import { Facet } from '../utils'; @@ -38,12 +39,13 @@ export const sqlAsyncSearchStrategyProvider = ( return { search: async (context, request: any, options) => { try { + const query: Query = request?.body?.query; // Create job: this should return a queryId and sessionId - if (request?.body?.query?.qs) { + if (query) { const df = request.body?.df; request.body = { - query: request.body.query.qs, - datasource: df?.meta?.queryConfig?.dataSourceName, + query: query.query, + datasource: query.dataset?.dataSource?.title, lang: SEARCH_STRATEGY.SQL, sessionId: df?.meta?.sessionId, }; @@ -60,13 +62,13 @@ export const sqlAsyncSearchStrategyProvider = ( const sessionId = rawResponse.data?.sessionId; const partial: PartialDataFrame = { - ...request.body.df, + ...df, fields: rawResponse?.data?.schema || [], }; const dataFrame = createDataFrame(partial); dataFrame.meta = { - ...dataFrame.meta, - query: request.body.query, + ...dataFrame?.meta, + query: query.query, queryId, sessionId, }; @@ -93,7 +95,7 @@ export const sqlAsyncSearchStrategyProvider = ( dataFrame.size = asyncResponse?.data?.datarows?.length || 0; dataFrame.meta = { - ...dataFrame.meta, + ...dataFrame?.meta, status, queryId, error: status === 'FAILED' && asyncResponse.data?.error, diff --git a/src/plugins/query_enhancements/server/search/sql_search_strategy.ts b/src/plugins/query_enhancements/server/search/sql_search_strategy.ts index b2f6af9ca144..914391471baa 100644 --- a/src/plugins/query_enhancements/server/search/sql_search_strategy.ts +++ b/src/plugins/query_enhancements/server/search/sql_search_strategy.ts @@ -12,6 +12,7 @@ import { IDataFrameResponse, IOpenSearchDashboardsSearchRequest, PartialDataFrame, + Query, createDataFrame, } from '../../../data/common'; import { Facet } from '../utils'; @@ -27,7 +28,6 @@ export const sqlSearchStrategyProvider = ( return { search: async (context, request: any, _options) => { try { - request.body.query = request.body.query.qs; const rawResponse: any = await sqlFacet.describeQuery(context, request); if (!rawResponse.success) { diff --git a/src/plugins/query_enhancements/server/utils/facet.ts b/src/plugins/query_enhancements/server/utils/facet.ts index e930ce612d9b..287e8b6e86ec 100644 --- a/src/plugins/query_enhancements/server/utils/facet.ts +++ b/src/plugins/query_enhancements/server/utils/facet.ts @@ -6,6 +6,7 @@ import { Logger } from 'opensearch-dashboards/server'; import { FacetResponse, IPPLEventsDataSource, IPPLVisualizationDataSource } from '../types'; import { shimSchemaRow, shimStats } from '.'; +import { Query } from '../../../data/common'; export interface FacetProps { client: any; @@ -36,12 +37,13 @@ export class Facet { endpoint: string ): Promise => { try { - const { format, df, dataSourceId, ...query } = request.body; + const query: Query = request.body.query; + const { format, df } = request.body; const params = { body: { ...query }, ...(format !== 'jdbc' && { format }), }; - const clientId = dataSourceId ?? df?.meta?.queryConfig?.dataSourceId; + const clientId = query.dataset?.dataSource?.id ?? df?.meta?.queryConfig?.dataSourceId; const client = clientId ? context.dataSource.opensearch.legacy.getClient(clientId).callAPI : this.defaultClient.asScoped(request).callAsCurrentUser;