Skip to content

Commit

Permalink
add search bar extensions
Browse files Browse the repository at this point in the history
Signed-off-by: Joshua Li <[email protected]>
  • Loading branch information
joshuali925 committed Jun 4, 2024
1 parent 0589b24 commit 04661f7
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 23 deletions.
3 changes: 3 additions & 0 deletions src/plugins/data/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,6 @@ export {
DataSourceGroup,
DataSourceOption,
} from './data_sources/datasource_selector';

export { SuggestionsComponent } from './ui';
export { PersistedLog } from './query';
2 changes: 1 addition & 1 deletion src/plugins/data/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export class DataPublicPlugin
},
]);

const dataServices = {
const dataServices: Omit<DataPublicPluginStart, 'ui'> = {

Check warning on line 237 in src/plugins/data/public/plugin.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/plugin.ts#L237

Added line #L237 was not covered by tests
actions: {
createFiltersFromValueClickAction,
createFiltersFromRangeSelectAction,
Expand Down
25 changes: 12 additions & 13 deletions src/plugins/data/public/ui/query_editor/query_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand All @@ -57,6 +54,7 @@ export interface QueryEditorProps {
size?: SuggestionsListSize;
className?: string;
isInvalid?: boolean;
queryEditorRef: React.RefObject<HTMLDivElement>;
}

interface Props extends QueryEditorProps {
Expand Down Expand Up @@ -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} />
<CodeEditor
height={70}
languageId="xjson"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface QueryEditorTopRowProps {
isDirty: boolean;
timeHistory?: TimeHistoryContract;
indicateNoData?: boolean;
queryEditorRef: React.RefObject<HTMLDivElement>;
}

// Needed for React.lazy
Expand Down Expand Up @@ -238,6 +239,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) {
getQueryStringInitialValue={getQueryStringInitialValue}
persistedLog={persistedLog}
dataTestSubj={props.dataTestSubj}
queryEditorRef={props.queryEditorRef}
/>
</EuiFlexItem>
);
Expand Down
46 changes: 37 additions & 9 deletions src/plugins/data/public/ui/search_bar/search_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -125,6 +123,12 @@ class SearchBarUI extends Component<SearchBarProps, State> {

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;

Expand Down Expand Up @@ -239,6 +243,15 @@ class SearchBarUI extends Component<SearchBarProps, State> {
);
}

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)
Expand Down Expand Up @@ -512,6 +525,20 @@ class SearchBarUI extends Component<SearchBarProps, State> {
filterBar={filterBar}
dataTestSubj={this.props.dataTestSubj}
indicateNoData={this.props.indicateNoData}
queryEditorRef={this.queryEditorRef}
/>
);
}

let searchBarExtensions;
if (this.shouldRenderExtensions() && this.queryEditorRef.current) {
searchBarExtensions = (

Check warning on line 535 in src/plugins/data/public/ui/search_bar/search_bar.tsx

View check run for this annotation

Codecov / codecov/patch

src/plugins/data/public/ui/search_bar/search_bar.tsx#L535

Added line #L535 was not covered by tests
<SearchBarExtensions
configs={
this.props.queryEnhancements?.get(this.state.query?.language!)?.searchBar?.extensions
}
dependencies={{ indexPatterns: this.props.indexPatterns }}
portalInsert={{ sibling: this.queryEditorRef.current, position: 'before' }}
/>
);
}
Expand All @@ -521,6 +548,7 @@ class SearchBarUI extends Component<SearchBarProps, State> {
return (
<div className={className} data-test-subj="globalQueryBar">
{queryBar}
{searchBarExtensions}
{queryEditor}
{!!!this.props.isEnhancementsEnabled && filterBar}

Expand Down
7 changes: 7 additions & 0 deletions src/plugins/data/public/ui/search_bar_extensions/index.ts
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>
);
};
Loading

0 comments on commit 04661f7

Please sign in to comment.