diff --git a/changelogs/fragments/7034.yml b/changelogs/fragments/7034.yml new file mode 100644 index 000000000000..f48b357f62ff --- /dev/null +++ b/changelogs/fragments/7034.yml @@ -0,0 +1,2 @@ +feat: +- Add search bar extensions ([#7034](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7034)) diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 3ce43324ac31..b82001018886 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -464,6 +464,7 @@ export { TimeHistoryContract, QueryStateChange, QueryStart, + PersistedLog, } from './query'; export { AggsStart } from './search/aggs'; diff --git a/src/plugins/data/public/ui/query_editor/index.tsx b/src/plugins/data/public/ui/query_editor/index.tsx index 20ec9ca4e032..bddef49af1d4 100644 --- a/src/plugins/data/public/ui/query_editor/index.tsx +++ b/src/plugins/data/public/ui/query_editor/index.tsx @@ -24,3 +24,5 @@ export const QueryEditor = (props: QueryEditorProps) => ( ); export type { QueryEditorProps }; + +export { QueryEditorExtensions, QueryEditorExtensionConfig } from './query_editor_extensions'; 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 f0c4078815d8..2688ae6a8303 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -43,6 +43,8 @@ export interface QueryEditorProps { isInvalid?: boolean; queryEditorHeaderRef: React.RefObject; queryEditorHeaderClassName?: string; + queryEditorBannerRef: React.RefObject; + queryEditorBannerClassName?: string; } interface Props extends QueryEditorProps { @@ -253,8 +255,14 @@ export default class QueryEditorUI extends Component { this.props.queryEditorHeaderClassName ); + const queryEditorBannerClassName = classNames( + 'osdQueryEditorBanner', + this.props.queryEditorBannerClassName + ); + return (
+
diff --git a/src/plugins/data/public/ui/query_editor/query_editor_extensions/index.tsx b/src/plugins/data/public/ui/query_editor/query_editor_extensions/index.tsx new file mode 100644 index 000000000000..f406423d616e --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/query_editor_extensions/index.tsx @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ComponentProps } from 'react'; + +const Fallback = () =>
; + +const LazyQueryEditorExtensions = React.lazy(() => import('./query_editor_extensions')); +export const QueryEditorExtensions = (props: ComponentProps) => ( + }> + + +); + +export { QueryEditorExtensionConfig } from './query_editor_extension'; diff --git a/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.test.tsx b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.test.tsx new file mode 100644 index 000000000000..b3c8747e833d --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render, waitFor } from '@testing-library/react'; +import React, { ComponentProps } from 'react'; +import { IIndexPattern } from '../../../../common'; +import { QueryEditorExtension } from './query_editor_extension'; + +jest.mock('react-dom', () => ({ + ...jest.requireActual('react-dom'), + createPortal: jest.fn((element) => element), +})); + +type QueryEditorExtensionProps = ComponentProps; + +const mockIndexPattern = { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], +} as IIndexPattern; + +describe('QueryEditorExtension', () => { + const getComponentMock = jest.fn(); + const getBannerMock = jest.fn(); + const isEnabledMock = jest.fn(); + + const defaultProps: QueryEditorExtensionProps = { + config: { + id: 'test-extension', + order: 1, + isEnabled: isEnabledMock, + getComponent: getComponentMock, + getBanner: getBannerMock, + }, + dependencies: { + indexPatterns: [mockIndexPattern], + language: 'Test', + }, + componentContainer: document.createElement('div'), + bannerContainer: document.createElement('div'), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly when isEnabled is true', async () => { + isEnabledMock.mockResolvedValue(true); + getComponentMock.mockReturnValue(
Test Component
); + getBannerMock.mockReturnValue(
Test Banner
); + + const { getByText } = render(); + + await waitFor(() => { + expect(getByText('Test Component')).toBeInTheDocument(); + expect(getByText('Test Banner')).toBeInTheDocument(); + }); + + expect(isEnabledMock).toHaveBeenCalled(); + expect(getComponentMock).toHaveBeenCalledWith(defaultProps.dependencies); + }); + + it('does not render when isEnabled is false', async () => { + isEnabledMock.mockResolvedValue(false); + getComponentMock.mockReturnValue(
Test Component
); + + const { queryByText } = render(); + + await waitFor(() => { + expect(queryByText('Test Component')).toBeNull(); + }); + + expect(isEnabledMock).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.tsx b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.tsx new file mode 100644 index 000000000000..30b02f0f15dc --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.tsx @@ -0,0 +1,112 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiErrorBoundary } from '@elastic/eui'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import ReactDOM from 'react-dom'; +import { IIndexPattern } from '../../../../common'; +import { DataSource } from '../../../data_sources/datasource'; + +interface QueryEditorExtensionProps { + config: QueryEditorExtensionConfig; + dependencies: QueryEditorExtensionDependencies; + componentContainer: Element; + bannerContainer: Element; +} + +export interface QueryEditorExtensionDependencies { + /** + * Currently selected index patterns. + */ + indexPatterns?: Array; + /** + * Currently selected data source. + */ + dataSource?: DataSource; + /** + * Currently selected query language. + */ + language: string; +} + +export interface QueryEditorExtensionConfig { + /** + * The id for the search bar extension. + */ + id: string; + /** + * Lower order indicates higher position on UI. + */ + order: number; + /** + * A function that determines if the search bar extension is enabled and should be rendered on UI. + * @returns whether the extension is enabled. + */ + isEnabled: (dependencies: QueryEditorExtensionDependencies) => Promise; + /** + * A function that returns the search bar extension component. The component + * will be displayed on top of the query editor in the search bar. + * @param dependencies - The dependencies required for the extension. + * @returns The component the search bar extension. + */ + getComponent?: (dependencies: QueryEditorExtensionDependencies) => React.ReactElement | null; + /** + * A function that returns the search bar extension banner. The banner is a + * component that will be displayed on top of the search bar. + * @param dependencies - The dependencies required for the extension. + * @returns The component the search bar extension. + */ + getBanner?: (dependencies: QueryEditorExtensionDependencies) => React.ReactElement | null; +} + +const QueryEditorExtensionPortal: React.FC<{ container: Element }> = (props) => { + if (!props.children) return null; + + return ReactDOM.createPortal( + {props.children}, + props.container + ); +}; + +export const QueryEditorExtension: React.FC = (props) => { + const [isEnabled, setIsEnabled] = useState(false); + const isMounted = useRef(false); + + const banner = useMemo(() => props.config.getBanner?.(props.dependencies), [ + props.config, + props.dependencies, + ]); + + const component = useMemo(() => props.config.getComponent?.(props.dependencies), [ + props.config, + props.dependencies, + ]); + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + useEffect(() => { + props.config.isEnabled(props.dependencies).then((enabled) => { + if (isMounted.current) setIsEnabled(enabled); + }); + }, [props.dependencies, props.config]); + + if (!isEnabled) return null; + + return ( + <> + + {banner} + + + {component} + + + ); +}; diff --git a/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.test.tsx b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.test.tsx new file mode 100644 index 000000000000..f3dcd43b13d0 --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render, waitFor } from '@testing-library/react'; +import React, { ComponentProps } from 'react'; +import { QueryEditorExtension } from './query_editor_extension'; +import QueryEditorExtensions from './query_editor_extensions'; + +type QueryEditorExtensionProps = ComponentProps; +type QueryEditorExtensionsProps = ComponentProps; + +jest.mock('./query_editor_extension', () => ({ + QueryEditorExtension: jest.fn(({ config, dependencies }: QueryEditorExtensionProps) => ( +
+ Mocked QueryEditorExtension {config.id} with{' '} + {dependencies.indexPatterns?.map((i) => (typeof i === 'string' ? i : i.title)).join(', ')} +
+ )), +})); + +describe('QueryEditorExtensions', () => { + const defaultProps: QueryEditorExtensionsProps = { + indexPatterns: [ + { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], + }, + ], + componentContainer: document.createElement('div'), + bannerContainer: document.createElement('div'), + language: 'Test', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders without any configurations', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it('renders without any items in map', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it('correctly orders configurations based on order property', () => { + const configMap = { + '1': { id: '1', order: 2, isEnabled: jest.fn(), getComponent: jest.fn() }, + '2': { id: '2', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }, + }; + + const { getAllByText } = render( + + ); + const renderedExtensions = getAllByText(/Mocked QueryEditorExtension/); + + expect(renderedExtensions).toHaveLength(2); + expect(renderedExtensions[0]).toHaveTextContent('2'); + expect(renderedExtensions[1]).toHaveTextContent('1'); + }); + + it('passes dependencies correctly to QueryEditorExtension', async () => { + const configMap = { + '1': { id: '1', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }, + }; + + const { getByText } = render(); + + await waitFor(() => { + expect(getByText(/logstash-\*/)).toBeInTheDocument(); + }); + + expect(QueryEditorExtension).toHaveBeenCalledWith( + expect.objectContaining({ + dependencies: { indexPatterns: defaultProps.indexPatterns, language: 'Test' }, + }), + expect.anything() + ); + }); +}); diff --git a/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.tsx b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.tsx new file mode 100644 index 000000000000..6b2d5011216c --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.tsx @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo } from 'react'; +import { + QueryEditorExtension, + QueryEditorExtensionConfig, + QueryEditorExtensionDependencies, +} from './query_editor_extension'; + +interface QueryEditorExtensionsProps extends QueryEditorExtensionDependencies { + configMap?: Record; + componentContainer: Element; + bannerContainer: Element; +} + +const QueryEditorExtensions: React.FC = React.memo((props) => { + const { configMap, componentContainer, bannerContainer, ...dependencies } = props; + + const sortedConfigs = useMemo(() => { + if (!configMap || !Object.keys(configMap)) return []; + return Object.values(configMap).sort((a, b) => a.order - b.order); + }, [configMap]); + + return ( + <> + {sortedConfigs.map((config) => ( + + ))} + + ); +}); + +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default QueryEditorExtensions; 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 cdeadec39613..713bd3345e9c 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 @@ -2,38 +2,37 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - import dateMath from '@elastic/datemath'; -import classNames from 'classnames'; -import React, { useRef, useState } from 'react'; - import { + EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, - EuiFieldText, + EuiSuperUpdateButton, + OnRefreshProps, prettyDuration, } from '@elastic/eui'; -// @ts-ignore -import { EuiSuperUpdateButton, OnRefreshProps } from '@elastic/eui'; -import { isEqual, compact } from 'lodash'; +import classNames from 'classnames'; +import { compact, isEqual } from 'lodash'; +import React, { useRef, useState } from 'react'; import { + DataSource, IDataPluginServices, IIndexPattern, - TimeRange, - TimeHistoryContract, Query, - DataSource, + TimeHistoryContract, + TimeRange, } from '../..'; import { useOpenSearchDashboards, withOpenSearchDashboards, } from '../../../../opensearch_dashboards_react/public'; -import QueryEditorUI from './query_editor'; import { UI_SETTINGS } from '../../../common'; -import { PersistedLog, fromUser, getQueryLog } from '../../query'; -import { NoDataPopover } from './no_data_popover'; +import { fromUser, getQueryLog, PersistedLog } from '../../query'; +import { QueryEditorExtensions } from './query_editor_extensions'; import { Settings } from '../types'; +import { NoDataPopover } from './no_data_popover'; +import QueryEditorUI from './query_editor'; const QueryEditor = withOpenSearchDashboards(QueryEditorUI); @@ -73,6 +72,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); const [isQueryEditorFocused, setIsQueryEditorFocused] = useState(false); const queryEditorHeaderRef = useRef(null); + const queryEditorBannerRef = useRef(null); const opensearchDashboards = useOpenSearchDashboards(); const { uiSettings, storage, appName } = opensearchDashboards.services; @@ -85,6 +85,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { props.settings && props.settings.getQueryEnhancements(queryLanguage)?.searchBar) || null; + const queryEditorExtensionMap = props.settings?.getQueryEditorExtensionMap(); const parsedQuery = !queryUiEnhancement || isValidQuery(props.query) ? props.query! @@ -246,11 +247,32 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { persistedLog={persistedLog} dataTestSubj={props.dataTestSubj} queryEditorHeaderRef={queryEditorHeaderRef} + queryEditorBannerRef={queryEditorBannerRef} /> ); } + function renderQueryEditorExtensions() { + if ( + !shouldRenderQueryEditorExtensions() || + !queryEditorHeaderRef.current || + !queryEditorBannerRef.current || + !queryLanguage + ) + return; + return ( + + ); + } + function renderSharingMetaFields() { const { from, to } = getDateRange(); const dateRangePretty = prettyDuration( @@ -282,6 +304,10 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { ); } + function shouldRenderQueryEditorExtensions(): boolean { + return Boolean(queryEditorExtensionMap && Object.keys(queryEditorExtensionMap).length); + } + function renderUpdateButton() { const button = props.customSubmitButton ? ( React.cloneElement(props.customSubmitButton, { onClick: onClickSubmitButton }) @@ -374,6 +400,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { direction="column" justifyContent="flexEnd" > + {renderQueryEditorExtensions()} {renderQueryEditor()} 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 37b7d3d16105..9baeab489d4b 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 @@ -210,6 +210,7 @@ export function createSearchBar({ showSaveQuery={props.showSaveQuery} screenTitle={props.screenTitle} indexPatterns={props.indexPatterns} + dataSource={props.dataSource} indicateNoData={props.indicateNoData} timeHistory={data.query.timefilter.history} dateRangeFrom={timeRange.from} 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 19806e96e812..54e39fcb0b8d 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -28,27 +28,25 @@ * under the License. */ -import { compact } from 'lodash'; import { InjectedIntl, injectI18n } from '@osd/i18n/react'; import classNames from 'classnames'; +import { compact, get, isEqual } from 'lodash'; import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; -import { get, isEqual } from 'lodash'; - +import { DataSource } from '../..'; import { - withOpenSearchDashboards, OpenSearchDashboardsReactContextValue, + withOpenSearchDashboards, } from '../../../../opensearch_dashboards_react/public'; - -import QueryBarTopRow from '../query_string_input/query_bar_top_row'; -import { SavedQueryAttributes, TimeHistoryContract, SavedQuery } from '../../query'; +import { Filter, IIndexPattern, Query, TimeRange, UI_SETTINGS } from '../../../common'; +import { SavedQuery, SavedQueryAttributes, TimeHistoryContract } from '../../query'; import { IDataPluginServices } from '../../types'; -import { TimeRange, Query, Filter, IIndexPattern, UI_SETTINGS } from '../../../common'; import { FilterBar } from '../filter_bar/filter_bar'; +import { QueryEditorTopRow } from '../query_editor'; +import QueryBarTopRow from '../query_string_input/query_bar_top_row'; import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; import { SavedQueryManagementComponent } from '../saved_query_management'; import { Settings } from '../types'; -import { QueryEditorTopRow } from '../query_editor'; interface SearchBarInjectedDeps { opensearchDashboards: OpenSearchDashboardsReactContextValue; @@ -62,6 +60,7 @@ interface SearchBarInjectedDeps { export interface SearchBarOwnProps { indexPatterns?: IIndexPattern[]; + dataSource?: DataSource; isLoading?: boolean; customSubmitButton?: React.ReactNode; screenTitle?: string; @@ -497,6 +496,7 @@ class SearchBarUI extends Component { screenTitle={this.props.screenTitle} onSubmit={this.onQueryBarSubmit} indexPatterns={this.props.indexPatterns} + dataSource={this.props.dataSource} isLoading={this.props.isLoading} prepend={this.props.showFilterBar ? savedQueryManagement : undefined} showDatePicker={this.props.showDatePicker} diff --git a/src/plugins/data/public/ui/settings/settings.ts b/src/plugins/data/public/ui/settings/settings.ts index 8e335a1ffa49..10a72d66cdab 100644 --- a/src/plugins/data/public/ui/settings/settings.ts +++ b/src/plugins/data/public/ui/settings/settings.ts @@ -5,10 +5,11 @@ import { BehaviorSubject } from 'rxjs'; import { IStorageWrapper } from '../../../../opensearch_dashboards_utils/public'; -import { ConfigSchema } from '../../../config'; import { setOverrides as setFieldOverrides } from '../../../common'; -import { QueryEnhancement } from '../types'; +import { ConfigSchema } from '../../../config'; import { ISearchStart } from '../../search'; +import { QueryEditorExtensionConfig } from '../query_editor/query_editor_extensions'; +import { QueryEnhancement } from '../types'; export interface DataSettings { userQueryLanguage: string; @@ -31,7 +32,8 @@ export class Settings { private readonly config: ConfigSchema['enhancements'], private readonly search: ISearchStart, private readonly storage: IStorageWrapper, - private readonly queryEnhancements: Map + private readonly queryEnhancements: Map, + private readonly queryEditorExtensionMap: Record ) { this.isEnabled = this.config.enabled; this.setUserQueryEnhancementsEnabled(this.isEnabled); @@ -65,6 +67,10 @@ export class Settings { return this.queryEnhancements.get(language); } + getQueryEditorExtensionMap() { + return this.queryEditorExtensionMap; + } + getUserQueryLanguageBlocklist() { return this.storage.get('opensearchDashboards.userQueryLanguageBlocklist') || []; } @@ -149,8 +155,15 @@ interface Deps { search: ISearchStart; storage: IStorageWrapper; queryEnhancements: Map; + queryEditorExtensionMap: Record; } -export function createSettings({ config, search, storage, queryEnhancements }: Deps) { - return new Settings(config, search, storage, queryEnhancements); +export function createSettings({ + config, + search, + storage, + queryEnhancements, + queryEditorExtensionMap, +}: Deps) { + return new Settings(config, search, storage, queryEnhancements, queryEditorExtensionMap); } diff --git a/src/plugins/data/public/ui/types.ts b/src/plugins/data/public/ui/types.ts index 0b44e78c2937..bd157b7dd62d 100644 --- a/src/plugins/data/public/ui/types.ts +++ b/src/plugins/data/public/ui/types.ts @@ -7,7 +7,9 @@ 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'; import { Settings } from './settings'; +import { SuggestionsComponentProps } from './typeahead/suggestions_component'; export * from './settings'; @@ -44,6 +46,7 @@ export interface QueryEnhancement { export interface UiEnhancements { query?: QueryEnhancement; + queryEditorExtension?: QueryEditorExtensionConfig; } /** @@ -60,6 +63,7 @@ export interface IUiSetup { export interface IUiStart { IndexPatternSelect: React.ComponentType; SearchBar: React.ComponentType; + SuggestionsComponent: React.ComponentType; Settings: Settings; container$: Observable; } diff --git a/src/plugins/data/public/ui/ui_service.ts b/src/plugins/data/public/ui/ui_service.ts index 221273d63c72..e2dcae737acc 100644 --- a/src/plugins/data/public/ui/ui_service.ts +++ b/src/plugins/data/public/ui/ui_service.ts @@ -3,16 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/public'; import { BehaviorSubject } from 'rxjs'; -import { IUiStart, IUiSetup, QueryEnhancement, UiEnhancements } from './types'; - +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; +import { IStorageWrapper } from '../../../opensearch_dashboards_utils/public'; import { ConfigSchema } from '../../config'; +import { DataPublicPluginStart } from '../types'; import { createIndexPatternSelect } from './index_pattern_select'; +import { QueryEditorExtensionConfig } from './query_editor'; import { createSearchBar } from './search_bar/create_search_bar'; import { createSettings } from './settings'; -import { DataPublicPluginStart } from '../types'; -import { IStorageWrapper } from '../../../opensearch_dashboards_utils/public'; +import { SuggestionsComponent } from './typeahead'; +import { IUiSetup, IUiStart, QueryEnhancement, UiEnhancements } from './types'; /** @internal */ // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -27,6 +28,7 @@ export interface UiServiceStartDependencies { export class UiService implements Plugin { enhancementsConfig: ConfigSchema['enhancements']; private queryEnhancements: Map = new Map(); + private queryEditorExtensionMap: Record = {}; private container$ = new BehaviorSubject(null); constructor(initializerContext: PluginInitializerContext) { @@ -43,6 +45,10 @@ export class UiService implements Plugin { if (enhancements.query && enhancements.query.language) { this.queryEnhancements.set(enhancements.query.language, enhancements.query); } + if (enhancements.queryEditorExtension) { + this.queryEditorExtensionMap[enhancements.queryEditorExtension.id] = + enhancements.queryEditorExtension; + } }, }; } @@ -53,6 +59,7 @@ export class UiService implements Plugin { search: dataServices.search, storage, queryEnhancements: this.queryEnhancements, + queryEditorExtensionMap: this.queryEditorExtensionMap, }); const setContainerRef = (ref: HTMLDivElement | null) => { @@ -70,6 +77,7 @@ export class UiService implements Plugin { return { IndexPatternSelect: createIndexPatternSelect(core.savedObjects.client), SearchBar, + SuggestionsComponent, Settings, container$: this.container$, }; 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 feb7b91e7c5e..974f90548aeb 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 @@ -4,16 +4,16 @@ */ import React, { useEffect, useMemo, useState } from 'react'; -import { TimeRange, Query } from 'src/plugins/data/common'; +import { Query, TimeRange } from 'src/plugins/data/common'; import { AppMountParameters } from '../../../../../../core/public'; -import { PLUGIN_ID } from '../../../../common'; +import { connectStorageToQueryState, opensearchFilters } from '../../../../../data/public'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { PLUGIN_ID } from '../../../../common'; import { DiscoverViewServices } from '../../../build_services'; import { IndexPattern } from '../../../opensearch_dashboards_services'; import { getTopNavLinks } from '../../components/top_nav/get_top_nav_links'; -import { useDiscoverContext } from '../context'; import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; -import { opensearchFilters, connectStorageToQueryState } from '../../../../../data/public'; +import { useDiscoverContext } from '../context'; export interface TopNavProps { opts: { @@ -89,6 +89,10 @@ export const TopNav = ({ opts }: TopNavProps) => { useDefaultBehaviors setMenuMountPoint={opts.setHeaderActionMenu} indexPatterns={indexPattern ? [indexPattern] : indexPatterns} + // TODO after + // https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6833 + // is ported to main, pass dataSource to TopNavMenu by picking + // commit 328e08e688c again. onQuerySubmit={opts.onQuerySubmit} /> );