From 71bba11caf167f46711c2a734fce71c355a64c15 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 4 Jun 2024 14:11:01 -0700 Subject: [PATCH 01/10] [Discover-next] Add search bar extensions (#6894) 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. ```ts export interface SearchBarExtensionDependencies { /** * Currently selected index patterns. */ indexPatterns?: IIndexPattern[]; } export interface SearchBarExtensionConfig { /** * 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: () => Promise; /** * A function that returns the mount point for the search bar extension. * @param dependencies - The dependencies required for the extension. * @returns The mount point for the search bar extension. */ getComponent: (dependencies: SearchBarExtensionDependencies) => React.ReactElement; } export interface QueryEnhancement { ... searchBar?: { ... extensions?: SearchBarExtensionConfig[]; }; } Signed-off-by: Joshua Li (cherry picked from commit e748e814c22cd1b9e6ccac3f19dc75f99e47a2c7) --- src/plugins/data/public/index.ts | 3 + .../ui/query_editor/query_editor_top_row.tsx | 26 ++--- .../data/public/ui/search_bar/search_bar.tsx | 43 +++++++- .../public/ui/search_bar_extensions/index.ts | 7 ++ .../search_bar_extension.test.tsx | 94 ++++++++++++++++++ .../search_bar_extension.tsx | 65 +++++++++++++ .../search_bar_extensions.test.tsx | 97 +++++++++++++++++++ .../search_bar_extensions.tsx | 47 +++++++++ src/plugins/data/public/ui/types.ts | 2 + 9 files changed, 367 insertions(+), 17 deletions(-) create mode 100644 src/plugins/data/public/ui/search_bar_extensions/index.ts create mode 100644 src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx create mode 100644 src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx create mode 100644 src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx create mode 100644 src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 3ce43324ac31..4a69fd54e0bd 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -526,3 +526,6 @@ export { DataSourceGroup, DataSourceOption, } from './data_sources/datasource_selector'; + +export { SuggestionsComponent } from './ui'; +export { PersistedLog } from './query'; 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..67aec0a80134 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 @@ -4,36 +4,35 @@ */ 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 { Settings } from '../types'; +import { NoDataPopover } from './no_data_popover'; +import QueryEditorUI from './query_editor'; const QueryEditor = withOpenSearchDashboards(QueryEditorUI); @@ -65,6 +64,7 @@ export interface QueryEditorTopRowProps { isDirty: boolean; timeHistory?: TimeHistoryContract; indicateNoData?: boolean; + queryEditorRef: React.RefObject; } // Needed for React.lazy 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..d0f9bf00f3da 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,32 @@ * 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 { - 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 { IDataPluginServices } from '../../types'; import { TimeRange, Query, Filter, IIndexPattern, UI_SETTINGS } from '../../../common'; +import { Filter, IIndexPattern, Query, TimeRange } from '../../../common'; +import { SavedQuery, SavedQueryAttributes, TimeHistoryContract } from '../../query'; +import { IDataPluginServices } from '../../types'; import { FilterBar } from '../filter_bar/filter_bar'; +import QueryEditorTopRow from '../query_editor/query_editor_top_row'; +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'; +import { SearchBarExtensions } from '../search_bar_extensions'; +import { QueryEnhancement, Settings } from '../types'; interface SearchBarInjectedDeps { opensearchDashboards: OpenSearchDashboardsReactContextValue; @@ -122,6 +127,12 @@ class SearchBarUI extends Component { private services = this.props.opensearchDashboards.services; private savedQueryService = this.services.data.query.savedQueries; + /** + * queryEditorRef can't be bound to the actual editor + * https://github.com/react-monaco-editor/react-monaco-editor/blob/v0.27.0/src/editor.js#L113, + * currently it is an element above. + */ + public queryEditorRef = React.createRef(); public filterBarRef: Element | null = null; public filterBarWrapperRef: Element | null = null; @@ -242,6 +253,15 @@ class SearchBarUI extends Component { ); } + private shouldRenderExtensions() { + return ( + this.props.isEnhancementsEnabled && + (!!this.props.queryEnhancements?.get(this.state.query?.language!)?.searchBar?.extensions + ?.length ?? + false) + ); + } + /* * This Function is here to show the toggle in saved query form * in case you the date range (from/to) @@ -516,6 +536,20 @@ class SearchBarUI extends Component { filterBar={filterBar} dataTestSubj={this.props.dataTestSubj} indicateNoData={this.props.indicateNoData} + queryEditorRef={this.queryEditorRef} + /> + ); + } + + let searchBarExtensions; + if (this.shouldRenderExtensions() && this.queryEditorRef.current) { + searchBarExtensions = ( + ); } @@ -525,6 +559,7 @@ class SearchBarUI extends Component { return (
{queryBar} + {searchBarExtensions} {queryEditor} {!isEnhancementsEnabledOverride && filterBar} diff --git a/src/plugins/data/public/ui/search_bar_extensions/index.ts b/src/plugins/data/public/ui/search_bar_extensions/index.ts new file mode 100644 index 000000000000..d14971c671e3 --- /dev/null +++ b/src/plugins/data/public/ui/search_bar_extensions/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { SearchBarExtensionConfig } from './search_bar_extension'; +export { SearchBarExtensions } from './search_bar_extensions'; diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx new file mode 100644 index 000000000000..5abbe0200f27 --- /dev/null +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.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 { IIndexPattern } from '../../../common'; +import { SearchBarExtension } from './search_bar_extension'; + +jest.mock('@elastic/eui', () => ({ + ...jest.requireActual('@elastic/eui'), + EuiPortal: jest.fn(({ children }) =>
{children}
), + EuiErrorBoundary: jest.fn(({ children }) =>
{children}
), +})); + +type SearchBarExtensionProps = ComponentProps; + +const mockIndexPattern = { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], +} as IIndexPattern; + +describe('SearchBarExtension', () => { + const getComponentMock = jest.fn(); + const isEnabledMock = jest.fn(); + + const defaultProps: SearchBarExtensionProps = { + config: { + id: 'test-extension', + order: 1, + isEnabled: isEnabledMock, + getComponent: getComponentMock, + }, + dependencies: { + indexPatterns: [mockIndexPattern], + }, + portalInsert: { sibling: document.createElement('div'), position: 'after' }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly when isEnabled is true', async () => { + isEnabledMock.mockResolvedValue(true); + getComponentMock.mockReturnValue(
Test Component
); + + const { getByText } = render(); + + await waitFor(() => { + expect(getByText('Test Component')).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(); + }); + + it('calls isEnabled and getComponent correctly', async () => { + isEnabledMock.mockResolvedValue(true); + getComponentMock.mockReturnValue(
Test Component
); + + render(); + + await waitFor(() => { + expect(isEnabledMock).toHaveBeenCalled(); + }); + + expect(getComponentMock).toHaveBeenCalledWith(defaultProps.dependencies); + }); +}); diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx new file mode 100644 index 000000000000..e22ed9220dd3 --- /dev/null +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiErrorBoundary, EuiPortal } from '@elastic/eui'; +import { EuiPortalProps } from '@opensearch-project/oui'; +import React, { useEffect, useMemo, useState } from 'react'; +import { IIndexPattern } from '../../../common'; + +interface SearchBarExtensionProps { + config: SearchBarExtensionConfig; + dependencies: SearchBarExtensionDependencies; + portalInsert: EuiPortalProps['insert']; +} + +export interface SearchBarExtensionDependencies { + /** + * Currently selected index patterns. + */ + indexPatterns?: IIndexPattern[]; +} + +export interface SearchBarExtensionConfig { + /** + * 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: () => Promise; + /** + * A function that returns the mount point for the search bar extension. + * @param dependencies - The dependencies required for the extension. + * @returns The component the search bar extension. + */ + getComponent: (dependencies: SearchBarExtensionDependencies) => React.ReactElement; +} + +export const SearchBarExtension: React.FC = (props) => { + const [isEnabled, setIsEnabled] = useState(false); + + const component = useMemo(() => props.config.getComponent(props.dependencies), [ + props.config, + props.dependencies, + ]); + + useEffect(() => { + props.config.isEnabled().then(setIsEnabled); + }, [props.dependencies, props.config]); + + if (!isEnabled) return null; + + return ( + + {component} + + ); +}; diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx new file mode 100644 index 000000000000..52b3b87dc419 --- /dev/null +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx @@ -0,0 +1,97 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render, waitFor } from '@testing-library/react'; +import React, { ComponentProps } from 'react'; +import { SearchBarExtension } from './search_bar_extension'; +import { SearchBarExtensions } from './search_bar_extensions'; + +type SearchBarExtensionProps = ComponentProps; +type SearchBarExtensionsProps = ComponentProps; + +jest.mock('./search_bar_extension', () => ({ + SearchBarExtension: jest.fn(({ config, dependencies }: SearchBarExtensionProps) => ( +
+ Mocked SearchBarExtension {config.id} with{' '} + {dependencies.indexPatterns?.map((i) => i.title).join(', ')} +
+ )), +})); + +describe('SearchBarExtensions', () => { + const defaultProps: SearchBarExtensionsProps = { + dependencies: { + indexPatterns: [ + { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], + }, + ], + }, + portalInsert: { sibling: document.createElement('div'), position: 'after' }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders without any configurations', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it('throws error on duplicate configuration ids', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + const configs = [ + { id: '1', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }, + { id: '1', order: 2, isEnabled: jest.fn(), getComponent: jest.fn() }, + ]; + + expect(() => { + render(); + }).toThrow("Duplicate search bar extension id '1' found."); + }); + + it('correctly orders configurations based on order property', () => { + const configs = [ + { id: '1', order: 2, isEnabled: jest.fn(), getComponent: jest.fn() }, + { id: '2', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }, + ]; + + const { getAllByText } = render(); + const renderedExtensions = getAllByText(/Mocked SearchBarExtension/); + + expect(renderedExtensions).toHaveLength(2); + expect(renderedExtensions[0]).toHaveTextContent('2'); + expect(renderedExtensions[1]).toHaveTextContent('1'); + }); + + it('passes dependencies correctly to SearchBarExtension', async () => { + const configs = [{ id: '1', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }]; + + const { getByText } = render(); + + await waitFor(() => { + expect(getByText(/logstash-\*/)).toBeInTheDocument(); + }); + + expect(SearchBarExtension).toHaveBeenCalledWith( + expect.objectContaining({ + dependencies: defaultProps.dependencies, + }), + expect.anything() + ); + }); +}); diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx new file mode 100644 index 000000000000..cef34e46fa28 --- /dev/null +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiPortalProps } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { + SearchBarExtension, + SearchBarExtensionConfig, + SearchBarExtensionDependencies, +} from './search_bar_extension'; + +interface SearchBarExtensionsProps { + configs?: SearchBarExtensionConfig[]; + dependencies: SearchBarExtensionDependencies; + portalInsert: EuiPortalProps['insert']; +} + +export const SearchBarExtensions: React.FC = (props) => { + const configs = useMemo(() => { + if (!props.configs) return []; + + const seenIds = new Set(); + props.configs.forEach((config) => { + if (seenIds.has(config.id)) { + throw new Error(`Duplicate search bar extension id '${config.id}' found.`); + } + seenIds.add(config.id); + }); + + return [...props.configs].sort((a, b) => a.order - b.order); + }, [props.configs]); + + return ( + <> + {configs.map((config) => ( + + ))} + + ); +}; diff --git a/src/plugins/data/public/ui/types.ts b/src/plugins/data/public/ui/types.ts index 0b44e78c2937..3598890fb0f7 100644 --- a/src/plugins/data/public/ui/types.ts +++ b/src/plugins/data/public/ui/types.ts @@ -8,6 +8,7 @@ import { SearchInterceptor } from '../search'; import { IndexPatternSelectProps } from './index_pattern_select'; import { StatefulSearchBarProps } from './search_bar'; import { Settings } from './settings'; +import { SearchBarExtensionConfig } from './search_bar_extensions/search_bar_extension'; export * from './settings'; @@ -31,6 +32,7 @@ export interface QueryEnhancement { initialFrom?: string; initialTo?: string; }; + extensions?: SearchBarExtensionConfig[]; }; fields?: { filterable?: boolean; From d90734aae7d90cecef2607045bdbfccbd977e3f8 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Tue, 4 Jun 2024 15:11:58 -0700 Subject: [PATCH 02/10] [Discover-next] add query assist to query enhancements plugin (#6895) it adds query assist specific logic in query enhancements plugin to show a UI above the PPL search bar if user has configured PPL agent. Issues Resolved: #6820 * add query assist to query enhancements Signed-off-by: Joshua Li * align language to uppercase Signed-off-by: Joshua Li * pick PR 6167 Signed-off-by: Joshua Li * use useState hooks for query assist There is a bug in data explorer `AppContainer` where memorized `DiscoverCanvas` gets unmounted after `setQuery`. PR 6167 works around it by memorizing `AppContainer`. As query assist is no longer being unmounted, we can use proper hooks to persist state now. Signed-off-by: Joshua Li * Revert "pick PR 6167" This reverts commit acb0d41937e30bd76c666a225407743243692d11. Wait for official 6167 to merge to avoid conflict Signed-off-by: Joshua Li * address comments for PR 6894 Signed-off-by: Joshua Li --------- Signed-off-by: Joshua Li (cherry picked from commit 016e0f2f73efd8bb0649151908c67dd7ac09d174) --- src/plugins/data/public/index.ts | 1 - .../ui/query_editor/query_editor_top_row.tsx | 21 +++++++++- .../data/public/ui/search_bar/search_bar.tsx | 42 +------------------ .../search_bar_extension.tsx | 2 +- src/plugins/data/public/ui/types.ts | 4 +- src/plugins/data/public/ui/ui_service.ts | 2 + 6 files changed, 27 insertions(+), 45 deletions(-) diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 4a69fd54e0bd..cdd41eb25a4a 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -527,5 +527,4 @@ export { DataSourceOption, } from './data_sources/datasource_selector'; -export { SuggestionsComponent } from './ui'; export { PersistedLog } from './query'; 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 67aec0a80134..2a47e0ef0778 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,7 +2,6 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - import dateMath from '@elastic/datemath'; import { EuiFieldText, @@ -30,6 +29,7 @@ import { } from '../../../../opensearch_dashboards_react/public'; import { UI_SETTINGS } from '../../../common'; import { fromUser, getQueryLog, PersistedLog } from '../../query'; +import { SearchBarExtensions } from '../search_bar_extensions'; import { Settings } from '../types'; import { NoDataPopover } from './no_data_popover'; import QueryEditorUI from './query_editor'; @@ -64,7 +64,6 @@ export interface QueryEditorTopRowProps { isDirty: boolean; timeHistory?: TimeHistoryContract; indicateNoData?: boolean; - queryEditorRef: React.RefObject; } // Needed for React.lazy @@ -251,6 +250,17 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { ); } + function renderSearchBarExtensions() { + if (!shouldRenderSearchBarExtensions() || !queryEditorHeaderRef.current) return; + return ( + + ); + } + function renderSharingMetaFields() { const { from, to } = getDateRange(); const dateRangePretty = prettyDuration( @@ -282,6 +292,12 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { ); } + function shouldRenderSearchBarExtensions(): boolean { + return Boolean( + queryLanguage && props.queryEnhancements?.get(queryLanguage)?.searchBar?.extensions?.length + ); + } + function renderUpdateButton() { const button = props.customSubmitButton ? ( React.cloneElement(props.customSubmitButton, { onClick: onClickSubmitButton }) @@ -374,6 +390,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { direction="column" justifyContent="flexEnd" > + {renderSearchBarExtensions()} {renderQueryEditor()} 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 d0f9bf00f3da..620a7d633e01 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -37,23 +37,15 @@ import { OpenSearchDashboardsReactContextValue, withOpenSearchDashboards, } from '../../../../opensearch_dashboards_react/public'; - -import QueryBarTopRow from '../query_string_input/query_bar_top_row'; -import { SavedQueryAttributes, TimeHistoryContract, SavedQuery } from '../../query'; -import { IDataPluginServices } from '../../types'; -import { TimeRange, Query, Filter, IIndexPattern, UI_SETTINGS } from '../../../common'; -import { Filter, IIndexPattern, Query, TimeRange } from '../../../common'; +import { Filter, IIndexPattern, Query, TimeRange, UI_SETTINGS } from '../../../common'; import { SavedQuery, SavedQueryAttributes, TimeHistoryContract } from '../../query'; import { IDataPluginServices } from '../../types'; import { FilterBar } from '../filter_bar/filter_bar'; -import QueryEditorTopRow from '../query_editor/query_editor_top_row'; +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'; -import { SearchBarExtensions } from '../search_bar_extensions'; -import { QueryEnhancement, Settings } from '../types'; interface SearchBarInjectedDeps { opensearchDashboards: OpenSearchDashboardsReactContextValue; @@ -127,12 +119,6 @@ class SearchBarUI extends Component { private services = this.props.opensearchDashboards.services; private savedQueryService = this.services.data.query.savedQueries; - /** - * queryEditorRef can't be bound to the actual editor - * https://github.com/react-monaco-editor/react-monaco-editor/blob/v0.27.0/src/editor.js#L113, - * currently it is an element above. - */ - public queryEditorRef = React.createRef(); public filterBarRef: Element | null = null; public filterBarWrapperRef: Element | null = null; @@ -253,15 +239,6 @@ class SearchBarUI extends Component { ); } - private shouldRenderExtensions() { - return ( - this.props.isEnhancementsEnabled && - (!!this.props.queryEnhancements?.get(this.state.query?.language!)?.searchBar?.extensions - ?.length ?? - false) - ); - } - /* * This Function is here to show the toggle in saved query form * in case you the date range (from/to) @@ -536,20 +513,6 @@ class SearchBarUI extends Component { filterBar={filterBar} dataTestSubj={this.props.dataTestSubj} indicateNoData={this.props.indicateNoData} - queryEditorRef={this.queryEditorRef} - /> - ); - } - - let searchBarExtensions; - if (this.shouldRenderExtensions() && this.queryEditorRef.current) { - searchBarExtensions = ( - ); } @@ -559,7 +522,6 @@ class SearchBarUI extends Component { return (
{queryBar} - {searchBarExtensions} {queryEditor} {!isEnhancementsEnabledOverride && filterBar} diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx index e22ed9220dd3..88a3fcdfbb08 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx @@ -18,7 +18,7 @@ export interface SearchBarExtensionDependencies { /** * Currently selected index patterns. */ - indexPatterns?: IIndexPattern[]; + indexPatterns?: Array; } export interface SearchBarExtensionConfig { diff --git a/src/plugins/data/public/ui/types.ts b/src/plugins/data/public/ui/types.ts index 3598890fb0f7..105ab3922b52 100644 --- a/src/plugins/data/public/ui/types.ts +++ b/src/plugins/data/public/ui/types.ts @@ -8,7 +8,8 @@ import { SearchInterceptor } from '../search'; import { IndexPatternSelectProps } from './index_pattern_select'; import { StatefulSearchBarProps } from './search_bar'; import { Settings } from './settings'; -import { SearchBarExtensionConfig } from './search_bar_extensions/search_bar_extension'; +import { SearchBarExtensionConfig } from './search_bar_extensions'; +import { SuggestionsComponentProps } from './typeahead/suggestions_component'; export * from './settings'; @@ -62,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..eb171586c86c 100644 --- a/src/plugins/data/public/ui/ui_service.ts +++ b/src/plugins/data/public/ui/ui_service.ts @@ -13,6 +13,7 @@ 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'; /** @internal */ // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -70,6 +71,7 @@ export class UiService implements Plugin { return { IndexPatternSelect: createIndexPatternSelect(core.savedObjects.client), SearchBar, + SuggestionsComponent, Settings, container$: this.container$, }; From 08a81e0614bddb3513b2dc0fd252f517dd5c00ba Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Wed, 5 Jun 2024 19:16:29 -0700 Subject: [PATCH 03/10] [Discover-next] Address comments for search bar extensions and query assist (#6933) * pass dependencies to isEnabled func Signed-off-by: Joshua Li * add lazy and memo to search bar extensions Signed-off-by: Joshua Li * move ppl specific string out from query assist Signed-off-by: Joshua Li * prevent setstate after hook unmounts Signed-off-by: Joshua Li * add max-height to search bar extensions Signed-off-by: Joshua Li * prevent setstate after component unmounts Signed-off-by: Joshua Li * move ml-commons API to common/index.ts Signed-off-by: Joshua Li * improve i18n and accessibility usages Signed-off-by: Joshua Li * add hard-coded suggestions for sample data indices Signed-off-by: Joshua Li --------- Signed-off-by: Joshua Li (cherry picked from commit 4aade0f993559b0bae9cbcee8e889868afa88547) --- .../ui/query_editor/query_editor_top_row.tsx | 4 +- .../public/ui/search_bar_extensions/index.ts | 7 ---- .../public/ui/search_bar_extensions/index.tsx | 17 ++++++++ .../search_bar_extension.test.tsx | 22 ++-------- .../search_bar_extension.tsx | 29 ++++++++----- .../search_bar_extensions.test.tsx | 42 +++++++++---------- .../search_bar_extensions.tsx | 32 +++++++------- 7 files changed, 80 insertions(+), 73 deletions(-) delete mode 100644 src/plugins/data/public/ui/search_bar_extensions/index.ts create mode 100644 src/plugins/data/public/ui/search_bar_extensions/index.tsx 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 2a47e0ef0778..5f763d84bc79 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 @@ -255,8 +255,8 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { return ( ); } diff --git a/src/plugins/data/public/ui/search_bar_extensions/index.ts b/src/plugins/data/public/ui/search_bar_extensions/index.ts deleted file mode 100644 index d14971c671e3..000000000000 --- a/src/plugins/data/public/ui/search_bar_extensions/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export { SearchBarExtensionConfig } from './search_bar_extension'; -export { SearchBarExtensions } from './search_bar_extensions'; diff --git a/src/plugins/data/public/ui/search_bar_extensions/index.tsx b/src/plugins/data/public/ui/search_bar_extensions/index.tsx new file mode 100644 index 000000000000..ab790aa655c9 --- /dev/null +++ b/src/plugins/data/public/ui/search_bar_extensions/index.tsx @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ComponentProps } from 'react'; + +const Fallback = () =>
; + +const LazySearchBarExtensions = React.lazy(() => import('./search_bar_extensions')); +export const SearchBarExtensions = (props: ComponentProps) => ( + }> + + +); + +export { SearchBarExtensionConfig } from './search_bar_extension'; diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx index 5abbe0200f27..194b92ca9bfa 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx @@ -8,10 +8,9 @@ import React, { ComponentProps } from 'react'; import { IIndexPattern } from '../../../common'; import { SearchBarExtension } from './search_bar_extension'; -jest.mock('@elastic/eui', () => ({ - ...jest.requireActual('@elastic/eui'), - EuiPortal: jest.fn(({ children }) =>
{children}
), - EuiErrorBoundary: jest.fn(({ children }) =>
{children}
), +jest.mock('react-dom', () => ({ + ...jest.requireActual('react-dom'), + createPortal: jest.fn((element) => element), })); type SearchBarExtensionProps = ComponentProps; @@ -45,7 +44,7 @@ describe('SearchBarExtension', () => { dependencies: { indexPatterns: [mockIndexPattern], }, - portalInsert: { sibling: document.createElement('div'), position: 'after' }, + portalContainer: document.createElement('div'), }; beforeEach(() => { @@ -78,17 +77,4 @@ describe('SearchBarExtension', () => { expect(isEnabledMock).toHaveBeenCalled(); }); - - it('calls isEnabled and getComponent correctly', async () => { - isEnabledMock.mockResolvedValue(true); - getComponentMock.mockReturnValue(
Test Component
); - - render(); - - await waitFor(() => { - expect(isEnabledMock).toHaveBeenCalled(); - }); - - expect(getComponentMock).toHaveBeenCalledWith(defaultProps.dependencies); - }); }); diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx index 88a3fcdfbb08..505846c66b08 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx @@ -3,15 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiErrorBoundary, EuiPortal } from '@elastic/eui'; -import { EuiPortalProps } from '@opensearch-project/oui'; -import React, { useEffect, useMemo, useState } from 'react'; +import { EuiErrorBoundary } from '@elastic/eui'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import ReactDOM from 'react-dom'; import { IIndexPattern } from '../../../common'; interface SearchBarExtensionProps { config: SearchBarExtensionConfig; dependencies: SearchBarExtensionDependencies; - portalInsert: EuiPortalProps['insert']; + portalContainer: Element; } export interface SearchBarExtensionDependencies { @@ -34,7 +34,7 @@ export interface SearchBarExtensionConfig { * A function that determines if the search bar extension is enabled and should be rendered on UI. * @returns whether the extension is enabled. */ - isEnabled: () => Promise; + isEnabled: (dependencies: SearchBarExtensionDependencies) => Promise; /** * A function that returns the mount point for the search bar extension. * @param dependencies - The dependencies required for the extension. @@ -45,6 +45,7 @@ export interface SearchBarExtensionConfig { export const SearchBarExtension: React.FC = (props) => { const [isEnabled, setIsEnabled] = useState(false); + const isMounted = useRef(true); const component = useMemo(() => props.config.getComponent(props.dependencies), [ props.config, @@ -52,14 +53,22 @@ export const SearchBarExtension: React.FC = (props) => ]); useEffect(() => { - props.config.isEnabled().then(setIsEnabled); + 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 ( - - {component} - + return ReactDOM.createPortal( + {component}, + props.portalContainer ); }; diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx index 52b3b87dc419..2c11db0a56f8 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx @@ -6,7 +6,7 @@ import { render, waitFor } from '@testing-library/react'; import React, { ComponentProps } from 'react'; import { SearchBarExtension } from './search_bar_extension'; -import { SearchBarExtensions } from './search_bar_extensions'; +import SearchBarExtensions from './search_bar_extensions'; type SearchBarExtensionProps = ComponentProps; type SearchBarExtensionsProps = ComponentProps; @@ -15,32 +15,30 @@ jest.mock('./search_bar_extension', () => ({ SearchBarExtension: jest.fn(({ config, dependencies }: SearchBarExtensionProps) => (
Mocked SearchBarExtension {config.id} with{' '} - {dependencies.indexPatterns?.map((i) => i.title).join(', ')} + {dependencies.indexPatterns?.map((i) => (typeof i === 'string' ? i : i.title)).join(', ')}
)), })); describe('SearchBarExtensions', () => { const defaultProps: SearchBarExtensionsProps = { - dependencies: { - indexPatterns: [ - { - id: '1234', - title: 'logstash-*', - fields: [ - { - name: 'response', - type: 'number', - esTypes: ['integer'], - aggregatable: true, - filterable: true, - searchable: true, - }, - ], - }, - ], - }, - portalInsert: { sibling: document.createElement('div'), position: 'after' }, + indexPatterns: [ + { + id: '1234', + title: 'logstash-*', + fields: [ + { + name: 'response', + type: 'number', + esTypes: ['integer'], + aggregatable: true, + filterable: true, + searchable: true, + }, + ], + }, + ], + portalContainer: document.createElement('div'), }; beforeEach(() => { @@ -89,7 +87,7 @@ describe('SearchBarExtensions', () => { expect(SearchBarExtension).toHaveBeenCalledWith( expect.objectContaining({ - dependencies: defaultProps.dependencies, + dependencies: { indexPatterns: defaultProps.indexPatterns }, }), expect.anything() ); diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx index cef34e46fa28..2ae444c334d1 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiPortalProps } from '@elastic/eui'; import React, { useMemo } from 'react'; import { SearchBarExtension, @@ -11,37 +10,42 @@ import { SearchBarExtensionDependencies, } from './search_bar_extension'; -interface SearchBarExtensionsProps { +interface SearchBarExtensionsProps extends SearchBarExtensionDependencies { configs?: SearchBarExtensionConfig[]; - dependencies: SearchBarExtensionDependencies; - portalInsert: EuiPortalProps['insert']; + portalContainer: Element; } -export const SearchBarExtensions: React.FC = (props) => { - const configs = useMemo(() => { - if (!props.configs) return []; +const SearchBarExtensions: React.FC = React.memo((props) => { + const { configs, portalContainer, ...dependencies } = props; + + const sortedConfigs = useMemo(() => { + if (!configs) return []; const seenIds = new Set(); - props.configs.forEach((config) => { + configs.forEach((config) => { if (seenIds.has(config.id)) { throw new Error(`Duplicate search bar extension id '${config.id}' found.`); } seenIds.add(config.id); }); - return [...props.configs].sort((a, b) => a.order - b.order); - }, [props.configs]); + return [...configs].sort((a, b) => a.order - b.order); + }, [configs]); return ( <> - {configs.map((config) => ( + {sortedConfigs.map((config) => ( ))} ); -}; +}); + +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default SearchBarExtensions; From 5b2ad95486f70fd3cf9c7c901f015555fc3617a9 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Fri, 7 Jun 2024 15:08:18 -0700 Subject: [PATCH 04/10] [Discover-next] Support data sources for query assist (#6972) * disable query assist for non-default datasource Signed-off-by: Joshua Li * disable query assist input when loading Signed-off-by: Joshua Li * support MDS for query assist Signed-off-by: Joshua Li * add unit tests for agents Signed-off-by: Joshua Li * Revert "add unit tests for agents" This reverts commit 983514ee11362c5efe4cdb59802b3ff402b61ef2. The test configs are not yet setup in query_enhancements plugins. Signed-off-by: Joshua Li --------- Signed-off-by: Joshua Li (cherry picked from commit 328e08e688c39de1f47fee1c357e9928c0576390) --- .../data/public/ui/query_editor/query_editor_top_row.tsx | 7 +++---- .../data/public/ui/search_bar/create_search_bar.tsx | 1 + src/plugins/data/public/ui/search_bar/search_bar.tsx | 3 +++ .../ui/search_bar_extensions/search_bar_extension.tsx | 5 +++++ .../public/application/view_components/canvas/top_nav.tsx | 3 +++ 5 files changed, 15 insertions(+), 4 deletions(-) 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 5f763d84bc79..be2cecfe11bd 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 @@ -254,9 +254,10 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { if (!shouldRenderSearchBarExtensions() || !queryEditorHeaderRef.current) return; return ( ); } @@ -293,9 +294,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { } function shouldRenderSearchBarExtensions(): boolean { - return Boolean( - queryLanguage && props.queryEnhancements?.get(queryLanguage)?.searchBar?.extensions?.length - ); + return Boolean(queryUiEnhancement?.extensions?.length); } function renderUpdateButton() { 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 620a7d633e01..54e39fcb0b8d 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -33,6 +33,7 @@ import classNames from 'classnames'; import { compact, get, isEqual } from 'lodash'; import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; +import { DataSource } from '../..'; import { OpenSearchDashboardsReactContextValue, withOpenSearchDashboards, @@ -59,6 +60,7 @@ interface SearchBarInjectedDeps { export interface SearchBarOwnProps { indexPatterns?: IIndexPattern[]; + dataSource?: DataSource; isLoading?: boolean; customSubmitButton?: React.ReactNode; screenTitle?: string; @@ -494,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/search_bar_extensions/search_bar_extension.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx index 505846c66b08..bbcfc30b3318 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx @@ -7,6 +7,7 @@ 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 SearchBarExtensionProps { config: SearchBarExtensionConfig; @@ -19,6 +20,10 @@ export interface SearchBarExtensionDependencies { * Currently selected index patterns. */ indexPatterns?: Array; + /** + * Currently selected data source. + */ + dataSource?: DataSource; } export interface SearchBarExtensionConfig { 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..134362c7b7e8 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 @@ -14,6 +14,7 @@ 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 { useDataSource } from '../utils/use_datasource'; export interface TopNavProps { opts: { @@ -24,6 +25,7 @@ export interface TopNavProps { export const TopNav = ({ opts }: TopNavProps) => { const { services } = useOpenSearchDashboards(); + const dataSource = useDataSource(services); const { inspectorAdapters, savedSearch, indexPattern } = useDiscoverContext(); const [indexPatterns, setIndexPatterns] = useState(undefined); @@ -89,6 +91,7 @@ export const TopNav = ({ opts }: TopNavProps) => { useDefaultBehaviors setMenuMountPoint={opts.setHeaderActionMenu} indexPatterns={indexPattern ? [indexPattern] : indexPatterns} + dataSource={dataSource} onQuerySubmit={opts.onQuerySubmit} /> ); From 84a2013df0fc8d6524c71a7752911bb84fd28987 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Fri, 14 Jun 2024 18:51:04 +0000 Subject: [PATCH 05/10] partially revert 5b2ad95486f SearchBar extensions depend on the currently selected dataSource, which depends on https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6833 to be ported to main Signed-off-by: Joshua Li --- .../view_components/canvas/top_nav.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) 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 134362c7b7e8..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,17 +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 { useDataSource } from '../utils/use_datasource'; +import { useDiscoverContext } from '../context'; export interface TopNavProps { opts: { @@ -25,7 +24,6 @@ export interface TopNavProps { export const TopNav = ({ opts }: TopNavProps) => { const { services } = useOpenSearchDashboards(); - const dataSource = useDataSource(services); const { inspectorAdapters, savedSearch, indexPattern } = useDiscoverContext(); const [indexPatterns, setIndexPatterns] = useState(undefined); @@ -91,7 +89,10 @@ export const TopNav = ({ opts }: TopNavProps) => { useDefaultBehaviors setMenuMountPoint={opts.setHeaderActionMenu} indexPatterns={indexPattern ? [indexPattern] : indexPatterns} - dataSource={dataSource} + // 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} /> ); From e79828736264a3d8ffdb5e89155df00466b3c3f8 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Fri, 14 Jun 2024 22:21:23 +0000 Subject: [PATCH 06/10] add changelog fragment Signed-off-by: Joshua Li --- changelogs/fragments/7034.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/7034.yml 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)) From 8b0af9de0215da61d23f4223bbce97a68d226e49 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Fri, 14 Jun 2024 23:52:04 +0000 Subject: [PATCH 07/10] separate searchBarExtensions from queryEnhancements To support bannar callout for query assist in un-supported languages and notify user the feature, the search bar extensions need to be enabled for all languages. Signed-off-by: Joshua Li --- .../public/ui/query_editor/query_editor.tsx | 8 ++++ .../ui/query_editor/query_editor_top_row.tsx | 19 ++++++-- .../search_bar_extension.test.tsx | 8 +++- .../search_bar_extension.tsx | 47 ++++++++++++++++--- .../search_bar_extensions.test.tsx | 6 ++- .../search_bar_extensions.tsx | 8 ++-- .../data/public/ui/settings/settings.ts | 23 +++++++-- src/plugins/data/public/ui/types.ts | 4 +- src/plugins/data/public/ui/ui_service.ts | 15 ++++-- 9 files changed, 109 insertions(+), 29 deletions(-) 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_top_row.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx index be2cecfe11bd..01891f08911c 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 @@ -72,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; @@ -84,6 +85,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { props.settings && props.settings.getQueryEnhancements(queryLanguage)?.searchBar) || null; + const searchBarExtensions = props.settings?.getSearchBarExtensions(); const parsedQuery = !queryUiEnhancement || isValidQuery(props.query) ? props.query! @@ -245,17 +247,26 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { persistedLog={persistedLog} dataTestSubj={props.dataTestSubj} queryEditorHeaderRef={queryEditorHeaderRef} + queryEditorBannerRef={queryEditorBannerRef} /> ); } function renderSearchBarExtensions() { - if (!shouldRenderSearchBarExtensions() || !queryEditorHeaderRef.current) return; + if ( + !shouldRenderSearchBarExtensions() || + !queryEditorHeaderRef.current || + !queryEditorBannerRef.current || + !queryLanguage + ) + return; return ( @@ -294,7 +305,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { } function shouldRenderSearchBarExtensions(): boolean { - return Boolean(queryUiEnhancement?.extensions?.length); + return Boolean(searchBarExtensions?.length); } function renderUpdateButton() { diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx index 194b92ca9bfa..8d3cd08a326b 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx @@ -32,6 +32,7 @@ const mockIndexPattern = { describe('SearchBarExtension', () => { const getComponentMock = jest.fn(); + const getBannerMock = jest.fn(); const isEnabledMock = jest.fn(); const defaultProps: SearchBarExtensionProps = { @@ -40,11 +41,14 @@ describe('SearchBarExtension', () => { order: 1, isEnabled: isEnabledMock, getComponent: getComponentMock, + getBanner: getBannerMock, }, dependencies: { indexPatterns: [mockIndexPattern], + language: 'Test', }, - portalContainer: document.createElement('div'), + componentContainer: document.createElement('div'), + bannerContainer: document.createElement('div'), }; beforeEach(() => { @@ -54,11 +58,13 @@ describe('SearchBarExtension', () => { 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(); diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx index bbcfc30b3318..4a065c9b2d77 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx @@ -12,7 +12,8 @@ import { DataSource } from '../../data_sources/datasource'; interface SearchBarExtensionProps { config: SearchBarExtensionConfig; dependencies: SearchBarExtensionDependencies; - portalContainer: Element; + componentContainer: Element; + bannerContainer: Element; } export interface SearchBarExtensionDependencies { @@ -24,6 +25,10 @@ export interface SearchBarExtensionDependencies { * Currently selected data source. */ dataSource?: DataSource; + /** + * Currently selected query language. + */ + language: string; } export interface SearchBarExtensionConfig { @@ -41,18 +46,40 @@ export interface SearchBarExtensionConfig { */ isEnabled: (dependencies: SearchBarExtensionDependencies) => Promise; /** - * A function that returns the mount point for the search bar extension. + * 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: SearchBarExtensionDependencies) => 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. */ - getComponent: (dependencies: SearchBarExtensionDependencies) => React.ReactElement; + getBanner?: (dependencies: SearchBarExtensionDependencies) => React.ReactElement | null; } +const SearchBarExtensionPortal: React.FC<{ container: Element }> = (props) => { + if (!props.children) return null; + + return ReactDOM.createPortal( + {props.children}, + props.container + ); +}; + export const SearchBarExtension: React.FC = (props) => { const [isEnabled, setIsEnabled] = useState(false); const isMounted = useRef(true); - const component = useMemo(() => props.config.getComponent(props.dependencies), [ + const banner = useMemo(() => props.config.getBanner?.(props.dependencies), [ + props.config, + props.dependencies, + ]); + + const component = useMemo(() => props.config.getComponent?.(props.dependencies), [ props.config, props.dependencies, ]); @@ -72,8 +99,14 @@ export const SearchBarExtension: React.FC = (props) => if (!isEnabled) return null; - return ReactDOM.createPortal( - {component}, - props.portalContainer + return ( + <> + + {banner} + + + {component} + + ); }; diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx index 2c11db0a56f8..c5ab52f55474 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx @@ -38,7 +38,9 @@ describe('SearchBarExtensions', () => { ], }, ], - portalContainer: document.createElement('div'), + componentContainer: document.createElement('div'), + bannerContainer: document.createElement('div'), + language: 'Test', }; beforeEach(() => { @@ -87,7 +89,7 @@ describe('SearchBarExtensions', () => { expect(SearchBarExtension).toHaveBeenCalledWith( expect.objectContaining({ - dependencies: { indexPatterns: defaultProps.indexPatterns }, + dependencies: { indexPatterns: defaultProps.indexPatterns, language: 'Test' }, }), expect.anything() ); diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx index 2ae444c334d1..e5e4de781ece 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx +++ b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx @@ -12,11 +12,12 @@ import { interface SearchBarExtensionsProps extends SearchBarExtensionDependencies { configs?: SearchBarExtensionConfig[]; - portalContainer: Element; + componentContainer: Element; + bannerContainer: Element; } const SearchBarExtensions: React.FC = React.memo((props) => { - const { configs, portalContainer, ...dependencies } = props; + const { configs, componentContainer, bannerContainer, ...dependencies } = props; const sortedConfigs = useMemo(() => { if (!configs) return []; @@ -39,7 +40,8 @@ const SearchBarExtensions: React.FC = React.memo((prop key={config.id} config={config} dependencies={dependencies} - portalContainer={portalContainer} + componentContainer={componentContainer} + bannerContainer={bannerContainer} /> ))} diff --git a/src/plugins/data/public/ui/settings/settings.ts b/src/plugins/data/public/ui/settings/settings.ts index 8e335a1ffa49..9756028686b2 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 { SearchBarExtensionConfig } from '../search_bar_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 searchBarExtensions: SearchBarExtensionConfig[] ) { this.isEnabled = this.config.enabled; this.setUserQueryEnhancementsEnabled(this.isEnabled); @@ -65,6 +67,10 @@ export class Settings { return this.queryEnhancements.get(language); } + getSearchBarExtensions() { + return this.searchBarExtensions; + } + getUserQueryLanguageBlocklist() { return this.storage.get('opensearchDashboards.userQueryLanguageBlocklist') || []; } @@ -149,8 +155,15 @@ interface Deps { search: ISearchStart; storage: IStorageWrapper; queryEnhancements: Map; + searchBarExtensions: SearchBarExtensionConfig[]; } -export function createSettings({ config, search, storage, queryEnhancements }: Deps) { - return new Settings(config, search, storage, queryEnhancements); +export function createSettings({ + config, + search, + storage, + queryEnhancements, + searchBarExtensions, +}: Deps) { + return new Settings(config, search, storage, queryEnhancements, searchBarExtensions); } diff --git a/src/plugins/data/public/ui/types.ts b/src/plugins/data/public/ui/types.ts index 105ab3922b52..dd2017045282 100644 --- a/src/plugins/data/public/ui/types.ts +++ b/src/plugins/data/public/ui/types.ts @@ -7,8 +7,8 @@ import { Observable } from 'rxjs'; import { SearchInterceptor } from '../search'; import { IndexPatternSelectProps } from './index_pattern_select'; import { StatefulSearchBarProps } from './search_bar'; -import { Settings } from './settings'; import { SearchBarExtensionConfig } from './search_bar_extensions'; +import { Settings } from './settings'; import { SuggestionsComponentProps } from './typeahead/suggestions_component'; export * from './settings'; @@ -33,7 +33,6 @@ export interface QueryEnhancement { initialFrom?: string; initialTo?: string; }; - extensions?: SearchBarExtensionConfig[]; }; fields?: { filterable?: boolean; @@ -47,6 +46,7 @@ export interface QueryEnhancement { export interface UiEnhancements { query?: QueryEnhancement; + searchBarExtensions?: SearchBarExtensionConfig[]; } /** diff --git a/src/plugins/data/public/ui/ui_service.ts b/src/plugins/data/public/ui/ui_service.ts index eb171586c86c..e9a0ec0bb289 100644 --- a/src/plugins/data/public/ui/ui_service.ts +++ b/src/plugins/data/public/ui/ui_service.ts @@ -3,17 +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 { createSearchBar } from './search_bar/create_search_bar'; +import { SearchBarExtensionConfig } from './search_bar_extensions'; 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 @@ -28,6 +28,7 @@ export interface UiServiceStartDependencies { export class UiService implements Plugin { enhancementsConfig: ConfigSchema['enhancements']; private queryEnhancements: Map = new Map(); + private searchBarExtensions: SearchBarExtensionConfig[] = []; private container$ = new BehaviorSubject(null); constructor(initializerContext: PluginInitializerContext) { @@ -44,6 +45,9 @@ export class UiService implements Plugin { if (enhancements.query && enhancements.query.language) { this.queryEnhancements.set(enhancements.query.language, enhancements.query); } + if (enhancements.searchBarExtensions) { + this.searchBarExtensions.push(...enhancements.searchBarExtensions); + } }, }; } @@ -54,6 +58,7 @@ export class UiService implements Plugin { search: dataServices.search, storage, queryEnhancements: this.queryEnhancements, + searchBarExtensions: this.searchBarExtensions, }); const setContainerRef = (ref: HTMLDivElement | null) => { From edb0390f812aed046ee627c3079c0ecdc51bc537 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Mon, 17 Jun 2024 13:05:17 -0700 Subject: [PATCH 08/10] refactor search bar extensions move SearchBarExtensions to QueryEditorExtensions and change config settings from list to map. Signed-off-by: Joshua Li --- .../data/public/ui/query_editor/index.tsx | 2 + .../query_editor_extensions/index.tsx | 17 ++++ .../query_editor_extension.test.tsx} | 14 +-- .../query_editor_extension.tsx} | 32 +++--- .../query_editor_extensions.test.tsx | 94 ++++++++++++++++++ .../query_editor_extensions.tsx | 44 +++++++++ .../ui/query_editor/query_editor_top_row.tsx | 18 ++-- .../public/ui/search_bar_extensions/index.tsx | 17 ---- .../search_bar_extensions.test.tsx | 97 ------------------- .../search_bar_extensions.tsx | 53 ---------- .../data/public/ui/settings/settings.ts | 14 +-- src/plugins/data/public/ui/types.ts | 4 +- src/plugins/data/public/ui/ui_service.ts | 13 ++- 13 files changed, 206 insertions(+), 213 deletions(-) create mode 100644 src/plugins/data/public/ui/query_editor/query_editor_extensions/index.tsx rename src/plugins/data/public/ui/{search_bar_extensions/search_bar_extension.test.tsx => query_editor/query_editor_extensions/query_editor_extension.test.tsx} (81%) rename src/plugins/data/public/ui/{search_bar_extensions/search_bar_extension.tsx => query_editor/query_editor_extensions/query_editor_extension.tsx} (70%) 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 delete mode 100644 src/plugins/data/public/ui/search_bar_extensions/index.tsx delete mode 100644 src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx delete mode 100644 src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx 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_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/search_bar_extensions/search_bar_extension.test.tsx b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.test.tsx similarity index 81% rename from src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx rename to src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.test.tsx index 8d3cd08a326b..b3c8747e833d 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.test.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.test.tsx @@ -5,15 +5,15 @@ import { render, waitFor } from '@testing-library/react'; import React, { ComponentProps } from 'react'; -import { IIndexPattern } from '../../../common'; -import { SearchBarExtension } from './search_bar_extension'; +import { IIndexPattern } from '../../../../common'; +import { QueryEditorExtension } from './query_editor_extension'; jest.mock('react-dom', () => ({ ...jest.requireActual('react-dom'), createPortal: jest.fn((element) => element), })); -type SearchBarExtensionProps = ComponentProps; +type QueryEditorExtensionProps = ComponentProps; const mockIndexPattern = { id: '1234', @@ -30,12 +30,12 @@ const mockIndexPattern = { ], } as IIndexPattern; -describe('SearchBarExtension', () => { +describe('QueryEditorExtension', () => { const getComponentMock = jest.fn(); const getBannerMock = jest.fn(); const isEnabledMock = jest.fn(); - const defaultProps: SearchBarExtensionProps = { + const defaultProps: QueryEditorExtensionProps = { config: { id: 'test-extension', order: 1, @@ -60,7 +60,7 @@ describe('SearchBarExtension', () => { getComponentMock.mockReturnValue(
Test Component
); getBannerMock.mockReturnValue(
Test Banner
); - const { getByText } = render(); + const { getByText } = render(); await waitFor(() => { expect(getByText('Test Component')).toBeInTheDocument(); @@ -75,7 +75,7 @@ describe('SearchBarExtension', () => { isEnabledMock.mockResolvedValue(false); getComponentMock.mockReturnValue(
Test Component
); - const { queryByText } = render(); + const { queryByText } = render(); await waitFor(() => { expect(queryByText('Test Component')).toBeNull(); diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.tsx similarity index 70% rename from src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx rename to src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.tsx index 4a065c9b2d77..c7e3d1d14ce5 100644 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extension.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.tsx @@ -6,17 +6,17 @@ 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'; +import { IIndexPattern } from '../../../../common'; +import { DataSource } from '../../../data_sources/datasource'; -interface SearchBarExtensionProps { - config: SearchBarExtensionConfig; - dependencies: SearchBarExtensionDependencies; +interface QueryEditorExtensionProps { + config: QueryEditorExtensionConfig; + dependencies: QueryEditorExtensionDependencies; componentContainer: Element; bannerContainer: Element; } -export interface SearchBarExtensionDependencies { +export interface QueryEditorExtensionDependencies { /** * Currently selected index patterns. */ @@ -31,7 +31,7 @@ export interface SearchBarExtensionDependencies { language: string; } -export interface SearchBarExtensionConfig { +export interface QueryEditorExtensionConfig { /** * The id for the search bar extension. */ @@ -44,24 +44,24 @@ export interface SearchBarExtensionConfig { * 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: SearchBarExtensionDependencies) => Promise; + 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: SearchBarExtensionDependencies) => React.ReactElement | null; + 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: SearchBarExtensionDependencies) => React.ReactElement | null; + getBanner?: (dependencies: QueryEditorExtensionDependencies) => React.ReactElement | null; } -const SearchBarExtensionPortal: React.FC<{ container: Element }> = (props) => { +const QueryEditorExtensionPortal: React.FC<{ container: Element }> = (props) => { if (!props.children) return null; return ReactDOM.createPortal( @@ -70,7 +70,7 @@ const SearchBarExtensionPortal: React.FC<{ container: Element }> = (props) => { ); }; -export const SearchBarExtension: React.FC = (props) => { +export const QueryEditorExtension: React.FC = (props) => { const [isEnabled, setIsEnabled] = useState(false); const isMounted = useRef(true); @@ -101,12 +101,12 @@ export const SearchBarExtension: React.FC = (props) => 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..2062fae75b5f --- /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 = new Map([ + ['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 = new Map([ + ['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..399fc5e54ab0 --- /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?: Map; + componentContainer: Element; + bannerContainer: Element; +} + +const QueryEditorExtensions: React.FC = React.memo((props) => { + const { configMap, componentContainer, bannerContainer, ...dependencies } = props; + + const sortedConfigs = useMemo(() => { + if (!configMap?.size) 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 01891f08911c..f11eca65a821 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 @@ -29,7 +29,7 @@ import { } from '../../../../opensearch_dashboards_react/public'; import { UI_SETTINGS } from '../../../common'; import { fromUser, getQueryLog, PersistedLog } from '../../query'; -import { SearchBarExtensions } from '../search_bar_extensions'; +import { QueryEditorExtensions } from './query_editor_extensions'; import { Settings } from '../types'; import { NoDataPopover } from './no_data_popover'; import QueryEditorUI from './query_editor'; @@ -85,7 +85,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { props.settings && props.settings.getQueryEnhancements(queryLanguage)?.searchBar) || null; - const searchBarExtensions = props.settings?.getSearchBarExtensions(); + const queryEditorExtensions = props.settings?.getQueryEditorExtensions(); const parsedQuery = !queryUiEnhancement || isValidQuery(props.query) ? props.query! @@ -253,18 +253,18 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { ); } - function renderSearchBarExtensions() { + function renderQueryEditorExtensions() { if ( - !shouldRenderSearchBarExtensions() || + !shouldRenderQueryEditorExtensions() || !queryEditorHeaderRef.current || !queryEditorBannerRef.current || !queryLanguage ) return; return ( - - {renderSearchBarExtensions()} + {renderQueryEditorExtensions()} {renderQueryEditor()} diff --git a/src/plugins/data/public/ui/search_bar_extensions/index.tsx b/src/plugins/data/public/ui/search_bar_extensions/index.tsx deleted file mode 100644 index ab790aa655c9..000000000000 --- a/src/plugins/data/public/ui/search_bar_extensions/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { ComponentProps } from 'react'; - -const Fallback = () =>
; - -const LazySearchBarExtensions = React.lazy(() => import('./search_bar_extensions')); -export const SearchBarExtensions = (props: ComponentProps) => ( - }> - - -); - -export { SearchBarExtensionConfig } from './search_bar_extension'; diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx deleted file mode 100644 index c5ab52f55474..000000000000 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.test.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { render, waitFor } from '@testing-library/react'; -import React, { ComponentProps } from 'react'; -import { SearchBarExtension } from './search_bar_extension'; -import SearchBarExtensions from './search_bar_extensions'; - -type SearchBarExtensionProps = ComponentProps; -type SearchBarExtensionsProps = ComponentProps; - -jest.mock('./search_bar_extension', () => ({ - SearchBarExtension: jest.fn(({ config, dependencies }: SearchBarExtensionProps) => ( -
- Mocked SearchBarExtension {config.id} with{' '} - {dependencies.indexPatterns?.map((i) => (typeof i === 'string' ? i : i.title)).join(', ')} -
- )), -})); - -describe('SearchBarExtensions', () => { - const defaultProps: SearchBarExtensionsProps = { - 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('throws error on duplicate configuration ids', () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - const configs = [ - { id: '1', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }, - { id: '1', order: 2, isEnabled: jest.fn(), getComponent: jest.fn() }, - ]; - - expect(() => { - render(); - }).toThrow("Duplicate search bar extension id '1' found."); - }); - - it('correctly orders configurations based on order property', () => { - const configs = [ - { id: '1', order: 2, isEnabled: jest.fn(), getComponent: jest.fn() }, - { id: '2', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }, - ]; - - const { getAllByText } = render(); - const renderedExtensions = getAllByText(/Mocked SearchBarExtension/); - - expect(renderedExtensions).toHaveLength(2); - expect(renderedExtensions[0]).toHaveTextContent('2'); - expect(renderedExtensions[1]).toHaveTextContent('1'); - }); - - it('passes dependencies correctly to SearchBarExtension', async () => { - const configs = [{ id: '1', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }]; - - const { getByText } = render(); - - await waitFor(() => { - expect(getByText(/logstash-\*/)).toBeInTheDocument(); - }); - - expect(SearchBarExtension).toHaveBeenCalledWith( - expect.objectContaining({ - dependencies: { indexPatterns: defaultProps.indexPatterns, language: 'Test' }, - }), - expect.anything() - ); - }); -}); diff --git a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx b/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx deleted file mode 100644 index e5e4de781ece..000000000000 --- a/src/plugins/data/public/ui/search_bar_extensions/search_bar_extensions.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from 'react'; -import { - SearchBarExtension, - SearchBarExtensionConfig, - SearchBarExtensionDependencies, -} from './search_bar_extension'; - -interface SearchBarExtensionsProps extends SearchBarExtensionDependencies { - configs?: SearchBarExtensionConfig[]; - componentContainer: Element; - bannerContainer: Element; -} - -const SearchBarExtensions: React.FC = React.memo((props) => { - const { configs, componentContainer, bannerContainer, ...dependencies } = props; - - const sortedConfigs = useMemo(() => { - if (!configs) return []; - - const seenIds = new Set(); - configs.forEach((config) => { - if (seenIds.has(config.id)) { - throw new Error(`Duplicate search bar extension id '${config.id}' found.`); - } - seenIds.add(config.id); - }); - - return [...configs].sort((a, b) => a.order - b.order); - }, [configs]); - - return ( - <> - {sortedConfigs.map((config) => ( - - ))} - - ); -}); - -// Needed for React.lazy -// eslint-disable-next-line import/no-default-export -export default SearchBarExtensions; diff --git a/src/plugins/data/public/ui/settings/settings.ts b/src/plugins/data/public/ui/settings/settings.ts index 9756028686b2..cd5539d87721 100644 --- a/src/plugins/data/public/ui/settings/settings.ts +++ b/src/plugins/data/public/ui/settings/settings.ts @@ -8,7 +8,7 @@ import { IStorageWrapper } from '../../../../opensearch_dashboards_utils/public' import { setOverrides as setFieldOverrides } from '../../../common'; import { ConfigSchema } from '../../../config'; import { ISearchStart } from '../../search'; -import { SearchBarExtensionConfig } from '../search_bar_extensions'; +import { QueryEditorExtensionConfig } from '../query_editor/query_editor_extensions'; import { QueryEnhancement } from '../types'; export interface DataSettings { @@ -33,7 +33,7 @@ export class Settings { private readonly search: ISearchStart, private readonly storage: IStorageWrapper, private readonly queryEnhancements: Map, - private readonly searchBarExtensions: SearchBarExtensionConfig[] + private readonly queryEditorExtensions: Map ) { this.isEnabled = this.config.enabled; this.setUserQueryEnhancementsEnabled(this.isEnabled); @@ -67,8 +67,8 @@ export class Settings { return this.queryEnhancements.get(language); } - getSearchBarExtensions() { - return this.searchBarExtensions; + getQueryEditorExtensions() { + return this.queryEditorExtensions; } getUserQueryLanguageBlocklist() { @@ -155,7 +155,7 @@ interface Deps { search: ISearchStart; storage: IStorageWrapper; queryEnhancements: Map; - searchBarExtensions: SearchBarExtensionConfig[]; + queryEditorExtensions: Map; } export function createSettings({ @@ -163,7 +163,7 @@ export function createSettings({ search, storage, queryEnhancements, - searchBarExtensions, + queryEditorExtensions, }: Deps) { - return new Settings(config, search, storage, queryEnhancements, searchBarExtensions); + return new Settings(config, search, storage, queryEnhancements, queryEditorExtensions); } diff --git a/src/plugins/data/public/ui/types.ts b/src/plugins/data/public/ui/types.ts index dd2017045282..bd157b7dd62d 100644 --- a/src/plugins/data/public/ui/types.ts +++ b/src/plugins/data/public/ui/types.ts @@ -7,7 +7,7 @@ import { Observable } from 'rxjs'; import { SearchInterceptor } from '../search'; import { IndexPatternSelectProps } from './index_pattern_select'; import { StatefulSearchBarProps } from './search_bar'; -import { SearchBarExtensionConfig } from './search_bar_extensions'; +import { QueryEditorExtensionConfig } from './query_editor/query_editor_extensions'; import { Settings } from './settings'; import { SuggestionsComponentProps } from './typeahead/suggestions_component'; @@ -46,7 +46,7 @@ export interface QueryEnhancement { export interface UiEnhancements { query?: QueryEnhancement; - searchBarExtensions?: SearchBarExtensionConfig[]; + queryEditorExtension?: QueryEditorExtensionConfig; } /** diff --git a/src/plugins/data/public/ui/ui_service.ts b/src/plugins/data/public/ui/ui_service.ts index e9a0ec0bb289..ad9fbfd8527a 100644 --- a/src/plugins/data/public/ui/ui_service.ts +++ b/src/plugins/data/public/ui/ui_service.ts @@ -10,7 +10,7 @@ import { ConfigSchema } from '../../config'; import { DataPublicPluginStart } from '../types'; import { createIndexPatternSelect } from './index_pattern_select'; import { createSearchBar } from './search_bar/create_search_bar'; -import { SearchBarExtensionConfig } from './search_bar_extensions'; +import { QueryEditorExtensionConfig } from './query_editor'; import { createSettings } from './settings'; import { SuggestionsComponent } from './typeahead'; import { IUiSetup, IUiStart, QueryEnhancement, UiEnhancements } from './types'; @@ -28,7 +28,7 @@ export interface UiServiceStartDependencies { export class UiService implements Plugin { enhancementsConfig: ConfigSchema['enhancements']; private queryEnhancements: Map = new Map(); - private searchBarExtensions: SearchBarExtensionConfig[] = []; + private queryEditorExtensions: Map = new Map(); private container$ = new BehaviorSubject(null); constructor(initializerContext: PluginInitializerContext) { @@ -45,8 +45,11 @@ export class UiService implements Plugin { if (enhancements.query && enhancements.query.language) { this.queryEnhancements.set(enhancements.query.language, enhancements.query); } - if (enhancements.searchBarExtensions) { - this.searchBarExtensions.push(...enhancements.searchBarExtensions); + if (enhancements.queryEditorExtension) { + this.queryEditorExtensions.set( + enhancements.queryEditorExtension.id, + enhancements.queryEditorExtension + ); } }, }; @@ -58,7 +61,7 @@ export class UiService implements Plugin { search: dataServices.search, storage, queryEnhancements: this.queryEnhancements, - searchBarExtensions: this.searchBarExtensions, + queryEditorExtensions: this.queryEditorExtensions, }); const setContainerRef = (ref: HTMLDivElement | null) => { From 886ace5a627ebf8582338149d978f72383f753c3 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Mon, 17 Jun 2024 13:35:53 -0700 Subject: [PATCH 09/10] address comments Signed-off-by: Joshua Li --- src/plugins/data/public/index.ts | 3 +-- .../query_editor_extensions/query_editor_extension.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index cdd41eb25a4a..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'; @@ -526,5 +527,3 @@ export { DataSourceGroup, DataSourceOption, } from './data_sources/datasource_selector'; - -export { PersistedLog } from './query'; 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 index c7e3d1d14ce5..30b02f0f15dc 100644 --- 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 @@ -72,7 +72,7 @@ const QueryEditorExtensionPortal: React.FC<{ container: Element }> = (props) => export const QueryEditorExtension: React.FC = (props) => { const [isEnabled, setIsEnabled] = useState(false); - const isMounted = useRef(true); + const isMounted = useRef(false); const banner = useMemo(() => props.config.getBanner?.(props.dependencies), [ props.config, From 7f0e39eb9809c95b98069cc971611edc2cbbc62b Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Mon, 17 Jun 2024 21:04:52 +0000 Subject: [PATCH 10/10] change query editor extension configs from map to record Signed-off-by: Joshua Li --- .../query_editor_extensions.test.tsx | 16 ++++++++-------- .../query_editor_extensions.tsx | 4 ++-- .../ui/query_editor/query_editor_top_row.tsx | 6 +++--- src/plugins/data/public/ui/settings/settings.ts | 12 ++++++------ src/plugins/data/public/ui/ui_service.ts | 12 +++++------- 5 files changed, 24 insertions(+), 26 deletions(-) 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 index 2062fae75b5f..f3dcd43b13d0 100644 --- 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 @@ -53,15 +53,15 @@ describe('QueryEditorExtensions', () => { }); it('renders without any items in map', () => { - const { container } = render(); + const { container } = render(); expect(container).toBeEmptyDOMElement(); }); it('correctly orders configurations based on order property', () => { - const configMap = new Map([ - ['1', { id: '1', order: 2, isEnabled: jest.fn(), getComponent: jest.fn() }], - ['2', { id: '2', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }], - ]); + 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( @@ -74,9 +74,9 @@ describe('QueryEditorExtensions', () => { }); it('passes dependencies correctly to QueryEditorExtension', async () => { - const configMap = new Map([ - ['1', { id: '1', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }], - ]); + const configMap = { + '1': { id: '1', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }, + }; const { getByText } = render(); 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 index 399fc5e54ab0..6b2d5011216c 100644 --- 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 @@ -11,7 +11,7 @@ import { } from './query_editor_extension'; interface QueryEditorExtensionsProps extends QueryEditorExtensionDependencies { - configMap?: Map; + configMap?: Record; componentContainer: Element; bannerContainer: Element; } @@ -20,7 +20,7 @@ const QueryEditorExtensions: React.FC = React.memo(( const { configMap, componentContainer, bannerContainer, ...dependencies } = props; const sortedConfigs = useMemo(() => { - if (!configMap?.size) return []; + if (!configMap || !Object.keys(configMap)) return []; return Object.values(configMap).sort((a, b) => a.order - b.order); }, [configMap]); 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 f11eca65a821..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 @@ -85,7 +85,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { props.settings && props.settings.getQueryEnhancements(queryLanguage)?.searchBar) || null; - const queryEditorExtensions = props.settings?.getQueryEditorExtensions(); + const queryEditorExtensionMap = props.settings?.getQueryEditorExtensionMap(); const parsedQuery = !queryUiEnhancement || isValidQuery(props.query) ? props.query! @@ -264,7 +264,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { return ( , - private readonly queryEditorExtensions: Map + private readonly queryEditorExtensionMap: Record ) { this.isEnabled = this.config.enabled; this.setUserQueryEnhancementsEnabled(this.isEnabled); @@ -67,8 +67,8 @@ export class Settings { return this.queryEnhancements.get(language); } - getQueryEditorExtensions() { - return this.queryEditorExtensions; + getQueryEditorExtensionMap() { + return this.queryEditorExtensionMap; } getUserQueryLanguageBlocklist() { @@ -155,7 +155,7 @@ interface Deps { search: ISearchStart; storage: IStorageWrapper; queryEnhancements: Map; - queryEditorExtensions: Map; + queryEditorExtensionMap: Record; } export function createSettings({ @@ -163,7 +163,7 @@ export function createSettings({ search, storage, queryEnhancements, - queryEditorExtensions, + queryEditorExtensionMap, }: Deps) { - return new Settings(config, search, storage, queryEnhancements, queryEditorExtensions); + return new Settings(config, search, storage, queryEnhancements, queryEditorExtensionMap); } diff --git a/src/plugins/data/public/ui/ui_service.ts b/src/plugins/data/public/ui/ui_service.ts index ad9fbfd8527a..e2dcae737acc 100644 --- a/src/plugins/data/public/ui/ui_service.ts +++ b/src/plugins/data/public/ui/ui_service.ts @@ -9,8 +9,8 @@ import { IStorageWrapper } from '../../../opensearch_dashboards_utils/public'; import { ConfigSchema } from '../../config'; import { DataPublicPluginStart } from '../types'; import { createIndexPatternSelect } from './index_pattern_select'; -import { createSearchBar } from './search_bar/create_search_bar'; import { QueryEditorExtensionConfig } from './query_editor'; +import { createSearchBar } from './search_bar/create_search_bar'; import { createSettings } from './settings'; import { SuggestionsComponent } from './typeahead'; import { IUiSetup, IUiStart, QueryEnhancement, UiEnhancements } from './types'; @@ -28,7 +28,7 @@ export interface UiServiceStartDependencies { export class UiService implements Plugin { enhancementsConfig: ConfigSchema['enhancements']; private queryEnhancements: Map = new Map(); - private queryEditorExtensions: Map = new Map(); + private queryEditorExtensionMap: Record = {}; private container$ = new BehaviorSubject(null); constructor(initializerContext: PluginInitializerContext) { @@ -46,10 +46,8 @@ export class UiService implements Plugin { this.queryEnhancements.set(enhancements.query.language, enhancements.query); } if (enhancements.queryEditorExtension) { - this.queryEditorExtensions.set( - enhancements.queryEditorExtension.id, - enhancements.queryEditorExtension - ); + this.queryEditorExtensionMap[enhancements.queryEditorExtension.id] = + enhancements.queryEditorExtension; } }, }; @@ -61,7 +59,7 @@ export class UiService implements Plugin { search: dataServices.search, storage, queryEnhancements: this.queryEnhancements, - queryEditorExtensions: this.queryEditorExtensions, + queryEditorExtensionMap: this.queryEditorExtensionMap, }); const setContainerRef = (ref: HTMLDivElement | null) => {