From 74a69d6639d26f5cf4ff07e50bb4c0ce6aa4bd1f Mon Sep 17 00:00:00 2001 From: Miki Date: Sat, 19 Oct 2024 09:33:04 -0700 Subject: [PATCH] [Manual backport 2.x] [Discover]Sample Queries and Saved Queries in No Results Page #8616 (#8663) * Update Discover appearance (#8651) * Update Discover appearance Signed-off-by: Miki * Changeset file for PR #8651 created/updated --------- Signed-off-by: Miki Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> (cherry picked from commit 17103ba86370456fa5df6ccc722a22bee8fe2d35) Signed-off-by: Miki * Improve Empty State Handling: Add No Index Patterns Panel with Data Selection in Discover View (#8613) * Improve Empty State Handling: Add No Index Patterns Panel with Data Selection in Discover View This PR primarily addresses the scenario when no index patterns (general) is available in the Discover view. Instead of redirecting users to the index management page, it introduces a new "No Index Patterns" panel. This panel provides users with the option to open a data selector and add index patterns directly from the Discover view, improving the user experience for new or empty deployments. To achieve, we move the selectedDataset state from ConnectedDatasetSelector to the app container's state management. This allows the AdvancedSelector, opened from the AppContainer, to update the dataset state effectively. Key changes include: * Implementing NoIndexPatternsPanel and AdvancedSelector components. * Refactoring dataset state management in AppContainer and Sidebar. * Modifying DiscoverCanvas to conditionally render NoIndexPatternsPanel. * Updating ConnectedDatasetSelector to use shared state and dataset change handling. Signed-off-by: Anan Zhuang Signed-off-by: Miki * Update design of no data selected Signed-off-by: Miki * use i18n Signed-off-by: Anan Zhuang Signed-off-by: Miki * fix comments Signed-off-by: Anan Zhuang * Update design of no data selected Signed-off-by: Miki * fix lint error Signed-off-by: Anan Zhuang --------- Signed-off-by: Anan Zhuang Signed-off-by: Miki Co-authored-by: Miki (cherry picked from commit 66591391f631400ec6825c7b6e09659aa0760a5d) Signed-off-by: Miki * [Discover]Sample Queries and Saved Queries in No Results Page (#8616) * Sample Queries and Saved Queries in No Results Page Signed-off-by: Sean Li Signed-off-by: Miki * Changeset file for PR #8616 created/updated * Update styling Signed-off-by: Miki --------- Signed-off-by: Sean Li Signed-off-by: Miki Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Co-authored-by: Miki (cherry picked from commit 9da1b77bca36d209be7ec9a986500b691fc521e0) --------- Signed-off-by: Miki Signed-off-by: Anan Zhuang Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Co-authored-by: Anan Zhuang Co-authored-by: Sean Li --- changelogs/fragments/8616.yml | 2 + changelogs/fragments/8651.yml | 2 + .../header/collapsible_nav_group_enabled.scss | 2 +- .../ensure_default_index_pattern.ts | 12 +- src/plugins/data/public/index.ts | 11 +- .../dataset_service/dataset_service.ts | 5 + .../dataset_service/lib/index_pattern_type.ts | 24 +++ .../dataset_service/lib/index_type.ts | 24 +++ .../query_string/dataset_service/types.ts | 4 + .../language_service/lib/dql_language.ts | 47 +++++ .../language_service/lib/lucene_language.ts | 47 +++++ .../query_string/language_service/types.ts | 6 + src/plugins/data/public/ui/_common.scss | 16 ++ src/plugins/data/public/ui/_index.scss | 1 + .../dataset_selector/_dataset_selector.scss | 4 +- .../ui/dataset_selector/advanced_selector.tsx | 38 +++- .../ui/dataset_selector/configurator.tsx | 5 +- .../ui/dataset_selector/dataset_selector.tsx | 69 ++++++- .../public/ui/dataset_selector/index.test.tsx | 70 +++++++- .../data/public/ui/dataset_selector/index.tsx | 51 ++++-- .../ui/filter_bar/_global_filter_group.scss | 4 + .../public/ui/filter_bar/filter_options.tsx | 3 +- src/plugins/data/public/ui/index.ts | 2 + .../data/public/ui/no_index_patterns/index.ts | 6 + .../no_index_patterns_panel.tsx | 145 +++++++++++++++ .../public/ui/query_editor/_query_editor.scss | 10 +- .../default_editor/_default_editor.scss | 33 +++- .../editors/default_editor/index.tsx | 14 +- .../ui/query_string_input/_query_bar.scss | 4 + .../query_string_input/query_bar_top_row.tsx | 2 +- .../public/components/app_container.scss | 10 ++ .../public/components/sidebar/index.scss | 4 - .../public/components/sidebar/index.tsx | 85 ++++++--- src/plugins/data_explorer/public/index.ts | 1 + .../utils/state_management/metadata_slice.ts | 22 ++- .../redux_persistence.test.tsx | 1 + .../public/utils/state_management/store.ts | 7 +- .../components/chart/_histogram.scss | 1 - .../timechart_header/timechart_header.tsx | 2 +- .../default_discover_table/_table_cell.scss | 2 +- .../default_discover_table/_table_header.scss | 2 +- .../components/no_results/no_results.scss | 10 ++ .../components/no_results/no_results.tsx | 168 +++++++++++++++--- .../sidebar/discover_field_search.tsx | 1 + .../components/sidebar/discover_sidebar.scss | 32 +++- .../components/sidebar/discover_sidebar.tsx | 15 +- .../components/top_nav/get_top_nav_links.tsx | 17 +- .../canvas/discover_canvas.scss | 2 +- .../view_components/canvas/index.tsx | 133 +++++++++++--- .../view_components/canvas/top_nav.tsx | 48 +++-- .../edit_index_pattern/edit_index_pattern.tsx | 2 + .../public/top_nav_menu/_index.scss | 2 +- .../public/datasets/s3_type.ts | 24 +++ .../query_enhancements/public/plugin.tsx | 55 ++++++ 54 files changed, 1133 insertions(+), 176 deletions(-) create mode 100644 changelogs/fragments/8616.yml create mode 100644 changelogs/fragments/8651.yml create mode 100644 src/plugins/data/public/ui/_common.scss create mode 100644 src/plugins/data/public/ui/no_index_patterns/index.ts create mode 100644 src/plugins/data/public/ui/no_index_patterns/no_index_patterns_panel.tsx create mode 100644 src/plugins/discover/public/application/components/no_results/no_results.scss 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/changelogs/fragments/8651.yml b/changelogs/fragments/8651.yml new file mode 100644 index 000000000000..0baf8f35105c --- /dev/null +++ b/changelogs/fragments/8651.yml @@ -0,0 +1,2 @@ +feat: +- Update the appearance of Discover ([#8651](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8651)) \ No newline at end of file diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss index de3a6021b47d..1310585eefea 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.scss @@ -1,9 +1,9 @@ .context-nav-wrapper { border: none !important; border-top-right-radius: $euiSizeL; - border-bottom-right-radius: $euiSizeL; background-color: $euiSideNavBackgroundColor; overflow: hidden; + box-shadow: 1px 0 0 $euiBorderColor !important; .nav-link-item { padding: $euiSizeS; diff --git a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts index e64b0bf33f63..9fed0e1d0519 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts @@ -32,7 +32,9 @@ import { includes } from 'lodash'; import { IndexPatternsContract } from './index_patterns'; import { UiSettingsCommon } from '../types'; -export type EnsureDefaultIndexPattern = () => Promise | undefined; +export type EnsureDefaultIndexPattern = ( + shouldRedirect?: boolean +) => Promise | undefined; export const createEnsureDefaultIndexPattern = ( uiSettings: UiSettingsCommon, @@ -42,7 +44,10 @@ export const createEnsureDefaultIndexPattern = ( * Checks whether a default index pattern is set and exists and defines * one otherwise. */ - return async function ensureDefaultIndexPattern(this: IndexPatternsContract) { + return async function ensureDefaultIndexPattern( + this: IndexPatternsContract, + shouldRedirect: boolean = true + ) { const patterns = await this.getIds(); let defaultId = await uiSettings.get('defaultIndex'); let defined = !!defaultId; @@ -62,7 +67,8 @@ export const createEnsureDefaultIndexPattern = ( defaultId = patterns[0]; await uiSettings.set('defaultIndex', defaultId); } else { - return onRedirectNoIndexPattern(); + if (shouldRedirect) return onRedirectNoIndexPattern(); + else return; } }; }; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index fb0e36fa1f1d..773f24118907 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -62,7 +62,16 @@ import { } from '../common'; import { FilterLabel } from './ui'; -export { createEditor, DefaultInput, DQLBody, SingleLineInput } from './ui'; +export { + createEditor, + DefaultInput, + DQLBody, + SingleLineInput, + DatasetSelector, + AdvancedSelector, + NoIndexPatternsPanel, + DatasetSelectorAppearance, +} from './ui'; import { generateFilters, diff --git a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts index 41d1547e8eeb..d8414a33779e 100644 --- a/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts +++ b/src/plugins/data/public/query/query_string/dataset_service/dataset_service.ts @@ -207,6 +207,11 @@ export class DatasetService { return Number(this.sessionStorage.get('lastCacheTime')) || undefined; } + public removeFromRecentDatasets(datasetId: string): void { + this.recentDatasets.del(datasetId); + this.serializeRecentDatasets(); + } + private setLastCacheTime(time: number): void { this.sessionStorage.set('lastCacheTime', time); } 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 new file mode 100644 index 000000000000..fcd98b9c7b31 --- /dev/null +++ b/src/plugins/data/public/ui/_common.scss @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.dataUI-centerPanel { + height: 100%; + width: 100%; + + // Push the centralized child up, just like ouiOverlayMask + padding-bottom: 10vh; + + & > * { + @include euiLegibilityMaxWidth(100%); + } +} diff --git a/src/plugins/data/public/ui/_index.scss b/src/plugins/data/public/ui/_index.scss index 9ab4ca672b38..cdbe539a2e19 100644 --- a/src/plugins/data/public/ui/_index.scss +++ b/src/plugins/data/public/ui/_index.scss @@ -1,3 +1,4 @@ +@import "./common"; @import "./filter_bar/index"; @import "./typeahead/index"; @import "./saved_query_management/index"; diff --git a/src/plugins/data/public/ui/dataset_selector/_dataset_selector.scss b/src/plugins/data/public/ui/dataset_selector/_dataset_selector.scss index 0fd82fc1b9ca..2f92480a4413 100644 --- a/src/plugins/data/public/ui/dataset_selector/_dataset_selector.scss +++ b/src/plugins/data/public/ui/dataset_selector/_dataset_selector.scss @@ -19,7 +19,9 @@ &__advancedModal { width: 1200px; height: 800px; - max-height: calc(100vh - $euiSizeS); + + // euiOverlayMask pushes the modal up due to having padding-bottom: 10vh + max-height: calc(90vh - $euiSizeL); .euiModal__flex { max-height: none; diff --git a/src/plugins/data/public/ui/dataset_selector/advanced_selector.tsx b/src/plugins/data/public/ui/dataset_selector/advanced_selector.tsx index 734153452eea..25434062de8e 100644 --- a/src/plugins/data/public/ui/dataset_selector/advanced_selector.tsx +++ b/src/plugins/data/public/ui/dataset_selector/advanced_selector.tsx @@ -13,19 +13,27 @@ import { } from '../../../common'; import { DatasetExplorer } from './dataset_explorer'; import { Configurator } from './configurator'; -import { getQueryService } from '../../services'; import { IDataPluginServices } from '../../types'; export const AdvancedSelector = ({ services, onSelect, onCancel, + selectedDataset, + setSelectedDataset, + setIndexPattern, + direct = false, }: { services: IDataPluginServices; onSelect: (dataset: Dataset) => void; onCancel: () => void; + selectedDataset?: Dataset; + setSelectedDataset: (data: Dataset | undefined) => void; + setIndexPattern: (id: string | undefined) => void; + direct?: boolean; }) => { - const queryString = getQueryService().queryString; + const queryService = services.data.query; + const queryString = queryService.queryString; const [path, setPath] = useState([ { @@ -48,14 +56,21 @@ export const AdvancedSelector = ({ }), }, ]); - const [selectedDataset, setSelectedDataset] = useState(); - return selectedDataset ? ( + const [currentSelectedDataset, setCurrentSelectedDataset] = useState( + selectedDataset + ); + + return currentSelectedDataset ? ( setSelectedDataset(undefined)} + onPrevious={() => { + setSelectedDataset(undefined); + setCurrentSelectedDataset(undefined); + }} + queryService={queryService} /> ) : ( setSelectedDataset(dataset)} + onNext={(dataset) => { + setSelectedDataset(dataset); + setIndexPattern(dataset.id); + setCurrentSelectedDataset(dataset); + if (direct) { + const query = queryString.getInitialQueryByDataset(dataset); + queryString.setQuery(query); + queryString.getDatasetService().addRecentDataset(dataset); + } + }} onCancel={onCancel} /> ); diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.tsx index db1cb80dd6e3..b8a74a9353e0 100644 --- a/src/plugins/data/public/ui/dataset_selector/configurator.tsx +++ b/src/plugins/data/public/ui/dataset_selector/configurator.tsx @@ -20,20 +20,21 @@ import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; import React, { useEffect, useMemo, useState } from 'react'; import { BaseDataset, DEFAULT_DATA, Dataset, DatasetField } from '../../../common'; -import { getIndexPatterns, getQueryService } from '../../services'; +import { getIndexPatterns } from '../../services'; export const Configurator = ({ baseDataset, onConfirm, onCancel, onPrevious, + queryService, }: { baseDataset: BaseDataset; onConfirm: (dataset: Dataset) => void; onCancel: () => void; onPrevious: () => void; + queryService: any; }) => { - const queryService = getQueryService(); const queryString = queryService.queryString; const languageService = queryService.queryString.getLanguageService(); const indexPatternsService = getIndexPatterns(); diff --git a/src/plugins/data/public/ui/dataset_selector/dataset_selector.tsx b/src/plugins/data/public/ui/dataset_selector/dataset_selector.tsx index 76c9fbe74546..6755645020d1 100644 --- a/src/plugins/data/public/ui/dataset_selector/dataset_selector.tsx +++ b/src/plugins/data/public/ui/dataset_selector/dataset_selector.tsx @@ -11,6 +11,7 @@ import { EuiSelectable, EuiSelectableOption, EuiSmallButtonEmpty, + EuiSmallButton, EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; @@ -22,12 +23,43 @@ import { getQueryService } from '../../services'; import { IDataPluginServices } from '../../types'; import { AdvancedSelector } from './advanced_selector'; +export enum DatasetSelectorAppearance { + Button = 'button', + None = 'none', +} + +type EuiSmallButtonProps = React.ComponentProps; +type EuiSmallButtonEmptyProps = React.ComponentProps; + interface DatasetSelectorProps { selectedDataset?: Dataset; - setSelectedDataset: (dataset: Dataset) => void; + setSelectedDataset: (data: Dataset | undefined) => void; + setIndexPattern: (id: string | undefined) => void; + handleDatasetChange: (dataset: Dataset) => void; services: IDataPluginServices; } +export interface DatasetSelectorUsingButtonProps { + appearance: DatasetSelectorAppearance.Button; + buttonProps?: EuiSmallButtonProps; +} + +export interface DatasetSelectorUsingButtonEmptyProps { + appearance?: DatasetSelectorAppearance.None; + buttonProps?: EuiSmallButtonEmptyProps; +} + +const RootComponent: React.FC< + (EuiSmallButtonEmptyProps | EuiSmallButtonProps) & { appearance?: DatasetSelectorAppearance } +> = (props) => { + const { appearance, ...rest } = props; + if (appearance === DatasetSelectorAppearance.Button) { + return ; + } else { + return ; + } +}; + /** * This component provides a dropdown selector for datasets and an advanced selector modal. * It fetches datasets once on mount to populate the selector options. @@ -41,8 +73,13 @@ interface DatasetSelectorProps { export const DatasetSelector = ({ selectedDataset, setSelectedDataset, + setIndexPattern, + handleDatasetChange, services, -}: DatasetSelectorProps) => { + appearance, + buttonProps, +}: DatasetSelectorProps & + (DatasetSelectorUsingButtonProps | DatasetSelectorUsingButtonEmptyProps)) => { const isMounted = useRef(false); const [isOpen, setIsOpen] = useState(false); const [indexPatterns, setIndexPatterns] = useState([]); @@ -69,7 +106,7 @@ export const DatasetSelector = ({ // If no dataset is selected, select the first one if (!selectedDataset && fetchedDatasets.length > 0) { - setSelectedDataset(fetchedDatasets[0]); + handleDatasetChange(fetchedDatasets[0]); } }; @@ -146,11 +183,11 @@ export const DatasetSelector = ({ indexPatterns.find((dataset) => dataset.id === selectedOption.key); if (foundDataset) { closePopover(); - setSelectedDataset(foundDataset); + handleDatasetChange(foundDataset); } } }, - [recentDatasets, indexPatterns, setSelectedDataset, closePopover] + [recentDatasets, indexPatterns, handleDatasetChange, closePopover] ); const datasetTitle = useMemo(() => { @@ -168,8 +205,18 @@ export const DatasetSelector = ({ return ( - + {datasetTitle} - + } isOpen={isOpen} @@ -223,10 +270,14 @@ export const DatasetSelector = ({ onSelect={(dataset?: Dataset) => { overlay?.close(); if (dataset) { - setSelectedDataset(dataset); + handleDatasetChange(dataset); } }} onCancel={() => overlay?.close()} + selectedDataset={undefined} + setSelectedDataset={setSelectedDataset} + setIndexPattern={setIndexPattern} + direct={true} /> ), { diff --git a/src/plugins/data/public/ui/dataset_selector/index.test.tsx b/src/plugins/data/public/ui/dataset_selector/index.test.tsx index 0dd456711c37..9e486beb5310 100644 --- a/src/plugins/data/public/ui/dataset_selector/index.test.tsx +++ b/src/plugins/data/public/ui/dataset_selector/index.test.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import { DatasetSelector as ConnectedDatasetSelector } from './index'; import { DatasetSelector } from './dataset_selector'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; @@ -19,6 +20,8 @@ jest.mock('./dataset_selector', () => ({ })); describe('ConnectedDatasetSelector', () => { + const mockSubscribe = jest.fn(); + const mockUnsubscribe = jest.fn(); const mockQueryString = { getQuery: jest.fn().mockReturnValue({}), getDefaultQuery: jest.fn().mockReturnValue({}), @@ -27,12 +30,14 @@ describe('ConnectedDatasetSelector', () => { getDatasetService: jest.fn().mockReturnValue({ addRecentDataset: jest.fn(), }), + getUpdates$: jest.fn().mockReturnValue({ + subscribe: mockSubscribe.mockReturnValue({ unsubscribe: mockUnsubscribe }), + }), }; const mockOnSubmit = jest.fn(); const mockServices = { data: { query: { - // @ts-ignore queryString: mockQueryString, }, }, @@ -40,36 +45,87 @@ describe('ConnectedDatasetSelector', () => { beforeEach(() => { (useOpenSearchDashboards as jest.Mock).mockReturnValue({ services: mockServices }); + jest.clearAllMocks(); }); it('should render DatasetSelector with correct props', () => { - const wrapper = mount(); + const wrapper = mount( + + ); expect(wrapper.find(DatasetSelector).props()).toEqual({ selectedDataset: undefined, setSelectedDataset: expect.any(Function), + setIndexPattern: expect.any(Function), + handleDatasetChange: expect.any(Function), services: mockServices, }); }); it('should initialize selectedDataset correctly', () => { const mockDataset: Dataset = { id: 'initial', title: 'Initial Dataset', type: 'test' }; - mockQueryString.getQuery.mockReturnValueOnce({ dataset: mockDataset }); - const wrapper = mount(); + const wrapper = mount( + + ); expect(wrapper.find(DatasetSelector).prop('selectedDataset')).toEqual(mockDataset); }); it('should call handleDatasetChange only once when dataset changes', () => { - const wrapper = mount(); - const setSelectedDataset = wrapper.find(DatasetSelector).prop('setSelectedDataset') as ( + const setSelectedDataset = jest.fn(); + const setIndexPattern = jest.fn(); + const wrapper = mount( + + ); + const handleDatasetChange = wrapper.find(DatasetSelector).prop('handleDatasetChange') as ( dataset?: Dataset ) => void; const newDataset: Dataset = { id: 'test', title: 'Test Dataset', type: 'test' }; - setSelectedDataset(newDataset); + act(() => { + handleDatasetChange(newDataset); + }); expect(mockQueryString.getInitialQueryByDataset).toHaveBeenCalledTimes(1); expect(mockQueryString.setQuery).toHaveBeenCalledTimes(1); expect(mockOnSubmit).toHaveBeenCalledTimes(1); + expect(setSelectedDataset).toHaveBeenCalledWith(newDataset); + expect(setIndexPattern).toHaveBeenCalledWith(newDataset.id); + }); + + it('should subscribe to queryString.getUpdates$ and unsubscribe on unmount', () => { + const wrapper = mount( + + ); + + expect(mockQueryString.getUpdates$).toHaveBeenCalledTimes(1); + expect(mockSubscribe).toHaveBeenCalledTimes(1); + + wrapper.unmount(); + + expect(mockUnsubscribe).toHaveBeenCalledTimes(1); }); }); diff --git a/src/plugins/data/public/ui/dataset_selector/index.tsx b/src/plugins/data/public/ui/dataset_selector/index.tsx index d89f92bc3ad7..48cd2926de22 100644 --- a/src/plugins/data/public/ui/dataset_selector/index.tsx +++ b/src/plugins/data/public/ui/dataset_selector/index.tsx @@ -3,27 +3,51 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect } from 'react'; import React from 'react'; import { Dataset, Query, TimeRange } from '../../../common'; -import { DatasetSelector } from './dataset_selector'; -import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; -import { IDataPluginServices } from '../../types'; +import { + DatasetSelector, + DatasetSelectorUsingButtonEmptyProps, + DatasetSelectorUsingButtonProps, + DatasetSelectorAppearance, +} from './dataset_selector'; +import { AdvancedSelector } from './advanced_selector'; interface ConnectedDatasetSelectorProps { onSubmit: ((query: Query, dateRange?: TimeRange | undefined) => void) | undefined; + selectedDataset?: Dataset; + setSelectedDataset: (data: Dataset | undefined) => void; + setIndexPattern: (id: string | undefined) => void; + services?: any; } -const ConnectedDatasetSelector = ({ onSubmit }: ConnectedDatasetSelectorProps) => { - const { services } = useOpenSearchDashboards(); +const ConnectedDatasetSelector = ({ + onSubmit, + selectedDataset, + setSelectedDataset, + setIndexPattern, + services, + ...datasetSelectorProps +}: ConnectedDatasetSelectorProps & + (DatasetSelectorUsingButtonProps | DatasetSelectorUsingButtonEmptyProps)) => { const queryString = services.data.query.queryString; - const [selectedDataset, setSelectedDataset] = useState( - () => queryString.getQuery().dataset || queryString.getDefaultQuery().dataset - ); + + useEffect(() => { + const subscription = queryString.getUpdates$().subscribe((query) => { + setSelectedDataset(query.dataset); + setIndexPattern(query.dataset?.id); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [queryString, setSelectedDataset, setIndexPattern]); const handleDatasetChange = useCallback( (dataset?: Dataset) => { setSelectedDataset(dataset); + setIndexPattern(dataset?.id); if (dataset) { const query = queryString.getInitialQueryByDataset(dataset); queryString.setQuery(query); @@ -31,16 +55,19 @@ const ConnectedDatasetSelector = ({ onSubmit }: ConnectedDatasetSelectorProps) = queryString.getDatasetService().addRecentDataset(dataset); } }, - [onSubmit, queryString] + [onSubmit, queryString, setSelectedDataset, setIndexPattern] ); return ( ); }; -export { ConnectedDatasetSelector as DatasetSelector }; +export { ConnectedDatasetSelector as DatasetSelector, AdvancedSelector, DatasetSelectorAppearance }; diff --git a/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss index 2f347b6e2996..9cab10db15f7 100644 --- a/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss +++ b/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss @@ -81,4 +81,8 @@ &--compressed { margin-top: -$euiSizeS; } + + &__allFiltersPopover { + background-color: $euiFormInputGroupLabelBackground; + } } diff --git a/src/plugins/data/public/ui/filter_bar/filter_options.tsx b/src/plugins/data/public/ui/filter_bar/filter_options.tsx index 131d317ecbc5..26fcc9714212 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_options.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_options.tsx @@ -407,7 +407,6 @@ const FilterOptionsUI = (props: Props) => { return ( { return ( void; +} + +export const NoIndexPatternsPanel: React.FC = ({ + onOpenDataSelector, +}) => ( + + + + + + + + + +

