Skip to content

Commit

Permalink
[Discover] notify user about field_type_tolerance for SQL/PPL in quer…
Browse files Browse the repository at this point in the history
…y footer

Signed-off-by: Joshua Li <[email protected]>
  • Loading branch information
joshuali925 committed Oct 24, 2024
1 parent 9a25d0d commit 7bc469b
Show file tree
Hide file tree
Showing 12 changed files with 268 additions and 10 deletions.
2 changes: 2 additions & 0 deletions src/core/public/doc_links/doc_links_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ export class DocLinksService {
sql: {
// https://opensearch.org/docs/latest/search-plugins/sql/sql/basic/
base: `${OPENSEARCH_WEBSITE_DOCS}/search-plugins/sql/sql/basic/`,
limitation: `${OPENSEARCH_WEBSITE_DOCS}/search-plugins/sql/limitation/`,
},
},
},
Expand Down Expand Up @@ -979,6 +980,7 @@ export interface DocLinksStart {
};
readonly sql: {
readonly base: string;
readonly limitation: string;
};
readonly ppl: {
readonly base: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function QueryResult(props: { queryStatus: QueryStatus }) {
color="text"
size="xs"
onClick={() => {}}
iconGap="m"
isLoading
data-test-subj="queryResultLoading"
className="editor__footerItem"
Expand Down Expand Up @@ -118,8 +119,9 @@ export function QueryResult(props: { queryStatus: QueryStatus }) {
return (
<EuiButtonEmpty
iconSide="left"
iconType={'checkInCircleEmpty'}
iconGap="s"
iconType="checkInCircleEmpty"
color="text"
iconGap="m"
size="xs"
onClick={() => {}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
QueryStringContract,
TimeRange,
} from '../../../../public';
import { EditorInstance } from '../../../ui/query_editor/editors';
import { DefaultInputProps, EditorInstance } from '../../../ui/query_editor/editors';

export interface RecentQueryItem {
id: number;
Expand Down Expand Up @@ -63,4 +63,5 @@ export interface LanguageConfig {
supportedAppNames?: string[];
hideDatePicker?: boolean;
sampleQueries?: SampleQuery[];
inputFooterItems?: DefaultInputProps['footerItems'];
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export interface DefaultInputProps extends React.JSX.IntrinsicAttributes {
onChange: (value: string) => void;
editorDidMount: (editor: any) => void;
footerItems?: {
start?: any[];
end?: any[];
start?: React.ReactNode[];
end?: React.ReactNode[];
};
headerRef?: React.RefObject<HTMLDivElement>;
provideCompletionItems: monaco.languages.CompletionItemProvider['provideCompletionItems'];
Expand Down
12 changes: 11 additions & 1 deletion src/plugins/data/public/ui/query_editor/query_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,9 @@ export default class QueryEditorUI extends Component<Props, State> {
value: this.getQueryString(),
};

const languageFooterItems = this.languageManager.getLanguage(this.props.query.language)
?.inputFooterItems;

const defaultInputProps: DefaultInputProps = {
...baseInputProps,
onChange: this.onInputChange,
Expand Down Expand Up @@ -387,12 +390,16 @@ export default class QueryEditorUI extends Component<Props, State> {
>
{this.props.query.dataset?.timeFieldName || ''}
</EuiText>,
...(languageFooterItems?.start || []),
<QueryResult queryStatus={this.props.queryStatus!} />,
],
end: [
...(languageFooterItems?.end || []),
<EuiButtonEmpty
iconSide="left"
iconType="clock"
iconGap="s"
color="text"
size="xs"
onClick={this.toggleRecentQueries}
className="queryEditor__footerItem"
Expand Down Expand Up @@ -445,13 +452,16 @@ export default class QueryEditorUI extends Component<Props, State> {
<EuiText size="xs" color="subdued" className="queryEditor__footerItem">
{this.props.query.dataset?.timeFieldName || ''}
</EuiText>,
...(languageFooterItems?.start || []),
<QueryResult queryStatus={this.props.queryStatus!} />,
],
end: [
...(languageFooterItems?.end || []),
<EuiButtonEmpty
iconSide="left"
iconType="clock"
iconGap="s"
iconGap="m"
color="text"
size="xs"
onClick={this.toggleRecentQueries}
className="queryEditor__footerItem"
Expand Down
1 change: 1 addition & 0 deletions src/plugins/query_enhancements/public/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
*/

@import "./query_assist";
@import "./query_editor_extensions";
13 changes: 12 additions & 1 deletion src/plugins/query_enhancements/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { i18n } from '@osd/i18n';
import React from 'react';
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '../../../core/public';
import { ConfigSchema } from '../common/config';
import { setData, setStorage } from './services';
Expand All @@ -19,7 +20,11 @@ import { LanguageConfig, Query } from '../../data/public';
import { s3TypeConfig } from './datasets';
import { createEditor, DefaultInput, SingleLineInput } from '../../data/public';
import { DataStorage } from '../../data/common';
import { pplLanguageReference, sqlLanguageReference } from './query_editor_extensions';
import {
FieldTypeToleranceInfoIcon,
pplLanguageReference,
sqlLanguageReference,
} from './query_editor_extensions';

export class QueryEnhancementsPlugin
implements
Expand Down Expand Up @@ -87,6 +92,9 @@ export class QueryEnhancementsPlugin
editor: enhancedPPLQueryEditor,
editorSupportedAppNames: ['discover'],
supportedAppNames: ['discover', 'data-explorer'],
inputFooterItems: {
start: [<FieldTypeToleranceInfoIcon core={core} data={data} />],
},
};
queryString.getLanguageService().registerLanguage(pplLanguageConfig);

Expand Down Expand Up @@ -167,6 +175,9 @@ export class QueryEnhancementsPlugin
query: `SELECT * FROM your_table WHERE description IS NOT NULL AND description != '';`,
},
],
inputFooterItems: {
start: [<FieldTypeToleranceInfoIcon core={core} data={data} />],
},
};
queryString.getLanguageService().registerLanguage(sqlLanguageConfig);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ const getAvailableLanguages$ = (http: HttpSetup, data: DataPublicPluginSetup) =>
// currently query assist tool relies on opensearch API to get index
// mappings, external data source types (e.g. s3) are not supported
if (
query.dataset?.dataSource?.type !== DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH && // datasource is MDS OpenSearch
query.dataset?.dataSource?.type !== 'DATA_SOURCE' && // datasource is MDS OpenSearch when using indexes
query.dataset?.type !== DEFAULT_DATA.SET_TYPES.INDEX_PATTERN // dataset is index pattern
query.dataset?.dataSource?.type !== DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH && // datasource is not MDS OpenSearch
query.dataset?.dataSource?.type !== 'DATA_SOURCE' && // datasource is not MDS OpenSearch when using indexes
query.dataset?.type !== DEFAULT_DATA.SET_TYPES.INDEX_PATTERN // dataset is not index pattern
)
return [];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

.queryEnhancements {
.sqlArrayInfoPopoverText {
width: 280px;

p {
// align with text after icon + gutter
margin-left: calc($euiSizeM + $euiSizeM);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import '@testing-library/jest-dom';
import { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';
import { IntlProvider } from 'react-intl';
import { coreMock } from '../../../../core/public/mocks';
import { DEFAULT_DATA } from '../../../data/common';
import { dataPluginMock } from '../../../data/public/mocks';
import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public';
import { FieldTypeToleranceInfoIcon } from './field_type_tolerance_info_icon';

jest.mock('../../../opensearch_dashboards_react/public', () => ({
useOpenSearchDashboards: jest.fn(),
}));

const coreSetupMock = coreMock.createSetup();
const dataMock = dataPluginMock.createSetupContract();
const getQueryMock = dataMock.query.queryString.getQuery as jest.Mock;
const startMock = coreMock.createStart();

describe('FieldTypeToleranceInfoIcon', () => {
const renderComponent = () =>
render(
<IntlProvider locale="en">
<FieldTypeToleranceInfoIcon core={coreSetupMock} data={dataMock} />
</IntlProvider>
);

beforeEach(() => {
jest.clearAllMocks();
localStorage.clear();
jest.useFakeTimers();
(useOpenSearchDashboards as jest.Mock).mockReturnValue({ services: startMock });
});

it('should render null when datasource is not OpenSearch', async () => {
getQueryMock.mockReturnValueOnce({ dataset: { dataSource: { type: 'S3' } } });
const { container } = renderComponent();
jest.runAllTimers();

await waitFor(() => expect(coreSetupMock.http.post).not.toHaveBeenCalled());
expect(container).toBeEmptyDOMElement();
});

it('should render null when field type tolerance is enabled', async () => {
coreSetupMock.http.post.mockResolvedValueOnce({
persistent: { 'plugins.query.field_type_tolerance': 'true' },
transient: {},
});
getQueryMock.mockReturnValueOnce({
dataset: { dataSource: { type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH } },
});

const { container } = renderComponent();
jest.runAllTimers();

await waitFor(() => expect(coreSetupMock.http.post).toHaveBeenCalled());
expect(container).toBeEmptyDOMElement();
});

it('should show popover if field type tolerance is disabled', async () => {
coreSetupMock.http.post.mockResolvedValueOnce({
persistent: { 'plugins.query.field_type_tolerance': 'false' },
transient: {},
});
getQueryMock.mockReturnValueOnce({
dataset: { dataSource: { type: DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH } },
});

const { getByRole, queryByText } = renderComponent();
jest.runAllTimers();

await waitFor(() => expect(getByRole('button')).toBeInTheDocument());
fireEvent.click(getByRole('button'));
expect(queryByText('No array datatype support')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiPopover,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@osd/i18n/react';
import { i18n } from '@osd/i18n';
import React, { useState } from 'react';
import { useEffectOnce } from 'react-use';
import { CoreSetup, DocLinksStart } from '../../../../core/public';
import { DEFAULT_DATA } from '../../../data/common';
import { DataPublicPluginSetup } from '../../../data/public';
import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public';

interface FieldTypeToleranceInfoIconProps {
core: CoreSetup;
data: DataPublicPluginSetup;
}

const SQL_ARRAY_INFO_FOOTER_STORAGE_KEY = 'queryEnhancements:sqlArrayInfoAcknowledged';
const FIELD_TYPE_TOLERANCE_SETTING_KEY = 'plugins.query.field_type_tolerance';

const fieldTypeToleranceEnabledByDataSource: Map<string | undefined, boolean> = new Map();

/**
* Info icon to be added in query editor footer to notify user about SQL/PPL
* field type tolerance. The icon should only be visible if field type
* tolerance is unset or set to false, and the selected datasource is
* OpenSearch Cluster. External datasources like S3 are not affected.
*/
export const FieldTypeToleranceInfoIcon: React.FC<FieldTypeToleranceInfoIconProps> = (props) => {
const { services } = useOpenSearchDashboards<{ docLinks: DocLinksStart }>();
const [isHidden, setIsHidden] = useState(true);
const [isPopoverOpen, _setIsPopoverOpen] = useState(false);
const setIsPopoverOpen: typeof _setIsPopoverOpen = (isOpen) => {
if (!isOpen) {
window.localStorage.setItem(SQL_ARRAY_INFO_FOOTER_STORAGE_KEY, 'true');
}
_setIsPopoverOpen(isOpen);
};

useEffectOnce(() => {
const query = props.data.query.queryString.getQuery();
if (
query.dataset?.dataSource?.type !== DEFAULT_DATA.SOURCE_TYPES.OPENSEARCH && // datasource is not MDS OpenSearch
query.dataset?.dataSource?.type !== 'DATA_SOURCE' && // datasource is not MDS OpenSearch when using indexes
query.dataset?.type !== DEFAULT_DATA.SET_TYPES.INDEX_PATTERN // dataset is not index pattern
)
return;

(async () => {
const dataSourceId = query.dataset?.dataSource?.id || undefined;
let isFieldTypeToleranceEnabled = fieldTypeToleranceEnabledByDataSource.get(dataSourceId);
if (isFieldTypeToleranceEnabled === undefined) {
isFieldTypeToleranceEnabled = await props.core.http
.post('/api/console/proxy', {
query: { path: '_cluster/settings?flat_settings=true', method: 'GET', dataSourceId },
})
.then(
(settings) =>
!!(
settings.persistent[FIELD_TYPE_TOLERANCE_SETTING_KEY] === 'true' ||
settings.transient[FIELD_TYPE_TOLERANCE_SETTING_KEY] === 'true'
)
)
.catch(() => true);
if (isFieldTypeToleranceEnabled === false) {
setIsHidden(false);
if (window.localStorage.getItem(SQL_ARRAY_INFO_FOOTER_STORAGE_KEY) !== 'true') {
// open popover after button rendering to position it correctly
setTimeout(() => setIsPopoverOpen(true), 1000);
}
}
}
})();
});

if (isHidden) return null;

return (
<EuiPopover
button={
<EuiButtonIcon
aria-label={i18n.translate('queryEnhancements.sqlArrayInfo.buttonIcon.ariaLabel', {
defaultMessage: 'Toggle field type tolerance information',
})}
iconType="iInCircle"
color="text"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
/>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
panelClassName="queryEnhancements"
>
<EuiText size="s" className="sqlArrayInfoPopoverText">
<h4>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="iInCircle" />
</EuiFlexItem>
<EuiFlexItem>
<FormattedMessage
id="queryEnhancements.sqlArrayInfo.title"
defaultMessage="No array datatype support"
/>
</EuiFlexItem>
</EuiFlexGroup>
</h4>
<p>
<FormattedMessage
id="queryEnhancements.sqlArrayInfo.message"
defaultMessage="Only the first element of multiple field values will be returned. "
/>
<EuiLink href={services.docLinks.links.noDocumentation.sql.limitation} target="_blank">
<FormattedMessage
id="queryEnhancements.sqlArrayInfo.learnMore"
defaultMessage="Learn more"
/>
</EuiLink>
</p>
</EuiText>
</EuiPopover>
);
};
Loading

0 comments on commit 7bc469b

Please sign in to comment.