-
Notifications
You must be signed in to change notification settings - Fork 917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Discover-next] Add search bar extensions #6894
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,36 +3,33 @@ | |
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import React, { Component, RefObject, createRef } from 'react'; | ||
import { i18n } from '@osd/i18n'; | ||
|
||
import classNames from 'classnames'; | ||
import { | ||
PopoverAnchorPosition, | ||
EuiButton, | ||
EuiFlexGroup, | ||
EuiFlexItem, | ||
EuiButton, | ||
EuiLink, | ||
htmlIdGenerator, | ||
PopoverAnchorPosition, | ||
} from '@elastic/eui'; | ||
|
||
import { i18n } from '@osd/i18n'; | ||
import { FormattedMessage } from '@osd/i18n/react'; | ||
import classNames from 'classnames'; | ||
import { isEqual, isFunction } from 'lodash'; | ||
import React, { Component, createRef, RefObject } from 'react'; | ||
import { Toast } from 'src/core/public'; | ||
import { Settings } from '..'; | ||
import { IDataPluginServices, IIndexPattern, Query, TimeRange } from '../..'; | ||
import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete'; | ||
|
||
import { | ||
CodeEditor, | ||
OpenSearchDashboardsReactContextValue, | ||
toMountPoint, | ||
} from '../../../../opensearch_dashboards_react/public'; | ||
import { fetchIndexPatterns } from './fetch_index_patterns'; | ||
import { QueryLanguageSwitcher } from './language_switcher'; | ||
import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../query'; | ||
import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete'; | ||
import { fromUser, getQueryLog, matchPairs, PersistedLog, toUser } from '../../query'; | ||
import { SuggestionsListSize } from '../typeahead/suggestions_component'; | ||
import { Settings } from '..'; | ||
import { DataSettings, QueryEnhancement } from '../types'; | ||
import { fetchIndexPatterns } from './fetch_index_patterns'; | ||
import { QueryLanguageSwitcher } from './language_switcher'; | ||
|
||
export interface QueryEditorProps { | ||
indexPatterns: Array<IIndexPattern | string>; | ||
|
@@ -57,6 +54,7 @@ export interface QueryEditorProps { | |
size?: SuggestionsListSize; | ||
className?: string; | ||
isInvalid?: boolean; | ||
queryEditorRef: React.RefObject<HTMLDivElement>; | ||
} | ||
|
||
interface Props extends QueryEditorProps { | ||
|
@@ -521,6 +519,7 @@ export default class QueryEditorUI extends Component<Props, State> { | |
</EuiFlexGroup> | ||
</EuiFlexItem> | ||
<EuiFlexItem onClick={this.onClickInput} grow={true}> | ||
<div ref={this.props.queryEditorRef} /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can this be renamed to like queryEditorHeaderRef? to avoid confusion. I'm fine with this name as well since we can think of this and the code editor the same items essentially. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we want to add a class and styling on this div as well? two things we should consider adding is a max-height and overflow scroll to restrict how long this could get based what is being portal'd in |
||
<CodeEditor | ||
height={70} | ||
languageId="xjson" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,26 +28,24 @@ | |
* 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 QueryEditorTopRow from '../query_editor/query_editor_top_row'; | ||
import { SavedQueryAttributes, TimeHistoryContract, SavedQuery } from '../../query'; | ||
import { Filter, IIndexPattern, Query, TimeRange } from '../../../common'; | ||
import { SavedQuery, SavedQueryAttributes, TimeHistoryContract } from '../../query'; | ||
import { IDataPluginServices } from '../../types'; | ||
import { TimeRange, Query, Filter, IIndexPattern } from '../../../common'; | ||
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 { SearchBarExtensions } from '../search_bar_extensions'; | ||
import { QueryEnhancement, Settings } from '../types'; | ||
|
||
interface SearchBarInjectedDeps { | ||
|
@@ -125,6 +123,12 @@ | |
|
||
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<HTMLDivElement>(); | ||
public filterBarRef: Element | null = null; | ||
public filterBarWrapperRef: Element | null = null; | ||
|
||
|
@@ -239,6 +243,15 @@ | |
); | ||
} | ||
|
||
private shouldRenderExtensions() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice! |
||
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) | ||
|
@@ -512,6 +525,20 @@ | |
filterBar={filterBar} | ||
dataTestSubj={this.props.dataTestSubj} | ||
indicateNoData={this.props.indicateNoData} | ||
queryEditorRef={this.queryEditorRef} | ||
/> | ||
); | ||
} | ||
|
||
let searchBarExtensions; | ||
if (this.shouldRenderExtensions() && this.queryEditorRef.current) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: do you think it's worth to shove this into the query editor top row? |
||
searchBarExtensions = ( | ||
<SearchBarExtensions | ||
configs={ | ||
this.props.queryEnhancements?.get(this.state.query?.language!)?.searchBar?.extensions | ||
} | ||
dependencies={{ indexPatterns: this.props.indexPatterns }} | ||
portalInsert={{ sibling: this.queryEditorRef.current, position: 'before' }} | ||
/> | ||
); | ||
} | ||
|
@@ -521,6 +548,7 @@ | |
return ( | ||
<div className={className} data-test-subj="globalQueryBar"> | ||
{queryBar} | ||
{searchBarExtensions} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. along with: https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6894/files#r1626586752, then we just have queryEditor here. i think probably because i didn't have the query editor work done while you working on this. and i promised i would clean it up but not sure if i will have the time. so if you rather keep this here for now for this pr and then a fast follow to clean it up that's cool. that is if you think it makes sense to put the extensions in the query editor top row. |
||
{queryEditor} | ||
{!!!this.props.isEnhancementsEnabled && filterBar} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; |
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 { IIndexPattern } from '../../../common'; | ||
import { SearchBarExtension } from './search_bar_extension'; | ||
|
||
jest.mock('@elastic/eui', () => ({ | ||
...jest.requireActual('@elastic/eui'), | ||
EuiPortal: jest.fn(({ children }) => <div>{children}</div>), | ||
EuiErrorBoundary: jest.fn(({ children }) => <div>{children}</div>), | ||
})); | ||
|
||
type SearchBarExtensionProps = ComponentProps<typeof SearchBarExtension>; | ||
|
||
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(<div>Test Component</div>); | ||
|
||
const { getByText } = render(<SearchBarExtension {...defaultProps} />); | ||
|
||
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(<div>Test Component</div>); | ||
|
||
const { queryByText } = render(<SearchBarExtension {...defaultProps} />); | ||
|
||
await waitFor(() => { | ||
expect(queryByText('Test Component')).toBeNull(); | ||
}); | ||
|
||
expect(isEnabledMock).toHaveBeenCalled(); | ||
}); | ||
|
||
it('calls isEnabled and getComponent correctly', async () => { | ||
isEnabledMock.mockResolvedValue(true); | ||
getComponentMock.mockReturnValue(<div>Test Component</div>); | ||
|
||
render(<SearchBarExtension {...defaultProps} />); | ||
|
||
await waitFor(() => { | ||
expect(isEnabledMock).toHaveBeenCalled(); | ||
}); | ||
|
||
expect(getComponentMock).toHaveBeenCalledWith(defaultProps.dependencies); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<boolean>; | ||
/** | ||
* 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<SearchBarExtensionProps> = (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 ( | ||
<EuiPortal insert={props.portalInsert}> | ||
<EuiErrorBoundary>{component}</EuiErrorBoundary> | ||
</EuiPortal> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
might be related to another PR. but for this could this be just apart of the ui service:
OpenSearch-Dashboards/src/plugins/data/public/ui/ui_service.ts
Line 64 in 830664c
if it's not already accessible?