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 query editor extensions #7034

Merged
merged 10 commits into from
Jun 20, 2024
2 changes: 2 additions & 0 deletions changelogs/fragments/7034.yml
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))
2 changes: 2 additions & 0 deletions src/plugins/data/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,3 +526,5 @@ export {
DataSourceGroup,
DataSourceOption,
} from './data_sources/datasource_selector';

export { PersistedLog } from './query';
joshuali925 marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 8 additions & 0 deletions src/plugins/data/public/ui/query_editor/query_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export interface QueryEditorProps {
isInvalid?: boolean;
queryEditorHeaderRef: React.RefObject<HTMLDivElement>;
queryEditorHeaderClassName?: string;
joshuali925 marked this conversation as resolved.
Show resolved Hide resolved
queryEditorBannerRef: React.RefObject<HTMLDivElement>;
queryEditorBannerClassName?: string;
}

interface Props extends QueryEditorProps {
Expand Down Expand Up @@ -253,8 +255,14 @@ export default class QueryEditorUI extends Component<Props, State> {
this.props.queryEditorHeaderClassName
);

const queryEditorBannerClassName = classNames(
'osdQueryEditorBanner',
this.props.queryEditorBannerClassName
);

return (
<div className={className}>
<div ref={this.props.queryEditorBannerRef} className={queryEditorBannerClassName} />
<EuiFlexGroup gutterSize="xs" direction="column">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs">
Expand Down
55 changes: 41 additions & 14 deletions src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,37 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

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 { SearchBarExtensions } from '../search_bar_extensions';
import { Settings } from '../types';
import { NoDataPopover } from './no_data_popover';
import QueryEditorUI from './query_editor';

const QueryEditor = withOpenSearchDashboards(QueryEditorUI);

Expand Down Expand Up @@ -73,6 +72,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) {
const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false);
const [isQueryEditorFocused, setIsQueryEditorFocused] = useState(false);
const queryEditorHeaderRef = useRef<HTMLDivElement | null>(null);
const queryEditorBannerRef = useRef<HTMLDivElement | null>(null);
joshuali925 marked this conversation as resolved.
Show resolved Hide resolved

const opensearchDashboards = useOpenSearchDashboards<IDataPluginServices>();
const { uiSettings, storage, appName } = opensearchDashboards.services;
Expand All @@ -85,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!
Expand Down Expand Up @@ -246,11 +247,32 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) {
persistedLog={persistedLog}
dataTestSubj={props.dataTestSubj}
queryEditorHeaderRef={queryEditorHeaderRef}
queryEditorBannerRef={queryEditorBannerRef}
/>
</EuiFlexItem>
);
}

function renderSearchBarExtensions() {
if (
!shouldRenderSearchBarExtensions() ||
!queryEditorHeaderRef.current ||
!queryEditorBannerRef.current ||
!queryLanguage
)
return;
return (
<SearchBarExtensions
language={queryLanguage}
configs={searchBarExtensions}
componentContainer={queryEditorHeaderRef.current}
bannerContainer={queryEditorBannerRef.current}
indexPatterns={props.indexPatterns}
dataSource={props.dataSource}
/>
);
}

function renderSharingMetaFields() {
const { from, to } = getDateRange();
const dateRangePretty = prettyDuration(
Expand Down Expand Up @@ -282,6 +304,10 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) {
);
}

function shouldRenderSearchBarExtensions(): boolean {
return Boolean(searchBarExtensions?.length);
}

function renderUpdateButton() {
const button = props.customSubmitButton ? (
React.cloneElement(props.customSubmitButton, { onClick: onClickSubmitButton })
Expand Down Expand Up @@ -374,6 +400,7 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) {
direction="column"
justifyContent="flexEnd"
>
{renderSearchBarExtensions()}
{renderQueryEditor()}
<EuiFlexItem>
<EuiFlexGroup responsive={false} gutterSize="none">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
18 changes: 9 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,27 +28,25 @@
* under the License.
*/

import { compact } from 'lodash';
import { InjectedIntl, injectI18n } from '@osd/i18n/react';
import classNames from 'classnames';
import { compact, get, isEqual } from 'lodash';
joshuali925 marked this conversation as resolved.
Show resolved Hide resolved
import React, { Component } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
import { get, isEqual } from 'lodash';
joshuali925 marked this conversation as resolved.
Show resolved Hide resolved

import { DataSource } from '../..';
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 { Filter, IIndexPattern, Query, TimeRange, UI_SETTINGS } from '../../../common';
import { SavedQuery, SavedQueryAttributes, TimeHistoryContract } from '../../query';
import { IDataPluginServices } from '../../types';
import { TimeRange, Query, Filter, IIndexPattern, UI_SETTINGS } from '../../../common';
import { FilterBar } from '../filter_bar/filter_bar';
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';

interface SearchBarInjectedDeps {
opensearchDashboards: OpenSearchDashboardsReactContextValue<IDataPluginServices>;
Expand All @@ -62,6 +60,7 @@ interface SearchBarInjectedDeps {

export interface SearchBarOwnProps {
indexPatterns?: IIndexPattern[];
dataSource?: DataSource;
isLoading?: boolean;
customSubmitButton?: React.ReactNode;
screenTitle?: string;
Expand Down Expand Up @@ -497,6 +496,7 @@ class SearchBarUI extends Component<SearchBarProps, State> {
screenTitle={this.props.screenTitle}
onSubmit={this.onQueryBarSubmit}
indexPatterns={this.props.indexPatterns}
dataSource={this.props.dataSource}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quick question: do we know if this will have to be reset if toggling this feature off?

like if the select a datasource and toggle it off and that data source isn't available that should be fine right? or do we think in the future we will need to reset it like how i do here: https://github.com/opensearch-project/OpenSearch-Dashboards/blob/main/src/plugins/data/public/ui/settings/settings.ts#L52.

this 100% makes sense to me as you have it right now so I don't think we should change this because it's being passed in like index Patterns. but if we do think there might be implications on the toggle on and then off again, might be worth renaming the UI Settings service to something to else to be more descriptive and reset all that stuff in that service.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure what the behavior would be, i wasn't able to test with data source too much. will need to revisit when we have the test environment

isLoading={this.props.isLoading}
prepend={this.props.showFilterBar ? savedQueryManagement : undefined}
showDatePicker={this.props.showDatePicker}
Expand Down
17 changes: 17 additions & 0 deletions src/plugins/data/public/ui/search_bar_extensions/index.tsx
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 LazySearchBarExtensions = React.lazy(() => import('./search_bar_extensions'));
export const SearchBarExtensions = (props: ComponentProps<typeof LazySearchBarExtensions>) => (
<React.Suspense fallback={<Fallback />}>
<LazySearchBarExtensions {...props} />
</React.Suspense>
);

export { SearchBarExtensionConfig } from './search_bar_extension';
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 { SearchBarExtension } from './search_bar_extension';

jest.mock('react-dom', () => ({
...jest.requireActual('react-dom'),
createPortal: jest.fn((element) => element),
}));

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 getBannerMock = jest.fn();
const isEnabledMock = jest.fn();

const defaultProps: SearchBarExtensionProps = {
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(<SearchBarExtension {...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(<SearchBarExtension {...defaultProps} />);

await waitFor(() => {
expect(queryByText('Test Component')).toBeNull();
});

expect(isEnabledMock).toHaveBeenCalled();
});
});
Loading
Loading