From 1d1ec2775d2635b46f92025da1b26c374b68e4c9 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Thu, 24 Oct 2024 14:07:13 -0700 Subject: [PATCH] show create indexed view banner in discover canvas; show indexed views in dataset selector Signed-off-by: Amardeepsingh Siglani --- src/plugins/data/common/datasets/types.ts | 26 +++++ src/plugins/data/public/index.ts | 1 + .../query_string/dataset_service/types.ts | 14 ++- .../data/public/query/query_string/index.ts | 1 + .../ui/dataset_selector/configurator.tsx | 97 ++++++++++++++++--- .../view_components/canvas/index.tsx | 66 ++++++++++--- .../view_components/utils/use_search.ts | 2 +- .../query_enhancements/public/plugin.tsx | 7 +- .../public/search/ppl_search_interceptor.ts | 2 +- .../public/search/sql_search_interceptor.ts | 2 +- 10 files changed, 184 insertions(+), 34 deletions(-) diff --git a/src/plugins/data/common/datasets/types.ts b/src/plugins/data/common/datasets/types.ts index e777eb8a45e8..09ffd715409c 100644 --- a/src/plugins/data/common/datasets/types.ts +++ b/src/plugins/data/common/datasets/types.ts @@ -247,6 +247,8 @@ export interface Dataset extends BaseDataset { timeFieldName?: string; /** Optional language to default to from the language selector */ language?: string; + /** Optional name of the indexed view to search data from */ + indexedView?: string; } export interface DatasetField { @@ -256,6 +258,30 @@ export interface DatasetField { // TODO: osdFieldType? } +export type CanvasBannerProps = + | { + componentType: 'callout'; + title: string; + iconType?: string; + content?: React.ReactNode; + } + | { + componentType: 'callout'; + title?: string; + iconType?: string; + content: React.ReactNode; + } + | { + componentType: 'custom'; + content: React.ReactNode; + }; + export interface DatasetSearchOptions { strategy?: string; + /** + * Returns props used to render a banner on the Discover canvas/results section + */ + getBannerProps?: ( + searchStatus: string + ) => CanvasBannerProps | Promise | undefined; } diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index e39722118721..1eba61b1373c 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -493,6 +493,7 @@ export { QueryStart, PersistedLog, LanguageReference, + IndexedViewsService, } from './query'; export { AggsStart } from './search/aggs'; 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 020bc369ff5e..933336e1e840 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 @@ -16,6 +16,14 @@ export interface DataStructureFetchOptions { paginationToken?: string; } +export interface IndexedView { + name: string; +} + +export interface IndexedViewsService { + getIndexedViews: (dataset: Dataset) => Promise; +} + /** * Configuration for handling dataset operations. */ @@ -72,7 +80,7 @@ export interface DatasetTypeConfig { * Retrieves the search options to be used for running the query on the data connection associated * with this Dataset */ - getSearchOptions?: () => DatasetSearchOptions; + getSearchOptions?: (dataset: Dataset) => DatasetSearchOptions; /** * Combines a list of user selected data structures into a single one to use in discover. * @see https://github.com/opensearch-project/OpenSearch-Dashboards/issues/8362. @@ -82,4 +90,8 @@ export interface DatasetTypeConfig { * Returns a list of sample queries for this dataset type */ getSampleQueries?: (dataset: Dataset, language: string) => any; + /** + * Service used for indexedViews related operations + */ + indexedViewsService?: IndexedViewsService; } diff --git a/src/plugins/data/public/query/query_string/index.ts b/src/plugins/data/public/query/query_string/index.ts index 96f473a3aea5..e5f0530d4e0c 100644 --- a/src/plugins/data/public/query/query_string/index.ts +++ b/src/plugins/data/public/query/query_string/index.ts @@ -34,6 +34,7 @@ export { DatasetService, DatasetServiceContract, DatasetTypeConfig, + IndexedViewsService, } from './dataset_service'; export { LanguageServiceContract, diff --git a/src/plugins/data/public/ui/dataset_selector/configurator.tsx b/src/plugins/data/public/ui/dataset_selector/configurator.tsx index 2a509eeed288..79b6f179ef89 100644 --- a/src/plugins/data/public/ui/dataset_selector/configurator.tsx +++ b/src/plugins/data/public/ui/dataset_selector/configurator.tsx @@ -22,6 +22,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { BaseDataset, DEFAULT_DATA, Dataset, DatasetField } from '../../../common'; import { getIndexPatterns, getQueryService } from '../../services'; import { IDataPluginServices } from '../../types'; +import { IndexedView } from '../../query/query_string/dataset_service'; export const Configurator = ({ services, @@ -41,7 +42,7 @@ export const Configurator = ({ const languageService = queryService.queryString.getLanguageService(); const indexPatternsService = getIndexPatterns(); const type = queryString.getDatasetService().getType(baseDataset.type); - const languages = type?.supportedLanguages(baseDataset) || []; + const [languages, setLanguages] = useState(type?.supportedLanguages(baseDataset) || []); const [dataset, setDataset] = useState(baseDataset); const [timeFields, setTimeFields] = useState([]); @@ -52,25 +53,49 @@ export const Configurator = ({ defaultMessage: "I don't want to use the time filter", } ); - const [language, setLanguage] = useState(() => { + const [selectedLanguage, setSelectedLanguage] = useState(''); + const indexedViewsService = type?.indexedViewsService; + const [selectedIndexedView, setSelectedIndexedView] = useState(); + const [indexedViews, setIndexedViews] = useState([]); + const [loadingIndexedViews, setLoadingIndexedViews] = useState(false); + + useEffect(() => { + setLanguages(type?.supportedLanguages(dataset) || []); + }, [dataset, type]); + + useEffect(() => { const currentLanguage = queryString.getQuery().language; if (languages.includes(currentLanguage)) { - return currentLanguage; + setSelectedLanguage(currentLanguage); } - return languages[0]; - }); + setSelectedLanguage(languages[0]); + }, [languages, queryString]); + + useEffect(() => { + const getIndexedViews = async () => { + if (indexedViewsService) { + setLoadingIndexedViews(true); + const fetchedIndexedViews = await indexedViewsService.getIndexedViews(baseDataset); + setLoadingIndexedViews(false); + setIndexedViews(fetchedIndexedViews || []); + } + }; + + getIndexedViews(); + }, [indexedViewsService, baseDataset]); const submitDisabled = useMemo(() => { return ( - timeFieldName === undefined && - !( - languageService.getLanguage(language)?.hideDatePicker || - dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN - ) && - timeFields && - timeFields.length > 0 + loadingIndexedViews || + (timeFieldName === undefined && + !( + languageService.getLanguage(selectedLanguage)?.hideDatePicker || + dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN + ) && + timeFields && + timeFields.length > 0) ); - }, [dataset, language, timeFieldName, timeFields, languageService]); + }, [dataset, selectedLanguage, timeFieldName, timeFields, languageService, loadingIndexedViews]); useEffect(() => { const fetchFields = async () => { @@ -123,6 +148,46 @@ export const Configurator = ({ > + {indexedViews.length > 0 && ( + + ({ + text: name, + value: name, + }))} + value={selectedIndexedView} + onChange={(e) => { + setSelectedIndexedView(e.target.value); + setDataset({ + ...dataset, + indexedView: e.target.value, + title: `${dataset.title}.${e.target.value}`, + }); + }} + hasNoInitialSelection + /> + + )} { - setLanguage(e.target.value); + setSelectedLanguage(e.target.value); setDataset({ ...dataset, language: e.target.value }); }} /> - {!languageService.getLanguage(language)?.hideDatePicker && + {!languageService.getLanguage(selectedLanguage)?.hideDatePicker && (dataset.type === DEFAULT_DATA.SET_TYPES.INDEX_PATTERN ? ( (); + const [canvasBanner, setCanvasBanner] = useState(null); const { columns } = useSelector((state) => { const stateColumns = state.discover.columns; @@ -77,8 +79,6 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR useEffect(() => { const subscription = data$.subscribe((next) => { - if (next.status === ResultStatus.LOADING) return; - let shouldUpdateState = false; if (next.status !== fetchState.status) shouldUpdateState = true; @@ -123,6 +123,54 @@ export default function DiscoverCanvas({ setHeaderActionMenu, history, optionalR } }; const showSaveQuery = !!capabilities.discover?.saveQuery; + const query = data.query.queryString.getQuery(); + + const updateCanvasBanner = useCallback( + (bannerData: CanvasBannerProps) => { + switch (bannerData.componentType) { + case 'callout': + setCanvasBanner( + <> + + + {bannerData.content} + + + ); + break; + case 'custom': + setCanvasBanner(bannerData.content); + break; + default: + setCanvasBanner(null); + } + }, + [setCanvasBanner] + ); + + useEffect(() => { + if (query.dataset) { + const typeConfig = data.query.queryString.getDatasetService().getType(query.dataset.type); + let bannerData; + if ( + (bannerData = typeConfig + ?.getSearchOptions?.(query.dataset) + ?.getBannerProps?.(fetchState.status)) + ) { + if (bannerData instanceof Promise) { + bannerData + .then((newBannerData) => updateCanvasBanner(newBannerData)) + .catch(() => { + // No-op + }); + } else if (bannerData) { + updateCanvasBanner(bannerData); + } else { + setCanvasBanner(null); + } + } + } + }, [query.dataset, data, fetchState.status, updateCanvasBanner, setCanvasBanner]); return ( - {fetchState.status === ResultStatus.NO_RESULTS && ( - - )} - {fetchState.status === ResultStatus.ERROR && ( + {isEnhancementsEnabled && canvasBanner} + {(fetchState.status === ResultStatus.NO_RESULTS || + fetchState.status === ResultStatus.ERROR) && ( { let elapsedMs; try { // Only show loading indicator if we are fetching when the rows are empty - if (fetchStateRef.current.rows?.length === 0) { + if (!fetchStateRef.current.rows || fetchStateRef.current.rows.length === 0) { data$.next({ status: ResultStatus.LOADING, queryStatus: { startTime } }); } diff --git a/src/plugins/query_enhancements/public/plugin.tsx b/src/plugins/query_enhancements/public/plugin.tsx index ef3512cb797b..94dbfebcaa68 100644 --- a/src/plugins/query_enhancements/public/plugin.tsx +++ b/src/plugins/query_enhancements/public/plugin.tsx @@ -71,7 +71,9 @@ export class QueryEnhancementsPlugin title: 'PPL', search: pplSearchInterceptor, getQueryString: (query: Query) => { - return `source = ${query.dataset?.title}`; + const dataset = query.dataset; + const source = dataset?.indexedView ?? dataset?.title; + return `source = ${source}`; }, fields: { filterable: false, @@ -96,7 +98,8 @@ export class QueryEnhancementsPlugin title: 'SQL', search: sqlSearchInterceptor, getQueryString: (query: Query) => { - return `SELECT * FROM ${query.dataset?.title} LIMIT 10`; + const source = query.dataset?.indexedView ?? query.dataset?.title; + return `SELECT * FROM ${source} LIMIT 10`; }, fields: { filterable: false, diff --git a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts index aecb7e32b009..e1b833d2177e 100644 --- a/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts +++ b/src/plugins/query_enhancements/public/search/ppl_search_interceptor.ts @@ -63,7 +63,7 @@ export class PPLSearchInterceptor extends SearchInterceptor { const datasetTypeConfig = this.queryService.queryString .getDatasetService() .getType(datasetType); - strategy = datasetTypeConfig?.getSearchOptions?.().strategy ?? strategy; + strategy = datasetTypeConfig?.getSearchOptions?.(dataset).strategy ?? strategy; } return this.runSearch(request, options.abortSignal, strategy); diff --git a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts index 4e62526653f9..f308768ca9b8 100644 --- a/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts +++ b/src/plugins/query_enhancements/public/search/sql_search_interceptor.ts @@ -61,7 +61,7 @@ export class SQLSearchInterceptor extends SearchInterceptor { const datasetTypeConfig = this.queryService.queryString .getDatasetService() .getType(datasetType); - strategy = datasetTypeConfig?.getSearchOptions?.().strategy ?? strategy; + strategy = datasetTypeConfig?.getSearchOptions?.(dataset).strategy ?? strategy; } return this.runSearch(request, options.abortSignal, strategy);