From 7bc469b5c3c535cd9ebc62ab1cb1bc0d676642fc Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Thu, 24 Oct 2024 22:58:53 +0000 Subject: [PATCH 1/3] [Discover] notify user about field_type_tolerance for SQL/PPL in query footer Signed-off-by: Joshua Li --- .../public/doc_links/doc_links_service.ts | 2 + .../language_service/lib/query_result.tsx | 6 +- .../query_string/language_service/types.ts | 3 +- .../editors/default_editor/index.tsx | 4 +- .../public/ui/query_editor/query_editor.tsx | 12 +- .../query_enhancements/public/index.scss | 1 + .../query_enhancements/public/plugin.tsx | 13 +- .../query_assist/utils/create_extension.tsx | 6 +- .../query_editor_extensions/_index.scss | 15 ++ .../field_type_tolerance_info_icon.test.tsx | 81 +++++++++++ .../field_type_tolerance_info_icon.tsx | 134 ++++++++++++++++++ .../public/query_editor_extensions/index.ts | 1 + 12 files changed, 268 insertions(+), 10 deletions(-) create mode 100644 src/plugins/query_enhancements/public/query_editor_extensions/_index.scss create mode 100644 src/plugins/query_enhancements/public/query_editor_extensions/field_type_tolerance_info_icon.test.tsx create mode 100644 src/plugins/query_enhancements/public/query_editor_extensions/field_type_tolerance_info_icon.tsx diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 3a08e3b239b3..14e84b3f928b 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -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/`, }, }, }, @@ -979,6 +980,7 @@ export interface DocLinksStart { }; readonly sql: { readonly base: string; + readonly limitation: string; }; readonly ppl: { readonly base: string; diff --git a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx index 2e8ab769e2e4..b51ce70bcfcd 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx +++ b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx @@ -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" @@ -118,8 +119,9 @@ export function QueryResult(props: { queryStatus: QueryStatus }) { return ( {}} > diff --git a/src/plugins/data/public/query/query_string/language_service/types.ts b/src/plugins/data/public/query/query_string/language_service/types.ts index c80858d67102..ef1d4fdeedd4 100644 --- a/src/plugins/data/public/query/query_string/language_service/types.ts +++ b/src/plugins/data/public/query/query_string/language_service/types.ts @@ -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; @@ -63,4 +63,5 @@ export interface LanguageConfig { supportedAppNames?: string[]; hideDatePicker?: boolean; sampleQueries?: SampleQuery[]; + inputFooterItems?: DefaultInputProps['footerItems']; } diff --git a/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx b/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx index 1eaf373f2c8e..9a6eaad52958 100644 --- a/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx +++ b/src/plugins/data/public/ui/query_editor/editors/default_editor/index.tsx @@ -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; provideCompletionItems: monaco.languages.CompletionItemProvider['provideCompletionItems']; diff --git a/src/plugins/data/public/ui/query_editor/query_editor.tsx b/src/plugins/data/public/ui/query_editor/query_editor.tsx index e67a696de838..dcd77ad872ce 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -358,6 +358,9 @@ export default class QueryEditorUI extends Component { value: this.getQueryString(), }; + const languageFooterItems = this.languageManager.getLanguage(this.props.query.language) + ?.inputFooterItems; + const defaultInputProps: DefaultInputProps = { ...baseInputProps, onChange: this.onInputChange, @@ -387,12 +390,16 @@ export default class QueryEditorUI extends Component { > {this.props.query.dataset?.timeFieldName || ''} , + ...(languageFooterItems?.start || []), , ], end: [ + ...(languageFooterItems?.end || []), { {this.props.query.dataset?.timeFieldName || ''} , + ...(languageFooterItems?.start || []), , ], end: [ + ...(languageFooterItems?.end || []), ], + }, }; queryString.getLanguageService().registerLanguage(pplLanguageConfig); @@ -167,6 +175,9 @@ export class QueryEnhancementsPlugin query: `SELECT * FROM your_table WHERE description IS NOT NULL AND description != '';`, }, ], + inputFooterItems: { + start: [], + }, }; queryString.getLanguageService().registerLanguage(sqlLanguageConfig); diff --git a/src/plugins/query_enhancements/public/query_assist/utils/create_extension.tsx b/src/plugins/query_enhancements/public/query_assist/utils/create_extension.tsx index 6cfb81d90186..d0267f2f555e 100644 --- a/src/plugins/query_enhancements/public/query_assist/utils/create_extension.tsx +++ b/src/plugins/query_enhancements/public/query_assist/utils/create_extension.tsx @@ -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 []; diff --git a/src/plugins/query_enhancements/public/query_editor_extensions/_index.scss b/src/plugins/query_enhancements/public/query_editor_extensions/_index.scss new file mode 100644 index 000000000000..7bd6029462bd --- /dev/null +++ b/src/plugins/query_enhancements/public/query_editor_extensions/_index.scss @@ -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); + } + } +} diff --git a/src/plugins/query_enhancements/public/query_editor_extensions/field_type_tolerance_info_icon.test.tsx b/src/plugins/query_enhancements/public/query_editor_extensions/field_type_tolerance_info_icon.test.tsx new file mode 100644 index 000000000000..bf7f7960280b --- /dev/null +++ b/src/plugins/query_enhancements/public/query_editor_extensions/field_type_tolerance_info_icon.test.tsx @@ -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( + + + + ); + + 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(); + }); +}); diff --git a/src/plugins/query_enhancements/public/query_editor_extensions/field_type_tolerance_info_icon.tsx b/src/plugins/query_enhancements/public/query_editor_extensions/field_type_tolerance_info_icon.tsx new file mode 100644 index 000000000000..c5bc60e45a94 --- /dev/null +++ b/src/plugins/query_enhancements/public/query_editor_extensions/field_type_tolerance_info_icon.tsx @@ -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 = 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 = (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 ( + setIsPopoverOpen(!isPopoverOpen)} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + panelClassName="queryEnhancements" + > + +

+ + + + + + + + +

+

+ + + + +

+
+
+ ); +}; diff --git a/src/plugins/query_enhancements/public/query_editor_extensions/index.ts b/src/plugins/query_enhancements/public/query_editor_extensions/index.ts index 5c07eb04061f..2e3490330478 100644 --- a/src/plugins/query_enhancements/public/query_editor_extensions/index.ts +++ b/src/plugins/query_enhancements/public/query_editor_extensions/index.ts @@ -4,3 +4,4 @@ */ export { pplLanguageReference } from './ppl_language_reference'; export { sqlLanguageReference } from './sql_language_reference'; +export { FieldTypeToleranceInfoIcon } from './field_type_tolerance_info_icon'; From 428afbacfcc7f459de0ca28a3ae81739df50c72e Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Thu, 24 Oct 2024 23:42:56 +0000 Subject: [PATCH 2/3] rename SQL to OpenSearch SQL Signed-off-by: Joshua Li --- src/plugins/query_enhancements/public/plugin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/query_enhancements/public/plugin.tsx b/src/plugins/query_enhancements/public/plugin.tsx index 85c926fa76cb..196fc79c4784 100644 --- a/src/plugins/query_enhancements/public/plugin.tsx +++ b/src/plugins/query_enhancements/public/plugin.tsx @@ -101,7 +101,7 @@ export class QueryEnhancementsPlugin // Register SQL language const sqlLanguageConfig: LanguageConfig = { id: 'SQL', - title: 'SQL', + title: 'OpenSearch SQL', search: sqlSearchInterceptor, getQueryString: (query: Query) => { return `SELECT * FROM ${query.dataset?.title} LIMIT 10`; From 48cddb95068ebeb8a7e726f208a6e2ef31093610 Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 23:43:39 +0000 Subject: [PATCH 3/3] Changeset file for PR #8702 created/updated --- changelogs/fragments/8702.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/8702.yml diff --git a/changelogs/fragments/8702.yml b/changelogs/fragments/8702.yml new file mode 100644 index 000000000000..c5a2deda1d93 --- /dev/null +++ b/changelogs/fragments/8702.yml @@ -0,0 +1,2 @@ +feat: +- Notify user about field_type_tolerance for SQL/PPL in discover query footer ([#8702](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8702)) \ No newline at end of file