+ {i18n.translate('data.noIndexPatterns.selectDataTitle', { + defaultMessage: 'Select data', + })} +

+
+
+ + + {i18n.translate('data.noIndexPatterns.selectDataDescription', { + defaultMessage: + 'Select an available data source and choose a query language to use for running queries. You can use the data dropdown or use the enhanced data selector to select data.', + })} + + + + + {i18n.translate('data.noIndexPatterns.openDataSelectorButton', { + defaultMessage: 'Open data selector', + })} + + + + + +

+ {i18n.translate('data.noIndexPatterns.learnMoreAboutQueryLanguages', { + defaultMessage: 'Learn more about query languages', + })} +

+
+
+ + + + + + {i18n.translate('data.noIndexPatterns.pplDocumentation', { + defaultMessage: 'PPL documentation', + })} + + + + + + + {i18n.translate('data.noIndexPatterns.sqlDocumentation', { + defaultMessage: 'SQL documentation', + })} + + + + + + + {i18n.translate('data.noIndexPatterns.luceneDocumentation', { + defaultMessage: 'Lucene documentation', + })} + + + + + + + {i18n.translate('data.noIndexPatterns.dqlDocumentation', { + defaultMessage: 'DQL documentation', + })} + + + + + +
+
+
+
+); diff --git a/src/plugins/data/public/ui/query_editor/_query_editor.scss b/src/plugins/data/public/ui/query_editor/_query_editor.scss index a970a970853b..719cc65785c2 100644 --- a/src/plugins/data/public/ui/query_editor/_query_editor.scss +++ b/src/plugins/data/public/ui/query_editor/_query_editor.scss @@ -137,7 +137,7 @@ .osdQueryEditor__header { display: flex; align-items: center; - padding: 0 $euiSizeXS $euiSizeXS; + padding: 0 $euiSizeXS $euiSizeS; } .osdQueryEditor__topBar { @@ -187,13 +187,19 @@ .osdQuerEditor__singleLine { padding: calc($euiSizeXS + 1px); - background-color: $euiColorEmptyShade; overflow: initial !important; // needed for suggestion window, otherwise will be hidden in child min-width: 0; .monaco-editor .view-overlays .current-line { border: none; } + + &, + & .monaco-editor, + & .monaco-editor .inputarea.ime-input, + & .monaco-editor-background { + background-color: $euiFormBackgroundColor; + } } .suggest-widget { diff --git a/src/plugins/data/public/ui/query_editor/editors/default_editor/_default_editor.scss b/src/plugins/data/public/ui/query_editor/editors/default_editor/_default_editor.scss index ccebd35c4ead..2516c32ec27c 100644 --- a/src/plugins/data/public/ui/query_editor/editors/default_editor/_default_editor.scss +++ b/src/plugins/data/public/ui/query_editor/editors/default_editor/_default_editor.scss @@ -1,9 +1,38 @@ .defaultEditor { border: $euiBorderThin; border-radius: $euiSizeXS; - margin: 0 $euiSizeXS $euiSizeXS; &__footer { - margin-left: $euiSizeXS; + padding-left: $euiSizeXS; + background-color: $euiColorLightestShade; } + + .monaco-editor { + border-radius: $euiSizeXS $euiSizeXS 0 0; + + .margin { + border-radius: $euiSizeXS 0 0 0; + background-color: $euiFormBackgroundColor; + padding: 0 $euiSizeXS; + } + + .view-lines { + padding: 0 $euiSizeXS; + } + + .monaco-scrollable-element { + border-radius: 0 $euiSizeXS 0 0; + } + } + + .monaco-editor, + .monaco-editor-background, + .monaco-editor .inputarea.ime-input, + .monaco-editor .decorationsOverviewRuler { + background-color: $euiColorEmptyShade; + } +} + +.defaultEditor__footerRow { + gap: $euiSizeM; } 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 764b25d4b9ac..1134d104befd 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 @@ -43,14 +43,15 @@ export const DefaultInput: React.FC = ({ options={{ minimap: { enabled: false }, scrollBeyondLastLine: false, - fontSize: 14, - fontFamily: 'Roboto Mono', + fontSize: 12, + lineHeight: 20, + fontFamily: 'var(--font-code)', lineNumbers: 'on', folding: true, wordWrap: 'on', wrappingIndent: 'same', lineDecorationsWidth: 0, - lineNumbersMinChars: 2, + lineNumbersMinChars: 1, wordBasedSuggestions: false, }} suggestionProvider={{ @@ -70,7 +71,12 @@ export const DefaultInput: React.FC = ({ />
{footerItems && ( - + {footerItems.start?.map((item) => ( {item} diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss index 3976a12d594d..e240d89281bf 100644 --- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss +++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss @@ -91,3 +91,7 @@ } } } + +.osdQueryBar--hideEmpty:empty { + display: none; +} diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index 14b9ef6d8fd9..45e718fd52ba 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -402,7 +402,7 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { > {renderQueryInput()} {renderSharingMetaFields()} - + {shouldUseDatePickerRef ? createPortal(renderUpdateButton(), props.datePickerRef!.current!) : renderUpdateButton()} diff --git a/src/plugins/data_explorer/public/components/app_container.scss b/src/plugins/data_explorer/public/components/app_container.scss index f0320d5d9b8e..0d9b66b8e471 100644 --- a/src/plugins/data_explorer/public/components/app_container.scss +++ b/src/plugins/data_explorer/public/components/app_container.scss @@ -11,6 +11,7 @@ $osdHeaderOffset: $euiHeaderHeightCompensation; .deLayout { height: calc(100vh - #{$osdHeaderOffset * 1}); + padding: $euiSizeS; &.dsc--next { height: calc(100vh - #{$osdHeaderOffset * 2}); @@ -19,6 +20,15 @@ $osdHeaderOffset: $euiHeaderHeightCompensation; &__canvas { height: 100%; } + + // stylelint-disable-next-line @osd/stylelint/no_modifying_global_selectors + & > .euiResizableContainer { + gap: $euiSizeXS; + } + + .globalQueryBar { + padding: 0; + } } .headerIsExpanded .deLayout { diff --git a/src/plugins/data_explorer/public/components/sidebar/index.scss b/src/plugins/data_explorer/public/components/sidebar/index.scss index 1828568bc361..bdabfcdb5943 100644 --- a/src/plugins/data_explorer/public/components/sidebar/index.scss +++ b/src/plugins/data_explorer/public/components/sidebar/index.scss @@ -2,10 +2,6 @@ &_panel { border-top: 0; } - - &_dataSource { - border-bottom: $euiBorderThin !important; - } } .dataPanelTypeFilterPopover { diff --git a/src/plugins/data_explorer/public/components/sidebar/index.tsx b/src/plugins/data_explorer/public/components/sidebar/index.tsx index 430d70fa9180..3c5975d9452e 100644 --- a/src/plugins/data_explorer/public/components/sidebar/index.tsx +++ b/src/plugins/data_explorer/public/components/sidebar/index.tsx @@ -12,27 +12,50 @@ import { DataSourceSelectable, UI_SETTINGS, } from '../../../../data/public'; -import { DataSourceOption } from '../../../../data/public/'; +import { + DataSourceOption, + DatasetSelector, + DatasetSelectorAppearance, +} from '../../../../data/public/'; +import { Dataset } from '../../../../data/common'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { DataExplorerServices } from '../../types'; -import { setIndexPattern, useTypedDispatch, useTypedSelector } from '../../utils/state_management'; +import { + setIndexPattern, + useTypedDispatch, + useTypedSelector, + setSelectedDataset, +} from '../../utils/state_management'; import './index.scss'; +type HandleSetIndexPattern = (id: string | undefined) => void; +type HandleSelectedDataset = (data: Dataset | undefined) => void; + export const Sidebar: FC = ({ children }) => { - const { indexPattern: indexPatternId } = useTypedSelector((state) => state.metadata); + const { indexPattern: indexPatternId, selectedDataset } = useTypedSelector( + (state) => state.metadata + ); const dispatch = useTypedDispatch(); const [selectedSources, setSelectedSources] = useState([]); const [dataSourceOptionList, setDataSourceOptionList] = useState([]); const [activeDataSources, setActiveDataSources] = useState([]); - + const { services } = useOpenSearchDashboards(); const { - services: { - data: { indexPatterns, dataSources }, - notifications: { toasts }, - application, - uiSettings, + data: { indexPatterns, dataSources }, + notifications: { toasts }, + application, + uiSettings, + } = services; + + const handleDatasetSubmit = useCallback( + (query: any) => { + // Update the index pattern + if (query.dataset) { + dispatch(setIndexPattern(query.dataset.id)); + } }, - } = useOpenSearchDashboards(); + [dispatch] + ); const [isEnhancementEnabled, setIsEnhancementEnabled] = useState(false); @@ -114,21 +137,41 @@ export const Sidebar: FC = ({ children }) => { dataSources.dataSourceService.reload(); }, [dataSources.dataSourceService]); + const handleSetIndexPattern: HandleSetIndexPattern = (id: string | undefined) => { + dispatch(setIndexPattern(id)); + }; + + const handleSelectedDataset: HandleSelectedDataset = (data: Dataset | undefined) => { + dispatch(setSelectedDataset(data)); + }; + return ( - {!isEnhancementEnabled && ( - + + {isEnhancementEnabled ? ( + + ) : ( { onRefresh={memorizedReload} fullWidth /> - - )} + )} + {children} diff --git a/src/plugins/data_explorer/public/index.ts b/src/plugins/data_explorer/public/index.ts index 6b0561261c16..9d9ae2f46d2b 100644 --- a/src/plugins/data_explorer/public/index.ts +++ b/src/plugins/data_explorer/public/index.ts @@ -18,5 +18,6 @@ export { useTypedSelector, useTypedDispatch, setIndexPattern, + setSelectedDataset, setDataSet, } from './utils/state_management'; diff --git a/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts b/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts index 1fca4a659244..c62e550b0073 100644 --- a/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts +++ b/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts @@ -6,11 +6,13 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { DataExplorerServices } from '../../types'; import { QUERY_ENHANCEMENT_ENABLED_SETTING } from '../../components/constants'; +import { Dataset } from '../../../../data/common'; export interface MetadataState { indexPattern?: string; originatingApp?: string; view?: string; + selectedDataset?: Dataset; } const initialState: MetadataState = {}; @@ -26,13 +28,16 @@ export const getPreloadedState = async ({ .getStateTransfer(scopedHistory) .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; const isQueryEnhancementEnabled = uiSettings.get(QUERY_ENHANCEMENT_ENABLED_SETTING); - const defaultIndexPattern = isQueryEnhancementEnabled - ? undefined - : await data.indexPatterns.getDefault(); + const defaultIndexPattern = await data.indexPatterns.getDefault(); + const selectedDataset = + data.query.queryString.getQuery().dataset || + data.query.queryString.getDefaultQuery().dataset || + undefined; const preloadedState: MetadataState = { ...initialState, originatingApp, indexPattern: defaultIndexPattern?.id, + selectedDataset, }; return preloadedState; @@ -51,6 +56,9 @@ export const slice = createSlice({ setView: (state, action: PayloadAction) => { state.view = action.payload; }, + setSelectedDataset: (state, action: PayloadAction) => { + state.selectedDataset = action.payload; + }, setState: (_state, action: PayloadAction) => { return action.payload; }, @@ -58,4 +66,10 @@ export const slice = createSlice({ }); export const { reducer } = slice; -export const { setIndexPattern, setOriginatingApp, setView, setState } = slice.actions; +export const { + setIndexPattern, + setOriginatingApp, + setView, + setState, + setSelectedDataset, +} = slice.actions; diff --git a/src/plugins/data_explorer/public/utils/state_management/redux_persistence.test.tsx b/src/plugins/data_explorer/public/utils/state_management/redux_persistence.test.tsx index 62159558a0c4..9af3bcf3d491 100644 --- a/src/plugins/data_explorer/public/utils/state_management/redux_persistence.test.tsx +++ b/src/plugins/data_explorer/public/utils/state_management/redux_persistence.test.tsx @@ -26,6 +26,7 @@ describe('test redux state persistence', () => { "metadata": Object { "indexPattern": "id", "originatingApp": undefined, + "selectedDataset": undefined, }, } `); diff --git a/src/plugins/data_explorer/public/utils/state_management/store.ts b/src/plugins/data_explorer/public/utils/state_management/store.ts index daf0b3d7e369..1ac3564e34ce 100644 --- a/src/plugins/data_explorer/public/utils/state_management/store.ts +++ b/src/plugins/data_explorer/public/utils/state_management/store.ts @@ -116,4 +116,9 @@ export type RenderState = Omit; // Remaining state after export type Store = ReturnType; export type AppDispatch = Store['dispatch']; -export { MetadataState, setIndexPattern, setOriginatingApp } from './metadata_slice'; +export { + MetadataState, + setIndexPattern, + setOriginatingApp, + setSelectedDataset, +} from './metadata_slice'; diff --git a/src/plugins/discover/public/application/components/chart/_histogram.scss b/src/plugins/discover/public/application/components/chart/_histogram.scss index 3b1c1799e0f6..f30d24d0bb45 100644 --- a/src/plugins/discover/public/application/components/chart/_histogram.scss +++ b/src/plugins/discover/public/application/components/chart/_histogram.scss @@ -11,7 +11,6 @@ } .dscChart__wrapper { - border-top: 1px solid $euiColorLightShade; border-bottom: 1px solid $euiColorLightShade; padding: 8px; gap: 8px; diff --git a/src/plugins/discover/public/application/components/chart/timechart_header/timechart_header.tsx b/src/plugins/discover/public/application/components/chart/timechart_header/timechart_header.tsx index a79876ccb455..a20f551d09fa 100644 --- a/src/plugins/discover/public/application/components/chart/timechart_header/timechart_header.tsx +++ b/src/plugins/discover/public/application/components/chart/timechart_header/timechart_header.tsx @@ -118,7 +118,7 @@ export function TimechartHeader({ return ( - + { +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/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx index 3469fd1d34dc..c93352d43589 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx @@ -275,6 +275,7 @@ export function DiscoverFieldSearch({ 0} aria-label={filterBtnAriaLabel} data-test-subj="toggleFieldFilterButton" diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss index aaba1c5a42aa..07f9e7e4ed06 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss @@ -4,19 +4,40 @@ */ .deSidebar_panel { - border-left: 0; - .dscSideBar_searchContainer { - padding: $euiSizeXS; border-bottom: $euiBorderThin; .toggleFieldFilterButton { border: none; + background-color: $euiFormInputGroupLabelBackground; } .dscSideBar_searchInput { box-shadow: none; - border-right: $euiBorderThin; + border: $euiBorderThin; + background-color: $euiFormBackgroundColor; + } + } + + .dscSideBar_fieldListContainer { + padding-top: $euiSizeS; + } + + .deSidebar_dataSource { + padding-bottom: 0; + } + + .datasetSelector__button { + border-radius: $euiFormControlCompressedBorderRadius; + border-color: $euiFormBorderColor; + background-color: $euiFormBackgroundColor; + + & > .euiButtonContent { + justify-content: space-between; + + .euiIcon { + fill: $euiTextSubduedColor; + } } } } @@ -27,7 +48,8 @@ .euiButtonEmpty__content { font-size: $euiFontSizeXS; font-weight: $euiFontWeightSemiBold; - justify-content: flex-end; + justify-content: flex-start; + padding-left: $euiSizeM + 2px; } } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 0b0162159a38..12ba460bb5a1 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -215,11 +215,7 @@ export function DiscoverSidebar(props: DiscoverSidebarProps) { color="transparent" hasBorder={false} > - + ) : null} - + {fields.length > 0 && ( <> setExpanded(!expanded)} @@ -319,7 +318,7 @@ const FieldList = ({ {expanded && ( diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx index 647b989f42e4..d87e1e47f702 100644 --- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.tsx @@ -277,7 +277,8 @@ export const getTopNavLinks = ( services: DiscoverViewServices, inspectorAdapters: Adapters, savedSearch: SavedSearch, - isEnhancementEnabled: boolean = false + isEnhancementEnabled: boolean = false, + useNoIndexPatternsTopNav: boolean = false ) => { const { history, @@ -503,8 +504,18 @@ export const getTopNavLinks = ( // Order their appearance return ['save', 'open', 'new', 'inspect', 'share'].reduce((acc, item) => { const itemDef = topNavLinksMap.get(item); - if (itemDef) acc.push(itemDef); - + if (itemDef) { + if (useNoIndexPatternsTopNav && item !== 'open') { + // Disable all buttons except 'open' when in no index patterns mode + acc.push({ + ...itemDef, + disabled: true, + run: () => {}, // Empty function for disabled buttons + }); + } else { + acc.push(itemDef); + } + } return acc; }, [] as TopNavMenuData[]); }; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss b/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss index e0ab20a15296..beac224ec6ba 100644 --- a/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss +++ b/src/plugins/discover/public/application/view_components/canvas/discover_canvas.scss @@ -1,5 +1,5 @@ .dscCanvas { - @include euiYScrollWithShadows; + @include euiYScroll; /* stylelint-disable-next-line */ container-type: inline-size; // containment context 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 2e956d6908e9..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,18 +27,31 @@ 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'; +import { toMountPoint } from '../../../../../opensearch_dashboards_react/public'; // eslint-disable-next-line import/no-default-export export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalRef }: ViewProps) { + const { indexPattern: currentIndexPattern, selectedDataset } = useSelector( + (state) => state.metadata + ); + const [loadedIndexPattern, setLoadedIndexPattern] = useState(selectedDataset?.id); const panelRef = useRef(null); const { data$, refetch$, indexPattern } = useDiscoverContext(); + const { services } = useOpenSearchDashboards(); const { - services: { - uiSettings, - capabilities, - chrome: { setHeaderVariant }, - }, - } = useOpenSearchDashboards(); + uiSettings, + capabilities, + chrome: { setHeaderVariant }, + 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; @@ -56,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, @@ -65,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(); } @@ -122,14 +139,52 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR }; const showSaveQuery = !!capabilities.discover?.saveQuery; + const handleDatasetChange = (dataset: Dataset) => { + dispatch(setSelectedDataset(dataset)); + + // Update query and other necessary state + const queryString = data.query.queryString; + const initialQuery = queryString.getInitialQueryByDataset(dataset); + queryString.setQuery(initialQuery); + queryString.getDatasetService().addRecentDataset(dataset); + }; + + const handleOpenDataSelector = () => { + const overlay = overlays?.openModal( + toMountPoint( + { + overlay?.close(); + if (dataset) { + handleDatasetChange(dataset); + } + }} + onCancel={() => overlay?.close()} + selectedDataset={undefined} + setSelectedDataset={setSelectedDataset} + setIndexPattern={setIndexPattern} + dispatch={dispatch} + /> + ), + { + maxWidth: false, + className: 'datasetSelector__advancedModal', + } + ); + }; + + const hasNoDataset = !currentIndexPattern && !loadedIndexPattern && isEnhancementsEnabled; + return ( - - {fetchState.status === ResultStatus.NO_RESULTS && ( - - )} - {fetchState.status === ResultStatus.ERROR && ( - - )} - {fetchState.status === ResultStatus.UNINITIALIZED && ( - refetch$.next()} /> - )} - {fetchState.status === ResultStatus.LOADING && } - {fetchState.status === ResultStatus.READY && isEnhancementsEnabled && ( + {hasNoDataset ? ( + + ) : ( <> - - + {fetchState.status === ResultStatus.NO_RESULTS && ( + + )} + {fetchState.status === ResultStatus.ERROR && ( + + )} + {fetchState.status === ResultStatus.UNINITIALIZED && ( + refetch$.next()} /> + )} + {fetchState.status === ResultStatus.LOADING && } + {fetchState.status === ResultStatus.READY && isEnhancementsEnabled && ( + <> + + + + )} + {fetchState.status === ResultStatus.READY && !isEnhancementsEnabled && ( + + + + + )} )} - {fetchState.status === ResultStatus.READY && !isEnhancementsEnabled && ( - - - - - )} ); } diff --git a/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx index 2aa288b9bdf1..0b3f1275d7ff 100644 --- a/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx @@ -35,9 +35,15 @@ export interface TopNavProps { }; showSaveQuery: boolean; isEnhancementsEnabled?: boolean; + useNoIndexPatternsTopNav?: boolean; } -export const TopNav = ({ opts, showSaveQuery, isEnhancementsEnabled }: TopNavProps) => { +export const TopNav = ({ + opts, + showSaveQuery, + isEnhancementsEnabled, + useNoIndexPatternsTopNav = false, +}: TopNavProps) => { const { services } = useOpenSearchDashboards(); const { data$, inspectorAdapters, savedSearch, indexPattern } = useDiscoverContext(); const [indexPatterns, setIndexPatterns] = useState(undefined); @@ -62,7 +68,13 @@ export const TopNav = ({ opts, showSaveQuery, isEnhancementsEnabled }: TopNavPro const showActionsInGroup = uiSettings.get('home:useNewHomePage'); const topNavLinks = savedSearch - ? getTopNavLinks(services, inspectorAdapters, savedSearch, isEnhancementsEnabled) + ? getTopNavLinks( + services, + inspectorAdapters, + savedSearch, + isEnhancementsEnabled, + useNoIndexPatternsTopNav + ) : []; connectStorageToQueryState( @@ -88,7 +100,7 @@ export const TopNav = ({ opts, showSaveQuery, isEnhancementsEnabled }: TopNavPro useEffect(() => { let isMounted = true; const initializeDataset = async () => { - await data.indexPatterns.ensureDefaultIndexPattern(); + await data.indexPatterns.ensureDefaultIndexPattern(isEnhancementsEnabled ? false : true); const defaultIndexPattern = await data.indexPatterns.getDefault(); // TODO: ROCKY do we need this? // const queryString = data.query.queryString; @@ -107,7 +119,7 @@ export const TopNav = ({ opts, showSaveQuery, isEnhancementsEnabled }: TopNavPro return () => { isMounted = false; }; - }, [data.indexPatterns, data.query]); + }, [data.indexPatterns, data.query, isEnhancementsEnabled]); useEffect(() => { const pageTitleSuffix = savedSearch?.id && savedSearch.title ? `: ${savedSearch.title}` : ''; @@ -164,18 +176,30 @@ export const TopNav = ({ opts, showSaveQuery, isEnhancementsEnabled }: TopNavPro {} : opts.onQuerySubmit} + savedQueryId={useNoIndexPatternsTopNav ? undefined : state.savedQuery} + onSavedQueryIdChange={useNoIndexPatternsTopNav ? () => {} : updateSavedQueryId} + datePickerRef={useNoIndexPatternsTopNav ? undefined : opts?.optionalRef?.datePickerRef} groupActions={showActionsInGroup} - screenTitle={screenTitle} + screenTitle={ + useNoIndexPatternsTopNav + ? i18n.translate('discover.noIndexPatterns.screenTitle', { + defaultMessage: 'Select data', + }) + : screenTitle + } queryStatus={queryStatus} /> diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index e5f87af2e974..4beafd982013 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -162,6 +162,8 @@ export const EditIndexPattern = withRouter( } if (indexPattern.id) { Promise.resolve(data.indexPatterns.delete(indexPattern.id)).then(function () { + const datasetService = data.query.queryString.getDatasetService(); + datasetService.removeFromRecentDatasets(indexPattern.id); history.push(''); }); } diff --git a/src/plugins/navigation/public/top_nav_menu/_index.scss b/src/plugins/navigation/public/top_nav_menu/_index.scss index da71d3047d06..68d2237a8522 100644 --- a/src/plugins/navigation/public/top_nav_menu/_index.scss +++ b/src/plugins/navigation/public/top_nav_menu/_index.scss @@ -9,7 +9,7 @@ } .osdTopNavMenuGroupedActions { - background-color: $euiColorEmptyShade; + background-color: $euiPageBackgroundColor; .newTopNavHeader & { margin: 0; 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);