diff --git a/changelogs/fragments/8616.yml b/changelogs/fragments/8616.yml new file mode 100644 index 000000000000..aa41137d4968 --- /dev/null +++ b/changelogs/fragments/8616.yml @@ -0,0 +1,2 @@ +feat: +- Adds sample queries and saved queries to Discover no results page ([#8616](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8616)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.ts b/src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.ts index 0764b061e699..5d230e0396bf 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index_pattern_type.ts @@ -4,6 +4,7 @@ */ import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; +import { i18n } from '@osd/i18n'; import { DataSourceAttributes } from '../../../../../../data_source/common/data_sources'; import { DEFAULT_DATA, @@ -70,6 +71,29 @@ export const indexPatternTypeConfig: DatasetTypeConfig = { } return ['kuery', 'lucene', 'PPL', 'SQL']; }, + + getSampleQueries: (dataset: Dataset, language: string) => { + switch (language) { + case 'PPL': + return [ + { + title: i18n.translate('data.indexPatternType.sampleQuery.basicPPLQuery', { + defaultMessage: 'Sample query for PPL', + }), + query: `source = ${dataset.title}`, + }, + ]; + case 'SQL': + return [ + { + title: i18n.translate('data.indexPatternType.sampleQuery.basicSQLQuery', { + defaultMessage: 'Sample query for SQL', + }), + query: `SELECT * FROM ${dataset.title} LIMIT 10`, + }, + ]; + } + }, }; const fetchIndexPatterns = async (client: SavedObjectsClientContract): Promise => { diff --git a/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts b/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts index 018fa90df397..655d3720dab2 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/lib/index_type.ts @@ -5,6 +5,7 @@ import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; import { map } from 'rxjs/operators'; +import { i18n } from '@osd/i18n'; import { DEFAULT_DATA, DataStructure, @@ -88,6 +89,29 @@ export const indexTypeConfig: DatasetTypeConfig = { supportedLanguages: (dataset: Dataset): string[] => { return ['SQL', 'PPL']; }, + + getSampleQueries: (dataset: Dataset, language: string) => { + switch (language) { + case 'PPL': + return [ + { + title: i18n.translate('data.indexType.sampleQuery.basicPPLQuery', { + defaultMessage: 'Sample query for PPL', + }), + query: `source = ${dataset.title}`, + }, + ]; + case 'SQL': + return [ + { + title: i18n.translate('data.indexType.sampleQuery.basicSQLQuery', { + defaultMessage: 'Sample query for SQL', + }), + query: `SELECT * FROM ${dataset.title} LIMIT 10`, + }, + ]; + } + }, }; const fetchDataSources = async (client: SavedObjectsClientContract) => { diff --git a/src/plugins/data/public/query/query_string/dataset_service/types.ts b/src/plugins/data/public/query/query_string/dataset_service/types.ts index 46b9bdabb5f4..43607fe49feb 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/types.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/types.ts @@ -71,4 +71,8 @@ export interface DatasetTypeConfig { * @see https://github.com/opensearch-project/OpenSearch-Dashboards/issues/8362. */ combineDataStructures?: (dataStructures: DataStructure[]) => DataStructure | undefined; + /** + * Returns a list of sample queries for this dataset type + */ + getSampleQueries?: (dataset: Dataset, language: string) => any; } diff --git a/src/plugins/data/public/query/query_string/language_service/lib/dql_language.ts b/src/plugins/data/public/query/query_string/language_service/lib/dql_language.ts index dfa3e2386da7..6816bf0d7121 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/dql_language.ts +++ b/src/plugins/data/public/query/query_string/language_service/lib/dql_language.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { i18n } from '@osd/i18n'; import { LanguageConfig } from '../types'; import { ISearchInterceptor } from '../../../../search'; @@ -25,5 +26,51 @@ export const getDQLLanguageConfig = ( showDocLinks: true, editorSupportedAppNames: ['discover'], supportedAppNames: ['discover', 'dashboards', 'visualize', 'data-explorer', 'vis-builder', '*'], + sampleQueries: [ + { + title: i18n.translate('data.dqlLanguage.sampleQuery.titleContainsWind', { + defaultMessage: 'The title field contains the word wind.', + }), + query: 'title: wind', + }, + { + title: i18n.translate('data.dqlLanguage.sampleQuery.titleContainsWindOrWindy', { + defaultMessage: 'The title field contains the word wind or the word windy.', + }), + query: 'title: (wind OR windy)', + }, + { + title: i18n.translate('data.dqlLanguage.sampleQuery.titleContainsPhraseWindRises', { + defaultMessage: 'The title field contains the phrase wind rises.', + }), + query: 'title: "wind rises"', + }, + { + title: i18n.translate('data.dqlLanguage.sampleQuery.titleKeywordExactMatch', { + defaultMessage: 'The title.keyword field exactly matches The wind rises.', + }), + query: 'title.keyword: The wind rises', + }, + { + title: i18n.translate('data.dqlLanguage.sampleQuery.titleFieldsContainWind', { + defaultMessage: + 'Any field that starts with title (for example, title and title.keyword) contains the word wind', + }), + query: 'title*: wind', + }, + { + title: i18n.translate('data.dqlLanguage.sampleQuery.articleTitleContainsWind', { + defaultMessage: + 'The field that starts with article and ends with title contains the word wind. Matches the field article title.', + }), + query: 'article*title: wind', + }, + { + title: i18n.translate('data.dqlLanguage.sampleQuery.descriptionFieldExists', { + defaultMessage: 'Documents in which the field description exists.', + }), + query: 'description:*', + }, + ], }; }; diff --git a/src/plugins/data/public/query/query_string/language_service/lib/lucene_language.ts b/src/plugins/data/public/query/query_string/language_service/lib/lucene_language.ts index b5d04f9e4a29..c42b14543633 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/lucene_language.ts +++ b/src/plugins/data/public/query/query_string/language_service/lib/lucene_language.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { i18n } from '@osd/i18n'; import { LanguageConfig } from '../types'; import { ISearchInterceptor } from '../../../../search'; @@ -25,5 +26,51 @@ export const getLuceneLanguageConfig = ( showDocLinks: true, editorSupportedAppNames: ['discover'], supportedAppNames: ['discover', 'dashboards', 'visualize', 'data-explorer', 'vis-builder', '*'], + sampleQueries: [ + { + title: i18n.translate('data.luceneLanguage.sampleQuery.titleContainsWind', { + defaultMessage: 'The title field contains the word wind.', + }), + query: 'title: wind', + }, + { + title: i18n.translate('data.luceneLanguage.sampleQuery.titleContainsWindOrWindy', { + defaultMessage: 'The title field contains the word wind or the word windy.', + }), + query: 'title: (wind OR windy)', + }, + { + title: i18n.translate('data.luceneLanguage.sampleQuery.titleContainsPhraseWindRises', { + defaultMessage: 'The title field contains the phrase wind rises.', + }), + query: 'title: "wind rises"', + }, + { + title: i18n.translate('data.luceneLanguage.sampleQuery.titleKeywordExactMatch', { + defaultMessage: 'The title.keyword field exactly matches The wind rises.', + }), + query: 'title.keyword: The wind rises', + }, + { + title: i18n.translate('data.luceneLanguage.sampleQuery.titleFieldsContainWind', { + defaultMessage: + 'Any field that starts with title (for example, title and title.keyword) contains the word wind', + }), + query: 'title*: wind', + }, + { + title: i18n.translate('data.luceneLanguage.sampleQuery.articleTitleContainsWind', { + defaultMessage: + 'The field that starts with article and ends with title contains the word wind. Matches the field article title.', + }), + query: 'article*title: wind', + }, + { + title: i18n.translate('data.luceneLanguage.sampleQuery.descriptionFieldExists', { + defaultMessage: 'Documents in which the field description exists.', + }), + query: 'description:*', + }, + ], }; }; 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 6fe7119789d4..0889f7e63950 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 @@ -35,6 +35,11 @@ export interface EditorEnhancements { queryEditorExtension?: QueryEditorExtensionConfig; } +export interface SampleQuery { + title: string; + query: string; +} + export interface LanguageConfig { id: string; title: string; @@ -53,4 +58,5 @@ export interface LanguageConfig { editorSupportedAppNames?: string[]; supportedAppNames?: string[]; hideDatePicker?: boolean; + sampleQueries?: SampleQuery[]; } diff --git a/src/plugins/data/public/ui/_common.scss b/src/plugins/data/public/ui/_common.scss index e74a1ccca6d9..fcd98b9c7b31 100644 --- a/src/plugins/data/public/ui/_common.scss +++ b/src/plugins/data/public/ui/_common.scss @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + .dataUI-centerPanel { height: 100%; width: 100%; diff --git a/src/plugins/discover/public/application/components/no_results/no_results.scss b/src/plugins/discover/public/application/components/no_results/no_results.scss new file mode 100644 index 000000000000..401aa9bb6f7a --- /dev/null +++ b/src/plugins/discover/public/application/components/no_results/no_results.scss @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.discoverNoResults-sampleContainer { + @include euiLegibilityMaxWidth(100%); + + margin: 0 auto; +} diff --git a/src/plugins/discover/public/application/components/no_results/no_results.tsx b/src/plugins/discover/public/application/components/no_results/no_results.tsx index 2c64bd6a0412..6c8421535fde 100644 --- a/src/plugins/discover/public/application/components/no_results/no_results.tsx +++ b/src/plugins/discover/public/application/components/no_results/no_results.tsx @@ -28,18 +28,44 @@ * under the License. */ -import React from 'react'; +import './no_results.scss'; +import React, { Fragment, useEffect, useMemo, useState } from 'react'; import { I18nProvider } from '@osd/i18n/react'; -import { EuiEmptyPrompt, EuiPanel, EuiText } from '@elastic/eui'; +import { + EuiEmptyPrompt, + EuiText, + EuiTabbedContent, + EuiCodeBlock, + EuiSpacer, + EuiPanel, +} from '@elastic/eui'; import { i18n } from '@osd/i18n'; +import { Query } from '../../../../../data/common'; +import { + DatasetServiceContract, + LanguageServiceContract, + SavedQuery, + SavedQueryService, +} from '../../../../../data/public/'; interface Props { + datasetService: DatasetServiceContract; + savedQuery: SavedQueryService; + languageService: LanguageServiceContract; + query: Query | undefined; timeFieldName?: string; queryLanguage?: string; } -export const DiscoverNoResults = ({ timeFieldName, queryLanguage }: Props) => { +export const DiscoverNoResults = ({ + datasetService, + savedQuery, + languageService, + query, + timeFieldName, + queryLanguage, +}: Props) => { // Commented out due to no usage in code // See: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/8149 // @@ -157,34 +183,118 @@ export const DiscoverNoResults = ({ timeFieldName, queryLanguage }: Props) => { // ); // } + const [savedQueries, setSavedQueries] = useState([]); + + useEffect(() => { + const fetchSavedQueries = async () => { + const { queries: savedQueryItems } = await savedQuery.findSavedQueries('', 1000); + setSavedQueries( + savedQueryItems.filter((sq) => query?.language === sq.attributes.query.language) + ); + }; + + fetchSavedQueries(); + }, [setSavedQueries, query, savedQuery]); + + const tabs = useMemo(() => { + const buildSampleQueryBlock = (sampleTitle: string, sampleQuery: string) => { + return ( + <> + {sampleTitle} + + {sampleQuery} + + + ); + }; + + const sampleQueries = []; + + // Samples for the dataset type + if (query?.dataset?.type) { + const datasetSampleQueries = datasetService + .getType(query.dataset.type) + ?.getSampleQueries?.(query.dataset, query.language); + if (Array.isArray(datasetSampleQueries)) sampleQueries.push(...datasetSampleQueries); + } + + // Samples for the language + if (query?.language) { + const languageSampleQueries = languageService.getLanguage(query.language)?.sampleQueries; + if (Array.isArray(languageSampleQueries)) sampleQueries.push(...languageSampleQueries); + } + + return [ + ...(sampleQueries.length > 0 + ? [ + { + id: 'sample_queries', + name: i18n.translate('discover.emptyPrompt.sampleQueries.title', { + defaultMessage: 'Sample Queries', + }), + content: ( + + + {sampleQueries + .slice(0, 5) + .map((sampleQuery) => + buildSampleQueryBlock(sampleQuery.title, sampleQuery.query) + )} + + ), + }, + ] + : []), + ...(savedQueries.length > 0 + ? [ + { + id: 'saved_queries', + name: i18n.translate('discover.emptyPrompt.savedQueries.title', { + defaultMessage: 'Saved Queries', + }), + content: ( + + + {savedQueries.map((sq) => + buildSampleQueryBlock(sq.id, sq.attributes.query.query as string) + )} + + ), + }, + ] + : []), + ]; + }, [datasetService, languageService, query, savedQueries]); + return ( - - -

- {i18n.translate('discover.emptyPrompt.title', { - defaultMessage: 'No Results', - })} -

- - } - body={ - -

- {i18n.translate('discover.emptyPrompt.body', { - defaultMessage: - 'Try selecting a different data source, expanding your time range or modifying the query & filters.', - })} -

-
- } - /> -
+ +

+ {i18n.translate('discover.emptyPrompt.title', { + defaultMessage: 'No Results', + })} +

+ + } + body={ + +

+ {i18n.translate('discover.emptyPrompt.body', { + defaultMessage: + 'Try selecting a different data source, expanding your time range or modifying the query & filters.', + })} +

+
+ } + /> +
+ +
); }; diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx index 4cf6a15dc7d5..af94e0e6f542 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -27,6 +27,7 @@ import { OpenSearchSearchHit } from '../../../application/doc_views/doc_views_ty import { buildColumns } from '../../utils/columns'; import './discover_canvas.scss'; import { HeaderVariant } from '../../../../../../core/public'; +import { Query } from '../../../../../../../src/plugins/data/common/types'; import { setIndexPattern, setSelectedDataset } from '../../../../../data_explorer/public'; import { NoIndexPatternsPanel, AdvancedSelector } from '../../../../../data/public'; import { Dataset } from '../../../../../data/common'; @@ -48,6 +49,9 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR data, overlays, } = services; + const datasetService = data.query.queryString.getDatasetService(); + const savedQuery = data.query.savedQueries; + const languageService = data.query.queryString.getLanguageService(); const { columns } = useSelector((state) => { const stateColumns = state.discover.columns; @@ -65,6 +69,7 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR ); const dispatch = useDispatch(); const prevIndexPattern = useRef(indexPattern); + const [query, setQuery] = useState(); const [fetchState, setFetchState] = useState({ status: data$.getValue().status, @@ -74,6 +79,9 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR const onQuerySubmit = useCallback( (payload, isUpdate) => { + if (payload?.query) { + setQuery(payload?.query); + } if (isUpdate === false) { refetch$.next(); } @@ -136,8 +144,8 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR // Update query and other necessary state const queryString = data.query.queryString; - const query = queryString.getInitialQueryByDataset(dataset); - queryString.setQuery(query); + const initialQuery = queryString.getInitialQueryByDataset(dataset); + queryString.setQuery(initialQuery); queryString.getDatasetService().addRecentDataset(dataset); }; @@ -193,10 +201,24 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR ) : ( <> {fetchState.status === ResultStatus.NO_RESULTS && ( - + )} {fetchState.status === ResultStatus.ERROR && ( - + )} {fetchState.status === ResultStatus.UNINITIALIZED && ( refetch$.next()} /> diff --git a/src/plugins/query_enhancements/public/datasets/s3_type.ts b/src/plugins/query_enhancements/public/datasets/s3_type.ts index a550464c4e66..2a26a7e5fcea 100644 --- a/src/plugins/query_enhancements/public/datasets/s3_type.ts +++ b/src/plugins/query_enhancements/public/datasets/s3_type.ts @@ -5,6 +5,7 @@ import { HttpSetup, SavedObjectsClientContract } from 'opensearch-dashboards/public'; import { trimEnd } from 'lodash'; +import { i18n } from '@osd/i18n'; import { DATA_STRUCTURE_META_TYPES, DEFAULT_DATA, @@ -102,6 +103,29 @@ export const s3TypeConfig: DatasetTypeConfig = { supportedLanguages: (dataset: Dataset): string[] => { return ['SQL']; }, + + getSampleQueries: (dataset: Dataset, language: string) => { + switch (language) { + case 'PPL': + return [ + { + title: i18n.translate('queryEnhancements.s3Type.sampleQuery.basicPPLQuery', { + defaultMessage: 'Sample query for PPL', + }), + query: `source = ${dataset.title}`, + }, + ]; + case 'SQL': + return [ + { + title: i18n.translate('queryEnhancements.s3Type.sampleQuery.basicSQLQuery', { + defaultMessage: 'Sample query for SQL', + }), + query: `SELECT * FROM ${dataset.title} LIMIT 10`, + }, + ]; + } + }, }; const fetch = async ( diff --git a/src/plugins/query_enhancements/public/plugin.tsx b/src/plugins/query_enhancements/public/plugin.tsx index 073e333c3204..ff3df7e9ce1c 100644 --- a/src/plugins/query_enhancements/public/plugin.tsx +++ b/src/plugins/query_enhancements/public/plugin.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { i18n } from '@osd/i18n'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '../../../core/public'; import { ConfigSchema } from '../common/config'; import { setData, setStorage } from './services'; @@ -100,6 +101,60 @@ export class QueryEnhancementsPlugin editorSupportedAppNames: ['discover'], supportedAppNames: ['discover', 'data-explorer'], hideDatePicker: true, + sampleQueries: [ + { + title: i18n.translate('queryEnhancements.sqlLanguage.sampleQuery.titleContainsWind', { + defaultMessage: 'The title field contains the word wind.', + }), + query: `SELECT * FROM your_table WHERE title LIKE '%wind%'`, + }, + { + title: i18n.translate( + 'queryEnhancements.sqlLanguage.sampleQuery.titleContainsWindOrWindy', + { + defaultMessage: 'The title field contains the word wind or the word windy.', + } + ), + query: `SELECT * FROM your_table WHERE title LIKE '%wind%' OR title LIKE '%windy%';`, + }, + { + title: i18n.translate( + 'queryEnhancements.sqlLanguage.sampleQuery.titleContainsPhraseWindRises', + { + defaultMessage: 'The title field contains the phrase wind rises.', + } + ), + query: `SELECT * FROM your_table WHERE title LIKE '%wind rises%'`, + }, + { + title: i18n.translate( + 'queryEnhancements.sqlLanguage.sampleQuery.titleExactMatchWindRises', + { + defaultMessage: 'The title.keyword field exactly matches The wind rises.', + } + ), + query: `SELECT * FROM your_table WHERE title = 'The wind rises'`, + }, + { + title: i18n.translate( + 'queryEnhancements.sqlLanguage.sampleQuery.titleFieldsContainWind', + { + defaultMessage: + 'Any field that starts with title (for example, title and title.keyword) contains the word wind', + } + ), + query: `SELECT * FROM your_table WHERE title LIKE '%wind%' OR title = 'wind'`, + }, + { + title: i18n.translate( + 'queryEnhancements.sqlLanguage.sampleQuery.descriptionFieldExists', + { + defaultMessage: 'Documents in which the field description exists.', + } + ), + query: `SELECT * FROM your_table WHERE description IS NOT NULL AND description != '';`, + }, + ], }; queryString.getLanguageService().registerLanguage(sqlLanguageConfig);