Skip to content
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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Copy link
Member

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:

if it's not already accessible?

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 @@
},
]);

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} />
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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"
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 @@

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 @@
);
}

private shouldRenderExtensions() {
Copy link
Member

Choose a reason for hiding this comment

The 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)
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The 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 = (

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 @@
return (
<div className={className} data-test-subj="globalQueryBar">
{queryBar}
{searchBarExtensions}
Copy link
Member

@kavilla kavilla Jun 4, 2024

Choose a reason for hiding this comment

The 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}

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
Loading