From 0766b40e02ae65ae4e673ceb994612441fcc5338 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Thu, 20 Jun 2024 03:03:12 -0700 Subject: [PATCH] [Discover-next] Add query editor extensions (#7034) ### Description see #6894 This PR picks #6894, #6895, #6933, #6972 to main. Additionally, - separates extensions from query enhancements - adds banner support - partially revert #6972 as it's pending on the data source commit to main - renames search bar extension to query editor extension A query editor extension can display a UI component above the query editor and/or a banner above the language selector. The component has the ability to read and write discover search bar states to enhance the search experience for users. The configuration is part of UI Enhancements. ```ts 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; } export interface UiEnhancements { query?: QueryEnhancement; + queryEditorExtension?: QueryEditorExtensionConfig; } ``` Developers can utilize search bar extensions to add additional features to the search bar, such as query assist. Issues resolved: #6077 A search bar extension can display a UI component above the query bar. The component has the ability to read and write discover search bar states to enhance the search experience for users. The configuration is part of Query Enhancements. Signed-off-by: Joshua Li --- changelogs/fragments/7034.yml | 2 + src/plugins/data/public/index.ts | 1 + .../data/public/ui/query_editor/index.tsx | 2 + .../public/ui/query_editor/query_editor.tsx | 8 ++ .../query_editor_extensions/index.tsx | 17 +++ .../query_editor_extension.test.tsx | 86 ++++++++++++++ .../query_editor_extension.tsx | 112 ++++++++++++++++++ .../query_editor_extensions.test.tsx | 94 +++++++++++++++ .../query_editor_extensions.tsx | 44 +++++++ .../ui/query_editor/query_editor_top_row.tsx | 55 ++++++--- .../ui/search_bar/create_search_bar.tsx | 1 + .../data/public/ui/search_bar/search_bar.tsx | 18 +-- .../data/public/ui/settings/settings.ts | 23 +++- src/plugins/data/public/ui/types.ts | 4 + src/plugins/data/public/ui/ui_service.ts | 18 ++- .../view_components/canvas/top_nav.tsx | 12 +- 16 files changed, 460 insertions(+), 37 deletions(-) create mode 100644 changelogs/fragments/7034.yml create mode 100644 src/plugins/data/public/ui/query_editor/query_editor_extensions/index.tsx create mode 100644 src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.test.tsx create mode 100644 src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.tsx create mode 100644 src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.test.tsx create mode 100644 src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.tsx 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 1904a531780f..6ced5cb34ad9 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -47,6 +47,8 @@ export interface QueryEditorProps { isInvalid?: boolean; queryEditorHeaderRef: React.RefObject; queryEditorHeaderClassName?: string; + queryEditorBannerRef: React.RefObject; + queryEditorBannerClassName?: string; } interface Props extends QueryEditorProps { @@ -289,8 +291,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 f4d0dcf9073d..48541b3c5016 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} /> );