-
Notifications
You must be signed in to change notification settings - Fork 917
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Discover-next] Add query editor extensions (#7034)
### Description see #6894 This PR picks #6894, #6895, #6933, #6972 to main. Additionally, - separates extensions from query enhancements - adds banner support - partially revert #6972 as it's pending on the data source commit to main - renames search bar extension to query editor extension A query editor extension can display a UI component above the query editor and/or a banner above the language selector. The component has the ability to read and write discover search bar states to enhance the search experience for users. The configuration is part of UI Enhancements. ```ts export interface QueryEditorExtensionDependencies { /** * Currently selected index patterns. */ indexPatterns?: Array<IIndexPattern | string>; /** * Currently selected data source. */ dataSource?: DataSource; /** * Currently selected query language. */ language: string; } export interface QueryEditorExtensionConfig { /** * The id for the search bar extension. */ id: string; /** * Lower order indicates higher position on UI. */ order: number; /** * A function that determines if the search bar extension is enabled and should be rendered on UI. * @returns whether the extension is enabled. */ isEnabled: (dependencies: QueryEditorExtensionDependencies) => Promise<boolean>; /** * A function that returns the search bar extension component. The component * will be displayed on top of the query editor in the search bar. * @param dependencies - The dependencies required for the extension. * @returns The component the search bar extension. */ getComponent?: (dependencies: QueryEditorExtensionDependencies) => React.ReactElement | null; /** * A function that returns the search bar extension banner. The banner is a * component that will be displayed on top of the search bar. * @param dependencies - The dependencies required for the extension. * @returns The component the search bar extension. */ getBanner?: (dependencies: QueryEditorExtensionDependencies) => React.ReactElement | null; } export interface UiEnhancements { query?: QueryEnhancement; + queryEditorExtension?: QueryEditorExtensionConfig; } ``` Developers can utilize search bar extensions to add additional features to the search bar, such as query assist. Issues resolved: #6077 A search bar extension can display a UI component above the query bar. The component has the ability to read and write discover search bar states to enhance the search experience for users. The configuration is part of Query Enhancements. Signed-off-by: Joshua Li <[email protected]>
- Loading branch information
1 parent
50f1066
commit 4f54049
Showing
16 changed files
with
460 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
feat: | ||
- Add search bar extensions ([#7034](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7034)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
src/plugins/data/public/ui/query_editor/query_editor_extensions/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import React, { ComponentProps } from 'react'; | ||
|
||
const Fallback = () => <div />; | ||
|
||
const LazyQueryEditorExtensions = React.lazy(() => import('./query_editor_extensions')); | ||
export const QueryEditorExtensions = (props: ComponentProps<typeof LazyQueryEditorExtensions>) => ( | ||
<React.Suspense fallback={<Fallback />}> | ||
<LazyQueryEditorExtensions {...props} /> | ||
</React.Suspense> | ||
); | ||
|
||
export { QueryEditorExtensionConfig } from './query_editor_extension'; |
86 changes: 86 additions & 0 deletions
86
...ugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { render, waitFor } from '@testing-library/react'; | ||
import React, { ComponentProps } from 'react'; | ||
import { IIndexPattern } from '../../../../common'; | ||
import { QueryEditorExtension } from './query_editor_extension'; | ||
|
||
jest.mock('react-dom', () => ({ | ||
...jest.requireActual('react-dom'), | ||
createPortal: jest.fn((element) => element), | ||
})); | ||
|
||
type QueryEditorExtensionProps = ComponentProps<typeof QueryEditorExtension>; | ||
|
||
const mockIndexPattern = { | ||
id: '1234', | ||
title: 'logstash-*', | ||
fields: [ | ||
{ | ||
name: 'response', | ||
type: 'number', | ||
esTypes: ['integer'], | ||
aggregatable: true, | ||
filterable: true, | ||
searchable: true, | ||
}, | ||
], | ||
} as IIndexPattern; | ||
|
||
describe('QueryEditorExtension', () => { | ||
const getComponentMock = jest.fn(); | ||
const getBannerMock = jest.fn(); | ||
const isEnabledMock = jest.fn(); | ||
|
||
const defaultProps: QueryEditorExtensionProps = { | ||
config: { | ||
id: 'test-extension', | ||
order: 1, | ||
isEnabled: isEnabledMock, | ||
getComponent: getComponentMock, | ||
getBanner: getBannerMock, | ||
}, | ||
dependencies: { | ||
indexPatterns: [mockIndexPattern], | ||
language: 'Test', | ||
}, | ||
componentContainer: document.createElement('div'), | ||
bannerContainer: document.createElement('div'), | ||
}; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('renders correctly when isEnabled is true', async () => { | ||
isEnabledMock.mockResolvedValue(true); | ||
getComponentMock.mockReturnValue(<div>Test Component</div>); | ||
getBannerMock.mockReturnValue(<div>Test Banner</div>); | ||
|
||
const { getByText } = render(<QueryEditorExtension {...defaultProps} />); | ||
|
||
await waitFor(() => { | ||
expect(getByText('Test Component')).toBeInTheDocument(); | ||
expect(getByText('Test Banner')).toBeInTheDocument(); | ||
}); | ||
|
||
expect(isEnabledMock).toHaveBeenCalled(); | ||
expect(getComponentMock).toHaveBeenCalledWith(defaultProps.dependencies); | ||
}); | ||
|
||
it('does not render when isEnabled is false', async () => { | ||
isEnabledMock.mockResolvedValue(false); | ||
getComponentMock.mockReturnValue(<div>Test Component</div>); | ||
|
||
const { queryByText } = render(<QueryEditorExtension {...defaultProps} />); | ||
|
||
await waitFor(() => { | ||
expect(queryByText('Test Component')).toBeNull(); | ||
}); | ||
|
||
expect(isEnabledMock).toHaveBeenCalled(); | ||
}); | ||
}); |
112 changes: 112 additions & 0 deletions
112
src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extension.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { EuiErrorBoundary } from '@elastic/eui'; | ||
import React, { useEffect, useMemo, useRef, useState } from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import { IIndexPattern } from '../../../../common'; | ||
import { DataSource } from '../../../data_sources/datasource'; | ||
|
||
interface QueryEditorExtensionProps { | ||
config: QueryEditorExtensionConfig; | ||
dependencies: QueryEditorExtensionDependencies; | ||
componentContainer: Element; | ||
bannerContainer: Element; | ||
} | ||
|
||
export interface QueryEditorExtensionDependencies { | ||
/** | ||
* Currently selected index patterns. | ||
*/ | ||
indexPatterns?: Array<IIndexPattern | string>; | ||
/** | ||
* Currently selected data source. | ||
*/ | ||
dataSource?: DataSource; | ||
/** | ||
* Currently selected query language. | ||
*/ | ||
language: string; | ||
} | ||
|
||
export interface QueryEditorExtensionConfig { | ||
/** | ||
* The id for the search bar extension. | ||
*/ | ||
id: string; | ||
/** | ||
* Lower order indicates higher position on UI. | ||
*/ | ||
order: number; | ||
/** | ||
* A function that determines if the search bar extension is enabled and should be rendered on UI. | ||
* @returns whether the extension is enabled. | ||
*/ | ||
isEnabled: (dependencies: QueryEditorExtensionDependencies) => Promise<boolean>; | ||
/** | ||
* A function that returns the search bar extension component. The component | ||
* will be displayed on top of the query editor in the search bar. | ||
* @param dependencies - The dependencies required for the extension. | ||
* @returns The component the search bar extension. | ||
*/ | ||
getComponent?: (dependencies: QueryEditorExtensionDependencies) => React.ReactElement | null; | ||
/** | ||
* A function that returns the search bar extension banner. The banner is a | ||
* component that will be displayed on top of the search bar. | ||
* @param dependencies - The dependencies required for the extension. | ||
* @returns The component the search bar extension. | ||
*/ | ||
getBanner?: (dependencies: QueryEditorExtensionDependencies) => React.ReactElement | null; | ||
} | ||
|
||
const QueryEditorExtensionPortal: React.FC<{ container: Element }> = (props) => { | ||
if (!props.children) return null; | ||
|
||
return ReactDOM.createPortal( | ||
<EuiErrorBoundary>{props.children}</EuiErrorBoundary>, | ||
props.container | ||
); | ||
}; | ||
|
||
export const QueryEditorExtension: React.FC<QueryEditorExtensionProps> = (props) => { | ||
const [isEnabled, setIsEnabled] = useState(false); | ||
const isMounted = useRef(false); | ||
|
||
const banner = useMemo(() => props.config.getBanner?.(props.dependencies), [ | ||
props.config, | ||
props.dependencies, | ||
]); | ||
|
||
const component = useMemo(() => props.config.getComponent?.(props.dependencies), [ | ||
props.config, | ||
props.dependencies, | ||
]); | ||
|
||
useEffect(() => { | ||
isMounted.current = true; | ||
return () => { | ||
isMounted.current = false; | ||
}; | ||
}, []); | ||
|
||
useEffect(() => { | ||
props.config.isEnabled(props.dependencies).then((enabled) => { | ||
if (isMounted.current) setIsEnabled(enabled); | ||
}); | ||
}, [props.dependencies, props.config]); | ||
|
||
if (!isEnabled) return null; | ||
|
||
return ( | ||
<> | ||
<QueryEditorExtensionPortal container={props.bannerContainer}> | ||
{banner} | ||
</QueryEditorExtensionPortal> | ||
<QueryEditorExtensionPortal container={props.componentContainer}> | ||
{component} | ||
</QueryEditorExtensionPortal> | ||
</> | ||
); | ||
}; |
94 changes: 94 additions & 0 deletions
94
...gins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof QueryEditorExtension>; | ||
type QueryEditorExtensionsProps = ComponentProps<typeof QueryEditorExtensions>; | ||
|
||
jest.mock('./query_editor_extension', () => ({ | ||
QueryEditorExtension: jest.fn(({ config, dependencies }: QueryEditorExtensionProps) => ( | ||
<div> | ||
Mocked QueryEditorExtension {config.id} with{' '} | ||
{dependencies.indexPatterns?.map((i) => (typeof i === 'string' ? i : i.title)).join(', ')} | ||
</div> | ||
)), | ||
})); | ||
|
||
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(<QueryEditorExtensions {...defaultProps} />); | ||
expect(container).toBeEmptyDOMElement(); | ||
}); | ||
|
||
it('renders without any items in map', () => { | ||
const { container } = render(<QueryEditorExtensions {...defaultProps} configMap={{}} />); | ||
expect(container).toBeEmptyDOMElement(); | ||
}); | ||
|
||
it('correctly orders configurations based on order property', () => { | ||
const configMap = { | ||
'1': { id: '1', order: 2, isEnabled: jest.fn(), getComponent: jest.fn() }, | ||
'2': { id: '2', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }, | ||
}; | ||
|
||
const { getAllByText } = render( | ||
<QueryEditorExtensions {...defaultProps} configMap={configMap} /> | ||
); | ||
const renderedExtensions = getAllByText(/Mocked QueryEditorExtension/); | ||
|
||
expect(renderedExtensions).toHaveLength(2); | ||
expect(renderedExtensions[0]).toHaveTextContent('2'); | ||
expect(renderedExtensions[1]).toHaveTextContent('1'); | ||
}); | ||
|
||
it('passes dependencies correctly to QueryEditorExtension', async () => { | ||
const configMap = { | ||
'1': { id: '1', order: 1, isEnabled: jest.fn(), getComponent: jest.fn() }, | ||
}; | ||
|
||
const { getByText } = render(<QueryEditorExtensions {...defaultProps} configMap={configMap} />); | ||
|
||
await waitFor(() => { | ||
expect(getByText(/logstash-\*/)).toBeInTheDocument(); | ||
}); | ||
|
||
expect(QueryEditorExtension).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
dependencies: { indexPatterns: defaultProps.indexPatterns, language: 'Test' }, | ||
}), | ||
expect.anything() | ||
); | ||
}); | ||
}); |
44 changes: 44 additions & 0 deletions
44
src/plugins/data/public/ui/query_editor/query_editor_extensions/query_editor_extensions.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import React, { useMemo } from 'react'; | ||
import { | ||
QueryEditorExtension, | ||
QueryEditorExtensionConfig, | ||
QueryEditorExtensionDependencies, | ||
} from './query_editor_extension'; | ||
|
||
interface QueryEditorExtensionsProps extends QueryEditorExtensionDependencies { | ||
configMap?: Record<string, QueryEditorExtensionConfig>; | ||
componentContainer: Element; | ||
bannerContainer: Element; | ||
} | ||
|
||
const QueryEditorExtensions: React.FC<QueryEditorExtensionsProps> = React.memo((props) => { | ||
const { configMap, componentContainer, bannerContainer, ...dependencies } = props; | ||
|
||
const sortedConfigs = useMemo(() => { | ||
if (!configMap || !Object.keys(configMap)) return []; | ||
return Object.values(configMap).sort((a, b) => a.order - b.order); | ||
}, [configMap]); | ||
|
||
return ( | ||
<> | ||
{sortedConfigs.map((config) => ( | ||
<QueryEditorExtension | ||
key={config.id} | ||
config={config} | ||
dependencies={dependencies} | ||
componentContainer={componentContainer} | ||
bannerContainer={bannerContainer} | ||
/> | ||
))} | ||
</> | ||
); | ||
}); | ||
|
||
// Needed for React.lazy | ||
// eslint-disable-next-line import/no-default-export | ||
export default QueryEditorExtensions; |
Oops, something went wrong.