diff --git a/package.json b/package.json index 59a54a182773..3392b44d0072 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", + "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", "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/plugins/data/public/antlr/opensearch_sql/code_completion.ts b/src/plugins/data/public/antlr/opensearch_sql/code_completion.ts index 8c24f44c7c39..31a9358e2c2d 100644 --- a/src/plugins/data/public/antlr/opensearch_sql/code_completion.ts +++ b/src/plugins/data/public/antlr/opensearch_sql/code_completion.ts @@ -46,7 +46,7 @@ export const getSuggestions = async ({ services, }: QuerySuggestionGetFnArgs): Promise => { const { api } = services.uiSettings; - const dataSetManager = services.data.query.dataSet; + const dataSetManager = services.data.query.dataSetManager; const suggestions = getOpenSearchSqlAutoCompleteSuggestions(query, { line: position?.lineNumber || selectionStart, column: position?.column || selectionEnd, diff --git a/src/plugins/data/public/query/dataset_manager/dataset_manager.ts b/src/plugins/data/public/query/dataset_manager/dataset_manager.ts index ba0ade407b08..3aff5b927597 100644 --- a/src/plugins/data/public/query/dataset_manager/dataset_manager.ts +++ b/src/plugins/data/public/query/dataset_manager/dataset_manager.ts @@ -7,6 +7,7 @@ import { BehaviorSubject } from 'rxjs'; import { CoreStart } from 'opensearch-dashboards/public'; import { skip } from 'rxjs/operators'; import { + IndexPattern, SIMPLE_DATA_SET_TYPES, SimpleDataSet, SimpleDataSource, @@ -24,9 +25,33 @@ export class DataSetManager { } public init = async (indexPatterns: IndexPatternsContract) => { + if (!this.uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED)) return; this.indexPatterns = indexPatterns; this.defaultDataSet = await this.fetchDefaultDataSet(); - return this.defaultDataSet; + }; + + public initWithIndexPattern = (indexPattern: IndexPattern | null) => { + if (!this.uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED)) return; + if (!indexPattern || !indexPattern.id) { + return undefined; + } + + this.defaultDataSet = { + id: indexPattern.id, + title: indexPattern.title, + type: SIMPLE_DATA_SET_TYPES.INDEX_PATTERN, + timeFieldName: indexPattern.timeFieldName, + fields: indexPattern.fields, + ...(indexPattern.dataSourceRef + ? { + dataSourceRef: { + id: indexPattern.dataSourceRef?.id, + name: indexPattern.dataSourceRef?.name, + type: indexPattern.dataSourceRef?.type, + } as SimpleDataSource, + } + : {}), + }; }; public getUpdates$ = () => { @@ -43,6 +68,13 @@ export class DataSetManager { */ public setDataSet = (dataSet: SimpleDataSet | undefined) => { if (!this.uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED)) return; + + // if (dataSet) { + // const { fields, ...dataSetWithoutFields } = dataSet; + // this.dataSet$.next(dataSetWithoutFields); + // } else { + // this.dataSet$.next(undefined); + // } this.dataSet$.next(dataSet); }; @@ -57,11 +89,7 @@ export class DataSetManager { } const indexPattern = await this.indexPatterns?.get(defaultIndexPatternId); - if (!indexPattern) { - return undefined; - } - - if (!indexPattern.id) { + if (!indexPattern || !indexPattern.id) { return undefined; } diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index 1cf116a00b04..5c9c8b309367 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -37,7 +37,7 @@ 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 { DataSetManager } from './dataset_manager'; +import { DataSetContract, DataSetManager } from './dataset_manager'; import { buildOpenSearchQuery, getOpenSearchQueryConfig, IndexPatternsService } from '../../common'; import { getUiSettings } from '../services'; import { IndexPattern } from '..'; @@ -63,7 +63,7 @@ export class QueryService { filterManager!: FilterManager; timefilter!: TimefilterSetup; queryStringManager!: QueryStringContract; - dataSetManager!: DataSetManager; + dataSetManager!: DataSetContract; state$!: ReturnType; @@ -83,14 +83,14 @@ export class QueryService { filterManager: this.filterManager, timefilter: this.timefilter, queryString: this.queryStringManager, - dataSet: this.dataSetManager, + dataSetManager: this.dataSetManager, }).pipe(share()); return { filterManager: this.filterManager, timefilter: this.timefilter, queryString: this.queryStringManager, - dataSet: this.dataSetManager, + dataSetManager: this.dataSetManager, state$: this.state$, }; } @@ -109,7 +109,7 @@ export class QueryService { }), filterManager: this.filterManager, queryString: this.queryStringManager, - dataSet: this.dataSetManager, + dataSetManager: this.dataSetManager, savedQueries: createSavedQueryService(savedObjectsClient), state$: this.state$, timefilter: this.timefilter, diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index d8a5fcb06142..fd73c589371e 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -90,6 +90,8 @@ setupMock.uiSettings.get.mockImplementation((key: string) => { return { from: 'now-15m', to: 'now' }; case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: return { pause: false, value: 0 }; + case UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED: + return false; default: throw new Error(`sync_query test: not mocked uiSetting: ${key}`); } diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts index 71ddb373e156..55fe08fa9106 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts @@ -56,13 +56,13 @@ import { validateTimeRange } from '../timefilter'; */ export const connectStorageToQueryState = async ( { - dataSet, + dataSetManager, filterManager, queryString, state$, }: Pick< QueryStart | QuerySetup, - 'timefilter' | 'filterManager' | 'queryString' | 'dataSet' | 'state$' + 'timefilter' | 'filterManager' | 'queryString' | 'dataSetManager' | 'state$' >, OsdUrlStateStorage: IOsdUrlStateStorage, syncConfig: { @@ -89,7 +89,7 @@ export const connectStorageToQueryState = async ( filters: filterManager.getAppFilters(), ...(uiSettings && uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED) && { - dataSet: dataSet.getDataSet(), + dataSet: dataSetManager.getDataSet(), }), }; @@ -106,13 +106,16 @@ export const connectStorageToQueryState = async ( } } - if (syncConfig.dataSet && !_.isEqual(initialStateFromURL.dataSet, dataSet.getDataSet())) { + if ( + syncConfig.dataSet && + !_.isEqual(initialStateFromURL.dataSet, dataSetManager.getDataSet()) + ) { if (initialStateFromURL.dataSet) { - dataSet.setDataSet(_.cloneDeep(initialStateFromURL.dataSet)); + dataSetManager.setDataSet(_.cloneDeep(initialStateFromURL.dataSet)); } else { - const defaultDataSet = await dataSet.getDefaultDataSet(); + const defaultDataSet = await dataSetManager.getDefaultDataSet(); if (defaultDataSet) { - dataSet.setDataSet(defaultDataSet); + dataSetManager.setDataSet(defaultDataSet); } } } @@ -150,7 +153,7 @@ export const connectStorageToQueryState = async ( } if (syncConfig.dataSet && changes.dataSet) { - newState.dataSet = dataSet.getDataSet(); + newState.dataSet = dataSetManager.getDataSet(); } return newState; @@ -182,11 +185,11 @@ export const connectToQueryState = ( timefilter: { timefilter }, filterManager, queryString, - dataSet, + dataSetManager, state$, }: Pick< QueryStart | QuerySetup, - 'timefilter' | 'filterManager' | 'dataSet' | 'queryString' | 'state$' + 'timefilter' | 'filterManager' | 'dataSetManager' | 'queryString' | 'state$' >, stateContainer: BaseStateContainer, syncConfig: { @@ -278,7 +281,7 @@ export const connectToQueryState = ( } if (syncConfig.dataSet && !initialState.dataSet) { - initialState.dataSet = dataSet.getDefaultDataSet(); + initialState.dataSet = dataSetManager.getDefaultDataSet(); initialDirty = true; } @@ -320,7 +323,7 @@ export const connectToQueryState = ( } } if (syncConfig.dataSet && changes.dataSet) { - newState.dataSet = dataSet.getDataSet(); + newState.dataSet = dataSetManager.getDataSet(); } return newState; }) @@ -382,14 +385,14 @@ export const connectToQueryState = ( } if (syncConfig.dataSet) { - const currentDataSet = dataSet.getDataSet(); + const currentDataSet = dataSetManager.getDataSet(); if (!_.isEqual(state.dataSet, currentDataSet)) { if (state.dataSet) { - dataSet.setDataSet(state.dataSet); + dataSetManager.setDataSet(state.dataSet); } else { - const defaultDataSet = await dataSet.getDefaultDataSet(); + const defaultDataSet = dataSetManager.getDefaultDataSet(); if (defaultDataSet) { - dataSet.setDataSet(defaultDataSet); + dataSetManager.setDataSet(defaultDataSet); stateContainer.set({ ...stateContainer.get(), dataSet: defaultDataSet }); } } 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 440ea836383e..a77b3f159e25 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 @@ -42,12 +42,12 @@ export function createQueryStateObservable({ timefilter: { timefilter }, filterManager, queryString, - dataSet, + dataSetManager, }: { timefilter: TimefilterSetup; filterManager: FilterManager; queryString: QueryStringContract; - dataSet: DataSetContract; + dataSetManager: DataSetContract; }): Observable<{ changes: QueryStateChange; state: QueryState }> { return new Observable((subscriber) => { const state = createStateContainer({ @@ -55,7 +55,7 @@ export function createQueryStateObservable({ refreshInterval: timefilter.getRefreshInterval(), filters: filterManager.getFilters(), query: queryString.getQuery(), - dataSet: dataSet.getDataSet(), + dataSet: dataSetManager.getDataSet(), }); let currentChange: QueryStateChange = {}; @@ -64,9 +64,9 @@ export function createQueryStateObservable({ currentChange.query = true; state.set({ ...state.get(), query: queryString.getQuery() }); }), - dataSet.getUpdates$().subscribe(() => { + dataSetManager.getUpdates$().subscribe(() => { currentChange.dataSet = true; - state.set({ ...state.get(), dataSet: dataSet.getDataSet() }); + state.set({ ...state.get(), dataSet: dataSetManager.getDataSet() }); }), timefilter.getTimeUpdate$().subscribe(() => { currentChange.time = true; diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts index 02a60ea64852..94c2ccc9a350 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts @@ -60,6 +60,8 @@ setupMock.uiSettings.get.mockImplementation((key: string) => { return 'kuery'; case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: return { pause: false, value: 0 }; + case UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED: + return false; default: throw new Error(`sync_query test: not mocked uiSetting: ${key}`); } diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.ts index 5280af25ae68..f34c0912200c 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.ts @@ -50,7 +50,7 @@ const GLOBAL_STATE_STORAGE_KEY = '_g'; export const syncQueryStateWithUrl = ( query: Pick< QueryStart | QuerySetup, - 'filterManager' | 'timefilter' | 'queryString' | 'dataSet' | 'state$' + 'filterManager' | 'timefilter' | 'queryString' | 'dataSetManager' | 'state$' >, osdUrlStateStorage: IOsdUrlStateStorage, uiSettings?: CoreStart['uiSettings'] @@ -58,7 +58,7 @@ export const syncQueryStateWithUrl = ( const { timefilter: { timefilter }, filterManager, - dataSet, + dataSetManager, } = query; const defaultState: QueryState = { time: timefilter.getTime(), @@ -66,7 +66,7 @@ export const syncQueryStateWithUrl = ( filters: filterManager.getGlobalFilters(), ...(uiSettings && uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED) && { - dataSet: dataSet.getDataSet(), + dataSet: dataSetManager.getDataSet(), }), }; diff --git a/src/plugins/data/public/ui/dataset_navigator/_dataset_navigator.scss b/src/plugins/data/public/ui/dataset_navigator/_dataset_navigator.scss index 82ff0e91e34e..144a2ae3edad 100644 --- a/src/plugins/data/public/ui/dataset_navigator/_dataset_navigator.scss +++ b/src/plugins/data/public/ui/dataset_navigator/_dataset_navigator.scss @@ -6,16 +6,42 @@ .dataSetNavigator { padding: $euiSizeXS; color: $euiColorPrimaryText; + max-height: 60vh; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: $euiColorMediumShade $euiColorLightestShade; &__icon { margin-right: 4px; } -} -.dataSetNavigatorFormWrapper { - padding: $euiSizeS; + &__loading { + padding: $euiSizeS; + } + + &:not(:hover)::-webkit-scrollbar { + width: 0; + background: transparent; + } + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: $euiColorLightestShade; + } + + &::-webkit-scrollbar-thumb { + background-color: $euiColorMediumShade; + border-radius: 4px; + } + + &__menu { + width: 365px; + } } -.dataSetNavigator__loading { +.dataSetNavigatorFormWrapper { padding: $euiSizeS; } diff --git a/src/plugins/data/public/ui/dataset_navigator/create_dataset_navigator.tsx b/src/plugins/data/public/ui/dataset_navigator/create_dataset_navigator.tsx index c1ab4b3f846b..94674ab2bcff 100644 --- a/src/plugins/data/public/ui/dataset_navigator/create_dataset_navigator.tsx +++ b/src/plugins/data/public/ui/dataset_navigator/create_dataset_navigator.tsx @@ -12,7 +12,7 @@ import { DataSetContract } from '../../query'; export function createDataSetNavigator( savedObjectsClient: SavedObjectsClientContract, http: HttpStart, - dataSet: DataSetContract + dataSetManager: DataSetContract ) { // Return a function that takes props, omitting the dependencies from the props type return (props: Omit) => ( @@ -20,7 +20,7 @@ export function createDataSetNavigator( {...props} savedObjectsClient={savedObjectsClient} http={http} - dataSet={dataSet} + dataSetManager={dataSetManager} /> ); } diff --git a/src/plugins/data/public/ui/dataset_navigator/dataset_navigator.tsx b/src/plugins/data/public/ui/dataset_navigator/dataset_navigator.tsx index 854089fa1efe..cc0269c741ee 100644 --- a/src/plugins/data/public/ui/dataset_navigator/dataset_navigator.tsx +++ b/src/plugins/data/public/ui/dataset_navigator/dataset_navigator.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiButton, EuiButtonEmpty, @@ -18,7 +18,6 @@ import { EuiToolTip, } from '@elastic/eui'; import { HttpStart, SavedObjectsClientContract } from 'opensearch-dashboards/public'; -import _ from 'lodash'; import { i18n } from '@osd/i18n'; import { SIMPLE_DATA_SET_TYPES, @@ -54,52 +53,38 @@ import { DataSetContract } from '../../query'; export interface DataSetNavigatorProps { savedObjectsClient?: SavedObjectsClientContract; http?: HttpStart; - dataSet?: DataSetContract; -} - -interface DataSetNavigatorState { - isMounted: boolean; - isOpen: boolean; - isLoading: boolean; - isExternalDataSourcesEnabled: boolean; - indexPatterns: any[]; - dataSources: SimpleDataSource[]; - externalDataSources: SimpleDataSource[]; - currentDataSourceRef?: SimpleDataSource; - currentDataSet?: SimpleDataSet; - cachedDatabases: SimpleObject[]; - cachedTables: SimpleObject[]; + dataSetManager?: DataSetContract; } interface SelectedDataSetState extends SimpleDataSet { database?: SimpleObject | undefined; } -export const DataSetNavigator = (props: DataSetNavigatorProps) => { - const { savedObjectsClient, http, dataSet: dataSetManager } = props; +export const DataSetNavigator: React.FC = ({ + savedObjectsClient, + http, + dataSetManager: initialDataSet, +}) => { const searchService = getSearchService(); const queryService = getQueryService(); const uiService = getUiService(); const indexPatternsService = getIndexPatterns(); const notifications = getNotifications(); - const { dataSet } = useDataSetManager({ dataSetManager: dataSetManager! }); - - const [navigatorState, setNavigatorState] = useState({ - isOpen: false, - isLoading: false, - isMounted: false, - isExternalDataSourcesEnabled: false, - dataSources: [], - externalDataSources: [], - currentDataSourceRef: undefined, - currentDataSet: undefined, - indexPatterns: [], - cachedDatabases: [], - cachedTables: [], - }); - - const [selectedDataSetState, setSelectedDataSetState] = useState(); + const { dataSet } = useDataSetManager({ dataSetManager: initialDataSet! }); + + const isInitialized = useRef(false); + const [isOpen, setIsOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isExternalDataSourcesEnabled, setIsExternalDataSourcesEnabled] = useState(false); + const [dataSources, setDataSources] = useState([]); + const [externalDataSources, setExternalDataSources] = useState([]); + const [indexPatterns, setIndexPatterns] = useState([]); + const [cachedDatabases, setCachedDatabases] = useState([]); + const [cachedTables, setCachedTables] = useState([]); + const [selectedDataSetState, setSelectedDataSetState] = useState< + SelectedDataSetState | undefined + >(undefined); const { loadStatus: dataSourcesLoadStatus, @@ -114,58 +99,32 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { notifications ); - const onClick = () => { - setNavigatorState((prevState) => ({ - ...prevState, - isOpen: !prevState.isOpen, - })); - }; - - const isLoading = (loading: boolean) => { - setNavigatorState((prevState) => ({ - ...prevState, - isLoading: loading, - })); - }; - - const closePopover = () => { - setNavigatorState((prevState) => ({ - ...prevState, - isOpen: false, - externalDataSources: [], - currentDataSet: undefined, - currentDataSourceRef: undefined, - cachedDatabases: [], - cachedTables: [], - })); - }; - - const onRefresh = () => { - if (!isCatalogCacheFetching(dataSourcesLoadStatus) && navigatorState.dataSources.length > 0) { - startLoadingDataSources(navigatorState.dataSources.map((dataSource) => dataSource.id)); + const togglePopover = () => setIsOpen((prev) => !prev); + const closePopover = () => setIsOpen(false); + + const onRefresh = useCallback(() => { + if (!isCatalogCacheFetching(dataSourcesLoadStatus) && dataSources.length > 0) { + startLoadingDataSources(dataSources.map((dataSource) => dataSource.id)); } - }; + }, [dataSourcesLoadStatus, dataSources, startLoadingDataSources]); const handleSelectedDataSet = useCallback( async (ds?: SimpleDataSet) => { - const selectedDataSet = ds ?? navigatorState.currentDataSet; + const selectedDataSet = ds ?? selectedDataSetState; if (!selectedDataSet || !selectedDataSet.id) return; const language = uiService.Settings.getUserQueryLanguage(); const queryEnhancements = uiService.Settings.getQueryEnhancements(language); const initialInput = queryEnhancements?.searchBar?.queryStringInput?.initialValue; - // Update query const query = initialInput ? initialInput.replace('', selectedDataSet.title!) : ''; uiService.Settings.setUserQueryString(query); queryService.queryString.setQuery({ query, language }); - // Update dataset - queryService.dataSet.setDataSet(selectedDataSet); + queryService.dataSetManager.setDataSet(selectedDataSet); - // Add to recent datasets CatalogCacheManager.addRecentDataSet({ id: selectedDataSet.id, title: selectedDataSet.title ?? selectedDataSet.id!, @@ -174,8 +133,7 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { type: selectedDataSet.type, }); - // Update data set manager - dataSetManager!.setDataSet({ + setSelectedDataSetState({ id: selectedDataSet.id, title: selectedDataSet.title, ...(selectedDataSet.dataSourceRef && { @@ -191,115 +149,76 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { closePopover(); }, - [ - dataSetManager, - navigatorState.currentDataSet, - queryService.dataSet, - queryService.queryString, - uiService.Settings, - ] + [queryService, setSelectedDataSetState, selectedDataSetState, uiService.Settings] ); useEffect(() => { - setNavigatorState((prevState) => ({ ...prevState, isMounted: true, isLoading: true })); - Promise.all([ - dataSetManager?.init(indexPatternsService), - fetchIndexPatterns(savedObjectsClient!, ''), - fetchDataSources(savedObjectsClient!), - fetchIfExternalDataSourcesEnabled(http!), - ]) - .then(([defaultDataSet, indexPatterns, dataSources, isExternalDataSourcesEnabled]) => { - if (!navigatorState.isMounted) return; - setNavigatorState((prevState) => ({ - ...prevState, - isExternalDataSourcesEnabled, - indexPatterns, - dataSources, - })); - - const selectedPattern = dataSet ?? defaultDataSet; + const initializeData = async () => { + if (isInitialized.current) return; - if (selectedPattern) { + setIsLoading(true); + try { + const [ + fetchedIndexPatterns, + fetchedDataSources, + fetchedIsExternalDataSourcesEnabled, + ] = await Promise.all([ + fetchIndexPatterns(savedObjectsClient!, ''), + fetchDataSources(savedObjectsClient!), + fetchIfExternalDataSourcesEnabled(http!), + ]); + + setIndexPatterns(fetchedIndexPatterns); + setDataSources(fetchedDataSources); + setIsExternalDataSourcesEnabled(fetchedIsExternalDataSourcesEnabled); + if (dataSet) { setSelectedDataSetState({ - id: selectedPattern.id, - title: selectedPattern.title, - type: selectedPattern.type, - timeFieldName: selectedPattern.timeFieldName, - fields: selectedPattern.fields, - ...(selectedPattern.dataSourceRef + id: dataSet.id, + title: dataSet.title, + type: dataSet.type, + timeFieldName: dataSet.timeFieldName, + fields: dataSet.fields, + ...(dataSet.dataSourceRef ? { dataSourceRef: { - id: selectedPattern.dataSourceRef.id, - name: selectedPattern.dataSourceRef.name, - type: selectedPattern.dataSourceRef.type, + id: dataSet.dataSourceRef.id, + name: dataSet.dataSourceRef.name, + type: dataSet.dataSourceRef.type, }, } : { dataSourceRef: undefined }), database: undefined, }); } - }) - .finally(() => { - isLoading(false); - }); - return () => { - setNavigatorState((prevState) => ({ ...prevState, isMounted: false })); + + isInitialized.current = true; + } finally { + setIsLoading(false); + } }; - }, [ - dataSet, - dataSetManager, - http, - indexPatternsService, - navigatorState.isMounted, - savedObjectsClient, - ]); + + initializeData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { const status = dataSourcesLoadStatus.toLowerCase(); const externalDataSourcesCache = CatalogCacheManager.getExternalDataSourcesCache(); if (status === DirectQueryLoadingStatus.SUCCESS) { - setNavigatorState((prevState) => ({ - ...prevState, - externalDataSources: externalDataSourcesCache.externalDataSources.map((ds) => ({ + setExternalDataSources( + externalDataSourcesCache.externalDataSources.map((ds) => ({ id: ds.dataSourceRef, name: ds.name, type: SIMPLE_DATA_SOURCE_TYPES.EXTERNAL, - })), - })); + })) + ); } else if ( status === DirectQueryLoadingStatus.CANCELED || status === DirectQueryLoadingStatus.FAILED ) { - setNavigatorState((prevState) => ({ ...prevState, failed: true })); - } - }, [dataSourcesLoadStatus]); - - useEffect(() => { - const status = databasesLoadStatus.toLowerCase(); - if ( - selectedDataSetState?.dataSourceRef && - selectedDataSetState.dataSourceRef.type === SIMPLE_DATA_SOURCE_TYPES.EXTERNAL - ) { - const dataSourceCache = CatalogCacheManager.getOrCreateDataSource( - selectedDataSetState.dataSourceRef.name, - selectedDataSetState.dataSourceRef.id - ); - if (status === DirectQueryLoadingStatus.SUCCESS) { - setNavigatorState((prevState) => ({ - ...prevState, - cachedDatabases: dataSourceCache.databases.map((database) => ({ - id: database.name, - title: database.name, - })), - })); - } else if ( - status === DirectQueryLoadingStatus.CANCELED || - status === DirectQueryLoadingStatus.FAILED - ) { - setNavigatorState((prevState) => ({ ...prevState, failed: true })); - } + notifications.toasts.addWarning('Error loading external data sources'); } - }, [databasesLoadStatus, selectedDataSetState?.dataSourceRef]); + }, [dataSourcesLoadStatus, notifications.toasts]); const handleSelectExternalDataSource = useCallback( async (dataSource: SimpleDataSource) => { @@ -318,13 +237,9 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { dataSourceMDSId: dataSource.id, }); } else if (dataSourceCache.status === CachedDataSourceStatus.Updated) { - setNavigatorState((prevState) => ({ - ...prevState, - cachedDatabases: dataSourceCache.databases.map((database) => ({ - id: database.name, - title: database.name, - })), - })); + setCachedDatabases( + dataSourceCache.databases.map((db) => ({ id: db.name, title: db.name })) + ); } setSelectedDataSetState((prevState) => ({ ...prevState!, @@ -335,7 +250,6 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { [databasesLoadStatus, startLoadingDatabases] ); - // Start loading tables for selected database const handleSelectExternalDatabase = useCallback( (externalDatabase: SimpleObject) => { if (selectedDataSetState?.dataSourceRef && externalDatabase && externalDatabase.title) { @@ -360,20 +274,18 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { dataSourceMDSId: selectedDataSetState.dataSourceRef.id, }); } else if (databaseCache.status === CachedDataSourceStatus.Updated) { - setNavigatorState((prevState) => ({ - ...prevState, - cachedTables: databaseCache.tables.map((table) => ({ + setCachedTables( + databaseCache.tables.map((table) => ({ id: table.name, title: table.name, - })), - })); + })) + ); } } }, [selectedDataSetState?.dataSourceRef, tablesLoadStatus, startLoadingTables] ); - // Retrieve tables from cache upon success useEffect(() => { if ( selectedDataSetState?.dataSourceRef && @@ -392,13 +304,12 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { return; } if (tablesStatus === DirectQueryLoadingStatus.SUCCESS) { - setNavigatorState((prevState) => ({ - ...prevState, - cachedTables: databaseCache.tables.map((table) => ({ + setCachedTables( + databaseCache.tables.map((table) => ({ id: table.name, title: table.name, - })), - })); + })) + ); } else if ( tablesStatus === DirectQueryLoadingStatus.CANCELED || tablesStatus === DirectQueryLoadingStatus.FAILED @@ -416,11 +327,10 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { const handleSelectedDataSource = useCallback( async (source: SimpleDataSource) => { if (source) { - isLoading(true); - const indices = await fetchIndices(searchService, source.id); - setNavigatorState((prevState) => ({ - ...prevState, - currentDataSourceRef: { + setIsLoading(true); + try { + const indices = await fetchIndices(searchService, source.id); + const updatedSource = { ...source, indices: indices.map((indexName: string) => ({ id: indexName, @@ -431,18 +341,28 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { type: source.type, }, })), - }, - })); - isLoading(false); + }; + + setDataSources((prevDataSources) => + prevDataSources.map((ds) => (ds.id === source.id ? updatedSource : ds)) + ); + + setSelectedDataSetState((prevState) => ({ + ...prevState!, + dataSourceRef: updatedSource, + })); + } finally { + setIsLoading(false); + } } }, [searchService] ); const handleSelectedObject = useCallback( - async (object) => { + async (object: SimpleObject) => { if (object) { - isLoading(true); + setIsLoading(true); const fields = await indexPatternsService.getFieldsForWildcard({ pattern: object.title, dataSourceId: object.dataSourceRef?.id, @@ -450,19 +370,17 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { const timeFields = fields.filter((field: any) => field.type === 'date'); const timeFieldName = timeFields?.length > 0 ? timeFields[0].name : undefined; - setNavigatorState((prevState) => ({ + setSelectedDataSetState((prevState) => ({ ...prevState, - currentDataSet: { - id: `${object.dataSourceRef ? object.dataSourceRef.id : ''}.${object.id}`, - title: object.title, - fields, - timeFields, - timeFieldName, - dataSourceRef: object.dataSourceRef, - type: SIMPLE_DATA_SET_TYPES.TEMPORARY, - }, + id: `${object.dataSourceRef ? object.dataSourceRef.id : ''}.${object.id}`, + title: object.title, + fields, + timeFields, + timeFieldName, + dataSourceRef: object.dataSourceRef, + type: SIMPLE_DATA_SET_TYPES.TEMPORARY, })); - isLoading(false); + setIsLoading(false); } }, [indexPatternsService] @@ -478,12 +396,15 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { defaultMessage: 'S3', }); - const createRefreshButton = () => ( - + const createRefreshButton = useCallback( + () => ( + + ), + [onRefresh, databasesLoadStatus, tablesLoadStatus] ); const createLoadingSpinner = () => ( @@ -492,85 +413,232 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { ); - const createIndexPatternsPanel = () => ({ - id: 1, - title: indexPatternsLabel, - items: navigatorState.indexPatterns.map((indexPattern) => ({ - name: indexPattern.title, - onClick: async () => await handleSelectedDataSet(indexPattern), - })), - content:
{navigatorState.indexPatterns.length === 0 && createLoadingSpinner()}
, - }); - - const createIndexesPanel = () => ({ - id: 2, - title: indicesLabel, - items: [ - ...navigatorState.dataSources.map((dataSource) => ({ - name: dataSource.name, - panel: 3, - onClick: async () => await handleSelectedDataSource(dataSource), - })), - ], - content:
{navigatorState.isLoading && createLoadingSpinner()}
, - }); - - const createDataSourcesPanel = () => ({ - id: 3, - title: navigatorState.currentDataSourceRef?.name ?? indicesLabel, - items: navigatorState.currentDataSourceRef?.indices?.map((object) => ({ - name: object.title, - panel: 7, - onClick: async () => await handleSelectedObject(object), - })), - content: ( -
- {navigatorState.isLoading && !navigatorState.currentDataSourceRef && createLoadingSpinner()} -
- ), - }); - - const createS3DataSourcesPanel = () => ({ - id: 4, - title: ( -
- {S3DataSourcesLabel} - {CatalogCacheManager.getExternalDataSourcesCache().status === - CachedDataSourceStatus.Updated && createRefreshButton()} -
- ), - items: [ - ...navigatorState.externalDataSources.map((dataSource) => ({ - name: dataSource.name, - onClick: async () => await handleSelectExternalDataSource(dataSource), - panel: 5, - })), - ], - content:
{dataSourcesLoadStatus && createLoadingSpinner()}
, - }); - - const createDatabasesPanel = () => ({ - id: 5, - title: selectedDataSetState?.dataSourceRef?.name - ? selectedDataSetState.dataSourceRef?.name - : 'Databases', - items: [ - ...navigatorState.cachedDatabases.map((db) => ({ - name: db.title, - onClick: async () => { - setSelectedDataSetState((prevState) => ({ - ...prevState!, - database: db, - })); - await handleSelectExternalDatabase(db); - }, - panel: 6, - })), + const panels = useMemo( + () => [ + { + id: 0, + items: [ + ...(CatalogCacheManager.getRecentDataSets().length > 0 + ? [ + { + name: 'Recently Used', + panel: 8, + }, + ] + : []), + { + name: indexPatternsLabel, + panel: 1, + }, + { + name: indicesLabel, + panel: 2, + }, + ...(isExternalDataSourcesEnabled + ? [ + { + name: S3DataSourcesLabel, + panel: 4, + onClick: () => { + const externalDataSourcesCache = CatalogCacheManager.getExternalDataSourcesCache(); + if ( + (externalDataSourcesCache.status === CachedDataSourceStatus.Empty || + externalDataSourcesCache.status === CachedDataSourceStatus.Failed) && + !isCatalogCacheFetching(dataSourcesLoadStatus) && + dataSources.length > 0 + ) { + startLoadingDataSources(dataSources.map((dataSource) => dataSource.id)); + } else if (externalDataSourcesCache.status === CachedDataSourceStatus.Updated) { + setExternalDataSources( + externalDataSourcesCache.externalDataSources.map((ds) => ({ + id: ds.dataSourceRef, + name: ds.name, + type: SIMPLE_DATA_SOURCE_TYPES.EXTERNAL, + })) + ); + } + }, + }, + ] + : []), + ], + }, + { + id: 1, + title: indexPatternsLabel, + items: indexPatterns.map((indexPattern) => ({ + name: indexPattern.title, + onClick: () => handleSelectedDataSet(indexPattern), + })), + content: indexPatterns.length === 0 && createLoadingSpinner(), + }, + { + id: 2, + title: indicesLabel, + items: dataSources.map((dataSource) => ({ + name: dataSource.name, + panel: 3, + onClick: () => handleSelectedDataSource(dataSource), + })), + content: isLoading && createLoadingSpinner(), + }, + { + id: 3, + title: selectedDataSetState?.dataSourceRef?.name ?? indicesLabel, + items: selectedDataSetState?.dataSourceRef?.indices?.map((object) => ({ + name: object.title, + panel: 7, + onClick: () => handleSelectedObject(object), + })), + content: + isLoading && !selectedDataSetState?.dataSourceRef?.indices && createLoadingSpinner(), + }, + { + id: 4, + title: ( +
+ {S3DataSourcesLabel} + {CatalogCacheManager.getExternalDataSourcesCache().status === + CachedDataSourceStatus.Updated && createRefreshButton()} +
+ ), + items: externalDataSources.map((dataSource) => ({ + name: dataSource.name, + onClick: () => handleSelectExternalDataSource(dataSource), + panel: 5, + })), + content: isCatalogCacheFetching(dataSourcesLoadStatus) && createLoadingSpinner(), + }, + { + id: 5, + title: selectedDataSetState?.dataSourceRef?.name ?? 'Databases', + items: cachedDatabases.map((db) => ({ + name: db.title, + onClick: () => { + setSelectedDataSetState((prevState) => ({ + ...prevState!, + database: db, + })); + handleSelectExternalDatabase(db); + }, + panel: 6, + })), + content: isCatalogCacheFetching(databasesLoadStatus) && createLoadingSpinner(), + }, + { + id: 6, + title: selectedDataSetState?.database?.title ?? 'Tables', + items: cachedTables.map((table) => ({ + name: table.title, + onClick: () => { + const tableObject: SimpleDataSet = { + ...selectedDataSetState, + id: `${selectedDataSetState?.dataSourceRef!.name}.${ + selectedDataSetState?.database?.title + }.${table.title}`, + title: `${selectedDataSetState?.dataSourceRef!.name}.${ + selectedDataSetState?.database?.title + }.${table.title}`, + ...(selectedDataSetState?.dataSourceRef && { + dataSourceRef: { + id: selectedDataSetState?.dataSourceRef!.id, + name: selectedDataSetState?.dataSourceRef!.name, + type: selectedDataSetState?.dataSourceRef!.type, + } as SimpleDataSource, + }), + type: SIMPLE_DATA_SET_TYPES.TEMPORARY_ASYNC, + }; + handleSelectedDataSet(tableObject); + }, + })), + content: isCatalogCacheFetching(tablesLoadStatus) && createLoadingSpinner(), + }, + { + id: 7, + title: selectedDataSetState?.title, + content: + !selectedDataSetState || !selectedDataSetState?.title ? ( + createLoadingSpinner() + ) : ( + + + 0 + ? selectedDataSetState.timeFields.map((field: any) => ({ + value: field.value, + text: field.name, + })) + : []), + { value: 'no-time-filter', text: "I don't want to use a time filter" }, + ]} + onChange={(event) => { + setSelectedDataSetState((prevState) => ({ + ...prevState!, + timeFieldName: + event.target.value !== 'no-time-filter' ? event.target.value : undefined, + })); + }} + aria-label="Select a date field" + /> + + handleSelectedDataSet()}> + Select + + + ), + }, + { + id: 8, + title: 'Recently Used', + items: CatalogCacheManager.getRecentDataSets().map((ds) => ({ + name: ds.title, + onClick: () => { + setSelectedDataSetState({ + id: ds.id ?? ds.title, + title: ds.title, + dataSourceRef: ds.dataSourceRef, + database: undefined, + timeFieldName: ds.timeFieldName, + type: ds.type, + }); + handleSelectedDataSet(); + }, + })), + }, ], - content:
{isCatalogCacheFetching(databasesLoadStatus) && createLoadingSpinner()}
, - }); + [ + indexPatternsLabel, + indicesLabel, + isExternalDataSourcesEnabled, + S3DataSourcesLabel, + indexPatterns, + dataSources, + isLoading, + selectedDataSetState, + createRefreshButton, + externalDataSources, + dataSourcesLoadStatus, + cachedDatabases, + databasesLoadStatus, + cachedTables, + tablesLoadStatus, + startLoadingDataSources, + handleSelectedDataSet, + handleSelectedDataSource, + handleSelectedObject, + handleSelectExternalDataSource, + handleSelectExternalDatabase, + ] + ); - const datasetTitle = + const dataSetTitle = selectedDataSetState && selectedDataSetState?.dataSourceRef && selectedDataSetState?.dataSourceRef.name @@ -580,21 +648,21 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { return ( + - {datasetTitle} + {dataSetTitle ?? 'Select data set'} } - isOpen={navigatorState.isOpen} + isOpen={isOpen} closePopover={closePopover} anchorPosition="downLeft" display="block" @@ -602,177 +670,13 @@ export const DataSetNavigator = (props: DataSetNavigatorProps) => { > 0 - ? [ - { - name: 'Recently Used', - panel: 8, - }, - ] - : []), - { - name: indexPatternsLabel, - panel: 1, - }, - { - name: indicesLabel, - panel: 2, - }, - ...(navigatorState.isExternalDataSourcesEnabled - ? [ - { - name: S3DataSourcesLabel, - panel: 4, - onClick: async () => { - const externalDataSourcesCache = CatalogCacheManager.getExternalDataSourcesCache(); - if ( - (externalDataSourcesCache.status === CachedDataSourceStatus.Empty || - externalDataSourcesCache.status === CachedDataSourceStatus.Failed) && - !isCatalogCacheFetching(dataSourcesLoadStatus) && - navigatorState.dataSources.length > 0 - ) { - startLoadingDataSources( - navigatorState.dataSources.map((dataSource) => dataSource.id) - ); - } else if ( - externalDataSourcesCache.status === CachedDataSourceStatus.Updated - ) { - setNavigatorState((prevState) => ({ - ...prevState, - externalDataSources: externalDataSourcesCache.externalDataSources.map( - (ds) => ({ - id: ds.dataSourceRef, - name: ds.name, - type: SIMPLE_DATA_SOURCE_TYPES.EXTERNAL, - }) - ), - })); - } - }, - }, - ] - : []), - ], - }, - createIndexPatternsPanel(), - createIndexesPanel(), - createDataSourcesPanel(), - createS3DataSourcesPanel(), - createDatabasesPanel(), - { - id: 6, - title: selectedDataSetState?.database ? selectedDataSetState.database.title : 'Tables', - items: [ - ...navigatorState.cachedTables.map((table) => ({ - name: table.title, - onClick: async () => { - const tableObject: SimpleDataSet = { - ...selectedDataSetState, - id: `${selectedDataSetState?.dataSourceRef!.name}.${ - selectedDataSetState?.database?.title - }.${table.title}`, - title: `${selectedDataSetState?.dataSourceRef!.name}.${ - selectedDataSetState?.database?.title - }.${table.title}`, - ...(selectedDataSetState?.dataSourceRef && { - dataSourceRef: { - id: selectedDataSetState?.dataSourceRef!.id, - name: selectedDataSetState?.dataSourceRef!.name, - type: selectedDataSetState?.dataSourceRef!.type, - } as SimpleDataSource, - }), - type: SIMPLE_DATA_SET_TYPES.TEMPORARY_ASYNC, - }; - await handleSelectedDataSet(tableObject); - }, - })), - ], - content: ( -
{isCatalogCacheFetching(tablesLoadStatus) && createLoadingSpinner()}
- ), - }, - { - id: 7, - title: navigatorState.currentDataSet?.title, - content: - !navigatorState.currentDataSet || !navigatorState.currentDataSet?.title ? ( -
{createLoadingSpinner()}
- ) : ( - - - 0 - ? [ - ...navigatorState.currentDataSet!.timeFields!.map((field: any) => ({ - value: field.name, - text: field.name, - })), - ] - : []), - { value: 'no-time-filter', text: "I don't want to use a time filter" }, - ]} - onChange={(event) => { - setNavigatorState((prevState) => ({ - ...prevState, - currentDataSet: { - ...prevState.currentDataSet!, - timeFieldName: - event.target.value !== 'no-time-filter' - ? (event.target.value as string) - : undefined, - }, - })); - }} - aria-label="Select a date field" - /> - - { - await handleSelectedDataSet(); - }} - > - Select - - - ), - }, - { - id: 8, - title: 'Recently Used', - items: CatalogCacheManager.getRecentDataSets().map((ds) => ({ - name: ds.title, - onClick: async () => { - setSelectedDataSetState({ - id: ds.id ?? ds.title, - title: ds.title, - dataSourceRef: ds.dataSourceRef, - database: undefined, - timeFieldName: ds.timeFieldName, - }); - await handleSelectedDataSet(); - }, - })), - }, - ]} + panels={panels} />
); }; // eslint-disable-next-line import/no-default-export -export default DataSetNavigator; +export default React.memo(DataSetNavigator); 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 d3495197512d..88ba526abcd5 100644 --- a/src/plugins/data/public/ui/query_editor/_query_editor.scss +++ b/src/plugins/data/public/ui/query_editor/_query_editor.scss @@ -144,7 +144,7 @@ margin-bottom: -3px; // TODO: Hack to handle the renderSharingMetaFields adding a new empty div to the UI &.expanded.emptyExpanded { - .osdQueryEditor__datasetPicker { + .osdQueryEditor__dataSetPicker { width: auto; } } @@ -160,7 +160,7 @@ border-right: $euiBorderThin; } - .osdQueryEditor__datasetPicker { + .osdQueryEditor__dataSetPicker { width: 155px; padding-left: 4px; diff --git a/src/plugins/data/public/ui/query_editor/query_editor.tsx b/src/plugins/data/public/ui/query_editor/query_editor.tsx index 805ba13c01bb..476a05fe4965 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -337,7 +337,7 @@ export default class QueryEditorUI extends Component { footerItems: { start: [ `${this.state.lineCount} ${this.state.lineCount === 1 ? 'line' : 'lines'}`, - typeof this.props.dataSet?.timeFieldName || '', + this.props.dataSet?.timeFieldName || '', ], }, // provideCompletionItems: this.provideCompletionItems, @@ -397,7 +397,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 6c16858f7925..4621944ecce3 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 @@ -71,7 +71,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { storage, appName, data: { - query: { dataSet: dataSetManager }, + query: { dataSetManager: dataSetManager }, }, } = opensearchDashboards.services; const { dataSet } = useDataSetManager({ dataSetManager: dataSetManager! }); diff --git a/src/plugins/data/public/ui/ui_service.ts b/src/plugins/data/public/ui/ui_service.ts index 4f403597467b..8359872137b2 100644 --- a/src/plugins/data/public/ui/ui_service.ts +++ b/src/plugins/data/public/ui/ui_service.ts @@ -79,7 +79,7 @@ export class UiService implements Plugin { DataSetNavigator: createDataSetNavigator( core.savedObjects.client, core.http, - dataServices.query.dataSet + dataServices.query.dataSetManager ), SearchBar, SuggestionsComponent, 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 f4e8910fe1d5..76d6c9789095 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 @@ -17,7 +17,9 @@ import { getTopNavLinks } from '../../components/top_nav/get_top_nav_links'; import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; import { useDiscoverContext } from '../context'; import { useDispatch, setSavedQuery, useSelector } from '../../utils/state_management'; + import './discover_canvas.scss'; +import { useDataSetManager } from '../utils/use_dataset_manager'; export interface TopNavProps { opts: { @@ -65,21 +67,27 @@ export const TopNav = ({ opts, showSaveQuery, isEnhancementsEnabled }: TopNavPro useEffect(() => { let isMounted = true; - const getDefaultIndexPattern = async () => { + const initializeDataSet = async () => { await data.indexPatterns.ensureDefaultIndexPattern(); const defaultIndexPattern = await data.indexPatterns.getDefault(); + const { dataSetManager } = data.query; + dataSetManager.initWithIndexPattern(defaultIndexPattern); + const defaultDataSet = dataSetManager.getDefaultDataSet(); if (!isMounted) return; setIndexPatterns(defaultIndexPattern ? [defaultIndexPattern] : undefined); + if (defaultDataSet) { + dataSetManager.setDataSet(defaultDataSet); + } }; - getDefaultIndexPattern(); + initializeDataSet(); return () => { isMounted = false; }; - }, [data.indexPatterns]); + }, [data.indexPatterns, data.query]); useEffect(() => { const pageTitleSuffix = savedSearch?.id && savedSearch.title ? `: ${savedSearch.title}` : ''; diff --git a/src/plugins/discover/public/application/view_components/panel/index.tsx b/src/plugins/discover/public/application/view_components/panel/index.tsx index e88a8f7dbede..1cdc0eae2a74 100644 --- a/src/plugins/discover/public/application/view_components/panel/index.tsx +++ b/src/plugins/discover/public/application/view_components/panel/index.tsx @@ -20,7 +20,6 @@ import { IndexPatternField, UI_SETTINGS, opensearchFilters } from '../../../../. import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { DiscoverViewServices } from '../../../build_services'; import { popularizeField } from '../../helpers/popularize_field'; -import { getDataSet } from '../../helpers/get_data_set'; import { buildColumns } from '../../utils/columns'; // eslint-disable-next-line import/no-default-export @@ -101,13 +100,6 @@ export default function DiscoverPanel(props: ViewProps) { }); }, [application, fetchState.title, indexPattern?.title]); - const onNormalize = useCallback(async () => { - if (!fetchState.title) return; - if (fetchState.title === indexPattern?.title) return; - const dataSet = getDataSet(indexPattern, fetchState, indexPatterns); - await indexPatterns.refreshFields(dataSet!, true); - }, [fetchState, indexPattern, indexPatterns]); - const isEnhancementsEnabledOverride = services.uiSettings.get( UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED ); @@ -118,9 +110,8 @@ export default function DiscoverPanel(props: ViewProps) { fieldCounts={fetchState.fieldCounts || {}} hits={fetchState.rows || []} onAddField={(fieldName, index) => { - const dataSet = getDataSet(indexPattern, fetchState, indexPatterns); - if (dataSet && capabilities.discover?.save) { - popularizeField(dataSet, fieldName, indexPatterns); + if (indexPattern && capabilities.discover?.save) { + popularizeField(indexPattern, fieldName, indexPatterns); } dispatch( @@ -131,9 +122,8 @@ export default function DiscoverPanel(props: ViewProps) { ); }} onRemoveField={(fieldName) => { - const dataSet = getDataSet(indexPattern, fetchState, indexPatterns); - if (dataSet && capabilities.discover?.save) { - popularizeField(dataSet, fieldName, indexPatterns); + if (indexPattern && capabilities.discover?.save) { + popularizeField(indexPattern, fieldName, indexPatterns); } dispatch(removeColumn(fieldName)); @@ -146,9 +136,9 @@ export default function DiscoverPanel(props: ViewProps) { }) ); }} - selectedIndexPattern={getDataSet(indexPattern, fetchState, indexPatterns)} + selectedIndexPattern={indexPattern} onCreateIndexPattern={onCreateIndexPattern} - onNormalize={onNormalize} + onNormalize={() => {}} onAddFilter={onAddFilter} isEnhancementsEnabledOverride={isEnhancementsEnabledOverride} /> 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 05d4a2dbd8b4..88982650bbcb 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,11 +30,11 @@ export const updateSearchSource = async ({ histogramConfigs, }: Props) => { const { uiSettings, data } = services; - const queryDataSet = data.query.dataSet.getDataSet(); + const queryDataSet = data.query.dataSetManager.getDataSet(); let dataSet = indexPattern.id === queryDataSet?.id - ? await data.indexPatterns.getByTitle(queryDataSet?.title!) + ? await data.indexPatterns.get(queryDataSet?.id!, true) : indexPattern; const dataFrame = searchSource?.getDataFrame(); if ( 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 046fe1fbcb2e..c03d3e6721e7 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 @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { i18n } from '@osd/i18n'; -import { SIMPLE_DATA_SET_TYPES } from '../../../../../data/common'; +import { SIMPLE_DATA_SET_TYPES, SimpleDataSet } from '../../../../../data/common'; import { IndexPattern } from '../../../../../data/public'; import { useSelector, updateIndexPattern } from '../../utils/state_management'; import { DiscoverViewServices } from '../../../build_services'; @@ -29,84 +29,99 @@ import { QUERY_ENHANCEMENT_ENABLED_SETTING } from '../../../../common'; */ export const useIndexPattern = (services: DiscoverViewServices) => { const { data, toastNotifications, uiSettings, store } = services; - const { dataSet } = useDataSetManager({ - dataSetManager: data.query.dataSet, - }); + const { dataSet } = useDataSetManager({ dataSetManager: data.query.dataSetManager }); const indexPatternIdFromState = useSelector((state) => state.metadata.indexPattern); const [indexPattern, setIndexPattern] = useState(undefined); - const isQueryEnhancementEnabled = uiSettings.get(QUERY_ENHANCEMENT_ENABLED_SETTING); + const isQueryEnhancementEnabled = useMemo( + () => uiSettings.get(QUERY_ENHANCEMENT_ENABLED_SETTING), + [uiSettings] + ); + + const fetchIndexPatternDetails = useCallback((id: string) => data.indexPatterns.get(id), [ + data.indexPatterns, + ]); - const fetchIndexPatternDetails = useCallback( - async (id: string) => { - return await data.indexPatterns.get(id); + const createTempIndexPattern = useCallback( + async (dataSetFromState: SimpleDataSet) => { + try { + const tempIndexPattern = await data.indexPatterns.create( + { + id: `${dataSetFromState.dataSourceRef?.id || ''}.${dataSetFromState.title}`, + title: dataSetFromState.title, + dataSourceRef: dataSetFromState.dataSourceRef, + type: dataSetFromState.type, + timeFieldName: dataSetFromState.timeFieldName, + fields: dataSetFromState.fields as any, + }, + true + ); + data.indexPatterns.saveToCache(tempIndexPattern.id!, tempIndexPattern); + return tempIndexPattern; + } catch (error) { + return null; + } }, [data.indexPatterns] ); useEffect(() => { - if (isQueryEnhancementEnabled) { - if (dataSet) { + let isMounted = true; + + const handleIndexPattern = async () => { + if (isQueryEnhancementEnabled && dataSet) { + let pattern; + if (dataSet.type === SIMPLE_DATA_SET_TYPES.INDEX_PATTERN) { - fetchIndexPatternDetails(dataSet.id).then((ip) => { - setIndexPattern(ip); - }); + pattern = await fetchIndexPatternDetails(dataSet.id); + } else { + pattern = await createTempIndexPattern(dataSet); } - } - } - }, [dataSet, fetchIndexPatternDetails, isQueryEnhancementEnabled]); - useEffect(() => { - let isMounted = true; - - const fetchIndexPattern = (id: string) => { - fetchIndexPatternDetails(id) - .then((result) => { + if (isMounted && pattern) { + setIndexPattern(pattern); + } + } else if (!isQueryEnhancementEnabled) { + if (!indexPatternIdFromState) { + const indexPatternList = await data.indexPatterns.getCache(); + const newId = getIndexPatternId('', indexPatternList, uiSettings.get('defaultIndex')); if (isMounted) { - setIndexPattern(result); + store!.dispatch(updateIndexPattern(newId)); + handleIndexPattern(); } - }) - .catch(() => { - if (isMounted) { - const indexPatternMissingWarning = i18n.translate( - 'discover.valueIsNotConfiguredIndexPatternIDWarningTitle', - { - defaultMessage: '{id} is not a configured index pattern ID', - values: { - id: `"${id}"`, - }, - } - ); - toastNotifications.addDanger({ - title: indexPatternMissingWarning, - }); + } else { + try { + const ip = await fetchIndexPatternDetails(indexPatternIdFromState); + if (isMounted) { + setIndexPattern(ip); + } + } catch (error) { + if (isMounted) { + const warningMessage = i18n.translate('discover.indexPatternFetchErrorWarning', { + defaultMessage: 'Error fetching index pattern: {error}', + values: { error: (error as Error).message }, + }); + toastNotifications.addWarning(warningMessage); + } } - }); + } + } }; - if (!isQueryEnhancementEnabled) { - if (!indexPatternIdFromState) { - data.indexPatterns.getCache().then((indexPatternList) => { - const newId = getIndexPatternId('', indexPatternList, uiSettings.get('defaultIndex')); - store!.dispatch(updateIndexPattern(newId)); - fetchIndexPattern(newId); - }); - } else { - fetchIndexPattern(indexPatternIdFromState); - } - } + handleIndexPattern(); return () => { isMounted = false; }; }, [ + isQueryEnhancementEnabled, + dataSet, indexPatternIdFromState, + fetchIndexPatternDetails, + createTempIndexPattern, data.indexPatterns, - toastNotifications, store, - isQueryEnhancementEnabled, - dataSet, + toastNotifications, uiSettings, - fetchIndexPatternDetails, ]); 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 026f98e552c0..7e8095c5af30 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 @@ -252,7 +252,7 @@ export const useSearch = (services: DiscoverViewServices) => { timefilter.getTimeUpdate$(), timefilter.getAutoRefreshFetch$(), data.query.queryString.getUpdates$(), - data.query.dataSet.getUpdates$() + data.query.dataSetManager.getUpdates$() ).pipe(debounceTime(100)); const subscription = fetch$.subscribe(() => { @@ -282,7 +282,7 @@ export const useSearch = (services: DiscoverViewServices) => { fetch, core.fatalErrors, shouldSearchOnPageLoad, - data.query.dataSet, + data.query.dataSetManager, ]); // Get savedSearch if it exists diff --git a/src/plugins/query_enhancements/common/constants.ts b/src/plugins/query_enhancements/common/constants.ts index 358d41745e8e..57316efdf5d2 100644 --- a/src/plugins/query_enhancements/common/constants.ts +++ b/src/plugins/query_enhancements/common/constants.ts @@ -43,6 +43,9 @@ export const OPENSEARCH_API = { DATA_CONNECTIONS: URI.DATA_CONNECTIONS, }; -export const UI_SETTINGS = {}; +export const UI_SETTINGS = { + QUERY_ENHANCEMENTS_ENABLED: 'query:enhancements:enabled', + STATE_STORE_IN_SESSION_STORAGE: 'state:storeInSessionStorage', +}; export const ERROR_DETAILS = { GUARDRAILS_TRIGGERED: 'guardrails triggered' }; diff --git a/src/plugins/query_enhancements/public/plugin.tsx b/src/plugins/query_enhancements/public/plugin.tsx index 13b2b01efc78..5b4a52c96012 100644 --- a/src/plugins/query_enhancements/public/plugin.tsx +++ b/src/plugins/query_enhancements/public/plugin.tsx @@ -7,7 +7,7 @@ import moment from 'moment'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '../../../core/public'; import { IStorageWrapper, Storage } from '../../opensearch_dashboards_utils/public'; import { ConfigSchema } from '../common/config'; -import { ConnectionsService, setData, setStorage } from './services'; +import { setData, setStorage } from './services'; import { createQueryAssistExtension } from './query_assist'; import { PPLSearchInterceptor, SQLSearchInterceptor } from './search'; import { @@ -16,6 +16,7 @@ import { QueryEnhancementsPluginStart, QueryEnhancementsPluginStartDependencies, } from './types'; +import { UI_SETTINGS } from '../common'; export class QueryEnhancementsPlugin implements @@ -27,7 +28,6 @@ export class QueryEnhancementsPlugin > { private readonly storage: IStorageWrapper; private readonly config: ConfigSchema; - private connectionsService!: ConnectionsService; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); @@ -38,9 +38,17 @@ export class QueryEnhancementsPlugin core: CoreSetup, { data }: QueryEnhancementsPluginSetupDependencies ): QueryEnhancementsPluginSetup { - this.connectionsService = new ConnectionsService({ - startServices: core.getStartServices(), - http: core.http, + 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 pplSearchInterceptor = new PPLSearchInterceptor({ @@ -79,7 +87,6 @@ export class QueryEnhancementsPlugin visualizable: false, }, supportedAppNames: ['discover'], - connectionService: this.connectionsService, }, }, }); @@ -102,7 +109,6 @@ export class QueryEnhancementsPlugin }, showDocLinks: false, supportedAppNames: ['discover'], - connectionService: this.connectionsService, }, }, }); 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 8af34d4861f0..9433047833b8 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 @@ -40,11 +40,11 @@ export const QueryAssistBar: React.FC = (props) => { const previousQuestionRef = useRef(); useEffect(() => { - const subscription = services.data.query.dataSet.getUpdates$().subscribe((dataSet) => { + const subscription = services.data.query.dataSetManager.getUpdates$().subscribe((dataSet) => { setSelectedDataSet(dataSet); }); return () => subscription.unsubscribe(); - }, [services.data.query.dataSet]); + }, [services.data.query.dataSetManager]); const onSubmit = async (e: SyntheticEvent) => { e.preventDefault(); 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 5bbe063e4fa1..c3fb47aef887 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 @@ -15,6 +15,24 @@ import { DataSetContract } from '../../../../data/public/query'; import { ConfigSchema } from '../../../common/config'; import { createQueryAssistExtension } from './create_extension'; +const mockSimpleDataSet = { + id: 'mock-data-set-id', + title: 'mock-title', + dataSourceRef: { + id: 'mock-data-source-id', + }, +} as SimpleDataSet; + +const mockDataSetManager: jest.Mocked = { + getUpdates$: jest.fn().mockReturnValue(of(mockSimpleDataSet)), + getDataSet: jest.fn().mockReturnValue(mockSimpleDataSet), + setDataSet: jest.fn(), + getDefaultDataSet: jest.fn().mockReturnValue(mockSimpleDataSet), + fetchDefaultDataSet: jest.fn().mockResolvedValue(mockSimpleDataSet), + init: jest.fn(), + initWithIndexPattern: jest.fn(), +}; + const coreSetupMock = coreMock.createSetup({ pluginStartDeps: { data: { @@ -23,18 +41,13 @@ const coreSetupMock = coreMock.createSetup({ }, }); const httpMock = coreSetupMock.http; -const dataMock = dataPluginMock.createSetupContract(); -const dataSetMock = dataMock.query.dataSet as jest.Mocked; - -const mockSimpleDataSet = { - id: 'mock-data-set-id', - title: 'mock-title', - dataSourceRef: { - id: 'mock-data-source-id', +const dataMock = { + ...dataPluginMock.createSetupContract(), + query: { + ...dataPluginMock.createSetupContract().query, + dataSetManager: mockDataSetManager, }, -} as SimpleDataSet; - -dataSetMock.getUpdates$.mockReturnValue(of(mockSimpleDataSet)); +}; jest.mock('../components', () => ({ QueryAssistBar: jest.fn(() =>
QueryAssistBar
), @@ -58,7 +71,17 @@ describe('CreateExtension', () => { it('should be enabled if at least one language is configured', async () => { httpMock.get.mockResolvedValueOnce({ configuredLanguages: ['PPL'] }); - const extension = createQueryAssistExtension(httpMock, dataMock, config); + const extension = createQueryAssistExtension( + httpMock, + { + ...dataMock, + query: { + ...dataMock.query, + dataSetManager: mockDataSetManager, + }, + }, + config + ); const isEnabled = await firstValueFrom(extension.isEnabled$(dependencies)); expect(isEnabled).toBeTruthy(); expect(httpMock.get).toBeCalledWith('/api/enhancements/assist/languages', { 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 a0d35e374f03..eaafaf88f7ce 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,7 +25,7 @@ const getAvailableLanguages$ = ( http: HttpSetup, data: DataPublicPluginSetup ) => - data.query.dataSet.getUpdates$().pipe( + data.query.dataSetManager.getUpdates$().pipe( distinctUntilChanged(), switchMap(async (simpleDataSet) => { // currently query assist tool relies on opensearch API to get index 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 8010cf31276f..1f28df9af998 100644 --- a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts +++ b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts @@ -63,9 +63,9 @@ export class PPLSearchInterceptor extends SearchInterceptor { const { fromDate, toDate } = formatTimePickerDate(dateRange, 'YYYY-MM-DD HH:mm:ss.SSS'); const getTimeFilter = (timeField: any) => { - return ` | where ${timeField} >= '${formatDate(fromDate)}' and ${timeField} <= '${formatDate( - toDate - )}'`; + return ` | where \`${timeField}\` >= '${formatDate( + fromDate + )}' and \`${timeField}\` <= '${formatDate(toDate)}'`; }; const insertTimeFilter = (query: string, filter: string) => { @@ -151,10 +151,10 @@ export class PPLSearchInterceptor extends SearchInterceptor { }, queryConfig: { ...dataFrame.meta.queryConfig, - ...(this.queryService.dataSet.getDataSet() && { - dataSourceId: this.queryService.dataSet.getDataSet()?.dataSourceRef?.id, - dataSourceName: this.queryService.dataSet.getDataSet()?.dataSourceRef?.name, - timeFieldName: this.queryService.dataSet.getDataSet()?.timeFieldName, + ...(this.queryService.dataSetManager.getDataSet() && { + dataSourceId: this.queryService.dataSetManager.getDataSet()?.dataSourceRef?.id, + dataSourceName: this.queryService.dataSetManager.getDataSet()?.dataSourceRef?.name, + timeFieldName: this.queryService.dataSetManager.getDataSet()?.timeFieldName, }), }, }; 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 2fa06e2b0be0..cbd04f02a431 100644 --- a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts +++ b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts @@ -66,10 +66,10 @@ export class SQLSearchInterceptor extends SearchInterceptor { ...dataFrame.meta, queryConfig: { ...dataFrame.meta.queryConfig, - ...(this.queryService.dataSet.getDataSet() && { - dataSourceId: this.queryService.dataSet.getDataSet()?.dataSourceRef?.id, - dataSourceName: this.queryService.dataSet.getDataSet()?.dataSourceRef?.name, - timeFieldName: this.queryService.dataSet.getDataSet()?.timeFieldName, + ...(this.queryService.dataSetManager.getDataSet() && { + dataSourceId: this.queryService.dataSetManager.getDataSet()?.dataSourceRef?.id, + dataSourceName: this.queryService.dataSetManager.getDataSet()?.dataSourceRef?.name, + timeFieldName: this.queryService.dataSetManager.getDataSet()?.timeFieldName, }), }, }; @@ -109,10 +109,10 @@ export class SQLSearchInterceptor extends SearchInterceptor { } const queryString = getRawQueryString(searchRequest) ?? ''; - const dataSourceRef = this.queryService.dataSet.getDataSet() + const dataSourceRef = this.queryService.dataSetManager.getDataSet() ? { - dataSourceId: this.queryService.dataSet.getDataSet()?.dataSourceRef?.id, - dataSourceName: this.queryService.dataSet.getDataSet()?.dataSourceRef?.name, + dataSourceId: this.queryService.dataSetManager.getDataSet()?.dataSourceRef?.id, + dataSourceName: this.queryService.dataSetManager.getDataSet()?.dataSourceRef?.name, } : {}; @@ -182,7 +182,7 @@ export class SQLSearchInterceptor extends SearchInterceptor { } public search(request: IOpenSearchDashboardsSearchRequest, options: ISearchOptions) { - const dataSet = this.queryService.dataSet.getDataSet(); + const dataSet = this.queryService.dataSetManager.getDataSet(); if (dataSet?.type === SIMPLE_DATA_SET_TYPES.TEMPORARY_ASYNC) { return this.runSearchAsync(request, options.abortSignal, SEARCH_STRATEGY.SQL_ASYNC); }