From afa80814713c10390ead9b544a8d632b4340be0a Mon Sep 17 00:00:00 2001 From: ananzh Date: Thu, 13 Jul 2023 04:47:41 +0000 Subject: [PATCH] [Data Explorer] Implement data fetch logic in Discover 2.0 Issue Resolve: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/4397 Signed-off-by: ananzh --- .../application/components/discover.scss | 33 -- .../application/components/discover.tsx | 317 ------------------ .../view_components/canvas/canvas.tsx | 6 +- .../view_components/canvas/discover_table.tsx | 66 ++++ .../canvas/discover_table_app.tsx | 66 ++++ .../canvas/discover_table_service.tsx | 49 +++ .../utils/use_discover_canvas_service.ts | 49 +++ .../utils/get_sort_for_search_source.ts | 65 ++++ .../utils/index_pattern_helper.ts | 114 +++++++ .../utils/update_data_source.ts | 42 +++ .../view_components/utils/use_saved_search.ts | 131 ++++++++ .../application/angular/doc_viewer_links.tsx | 28 ++ 12 files changed, 614 insertions(+), 352 deletions(-) delete mode 100644 src/plugins/discover/public/application/components/discover.scss delete mode 100644 src/plugins/discover/public/application/components/discover.tsx create mode 100644 src/plugins/discover/public/application/view_components/canvas/discover_table.tsx create mode 100644 src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx create mode 100644 src/plugins/discover/public/application/view_components/canvas/discover_table_service.tsx create mode 100644 src/plugins/discover/public/application/view_components/canvas/utils/use_discover_canvas_service.ts create mode 100644 src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts create mode 100644 src/plugins/discover/public/application/view_components/utils/index_pattern_helper.ts create mode 100644 src/plugins/discover/public/application/view_components/utils/update_data_source.ts create mode 100644 src/plugins/discover/public/application/view_components/utils/use_saved_search.ts create mode 100644 src/plugins/discover_legacy/public/application/angular/doc_viewer_links.tsx diff --git a/src/plugins/discover/public/application/components/discover.scss b/src/plugins/discover/public/application/components/discover.scss deleted file mode 100644 index aaa5a4c5c90..00000000000 --- a/src/plugins/discover/public/application/components/discover.scss +++ /dev/null @@ -1,33 +0,0 @@ -.dscAppContainer { - flex-direction: column; - flex-grow: 1; - overflow: hidden; - - > * { - position: relative; - } -} - -.dscTimechart { - display: block; - position: relative; - - // SASSTODO: the visualizing component should have an option or a modifier - .series > rect { - fill-opacity: 0.5; - stroke-width: 1; - } -} - -.dscTimechart__header { - display: flex; - justify-content: center; - min-height: $euiSizeXXL; - padding: $euiSizeXS 0; -} - -.dscHistogram { - display: flex; - height: 200px; - padding: $euiSizeS; -} diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx deleted file mode 100644 index b9cd40f5fc2..00000000000 --- a/src/plugins/discover/public/application/components/discover.tsx +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import './discover.scss'; -import React, { useState, useCallback, useEffect } from 'react'; -import { - EuiPage, - EuiPageBody, - EuiButtonEmpty, - EuiButtonIcon, - EuiPageSideBar, - EuiPageContent, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { i18n } from '@osd/i18n'; -import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; -import { IUiSettingsClient, MountPoint } from 'opensearch-dashboards/public'; -import { HitsCounter } from './hits_counter'; -import { TimechartHeader } from './timechart_header'; -import { DiscoverSidebar } from './sidebar'; -import { DataGridTable } from './data_grid/data_grid_table'; -import { getServices, IndexPattern } from '../../opensearch_dashboards_services'; -// @ts-ignore -import { DiscoverNoResults } from '../angular/directives/no_results'; -import { DiscoverUninitialized } from '../angular/directives/uninitialized'; -import { DiscoverHistogram } from '../angular/directives/histogram'; -import { LoadingSpinner } from './loading_spinner/loading_spinner'; -import { SkipBottomButton } from './skip_bottom_button'; -import { - IndexPatternField, - search, - ISearchSource, - TimeRange, - Query, - IndexPatternAttributes, -} from '../../../../data/public'; -import { Chart } from '../angular/helpers/point_series'; -import { AppState } from '../angular/discover_state'; -import { SavedSearch } from '../../saved_searches'; - -import { SavedObject } from '../../../../../core/types'; -import { Vis } from '../../../../visualizations/public'; -import { TopNavMenuData } from '../../../../navigation/public'; -import { DocViewFilterFn } from '../doc_views/doc_views_types'; - -export interface DiscoverProps { - addColumn: (column: string) => void; - fetch: () => void; - fetchCounter: number; - fieldCounts: Record; - histogramData: Chart; - hits: number; - indexPattern: IndexPattern; - onAddFilter: DocViewFilterFn; - onChangeInterval: (interval: string) => void; - onMoveColumn: (columns: string, newIdx: number) => void; - onRemoveColumn: (column: string) => void; - onSetColumns: (columns: string[]) => void; - onSkipBottomButtonClick: () => void; - onSort: (sort: string[][]) => void; - opts: { - savedSearch: SavedSearch; - config: IUiSettingsClient; - indexPatternList: Array>; - timefield: string; - sampleSize: number; - fixedScroll: (el: HTMLElement) => void; - setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; - }; - resetQuery: () => void; - resultState: string; - rows: Array>; - searchSource: ISearchSource; - setIndexPattern: (id: string) => void; - showSaveQuery: boolean; - state: AppState; - timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; - timeRange?: { from: string; to: string }; - topNavMenu: TopNavMenuData[]; - updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; - updateSavedQueryId: (savedQueryId?: string) => void; - vis?: Vis; -} - -export function Discover({ - addColumn, - fetch, - fetchCounter, - fieldCounts, - histogramData, - hits, - indexPattern, - onAddFilter, - onChangeInterval, - onMoveColumn, - onRemoveColumn, - onSkipBottomButtonClick, - onSetColumns, - onSort, - opts, - resetQuery, - resultState, - rows, - searchSource, - setIndexPattern, - showSaveQuery, - state, - timefilterUpdateHandler, - timeRange, - topNavMenu, - updateQuery, - updateSavedQueryId, - vis, -}: DiscoverProps) { - const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const services = getServices(); - const { TopNavMenu } = services.navigation.ui; - const { savedSearch, indexPatternList, config } = opts; - const bucketAggConfig = vis?.data?.aggs?.aggs[1]; - const bucketInterval = - bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) - ? bucketAggConfig.buckets?.getInterval() - : undefined; - const [fixedScrollEl, setFixedScrollEl] = useState(); - - useEffect(() => (fixedScrollEl ? opts.fixedScroll(fixedScrollEl) : undefined), [ - fixedScrollEl, - opts, - ]); - const fixedScrollRef = useCallback( - (node: HTMLElement) => { - if (node !== null) { - setFixedScrollEl(node); - } - }, - [setFixedScrollEl] - ); - const displayTimeColumn = Boolean( - !config.get('doc_table:hideTimeColumn', false) && indexPattern.timeFieldName - ); - - return ( - - -

{savedSearch.title}

- - - - - - - - {!isSidebarClosed && ( -
- -
- )} - setIsSidebarClosed(!isSidebarClosed)} - data-test-subj="collapseSideBarButton" - aria-controls="discover-sidebar" - aria-expanded={isSidebarClosed ? 'false' : 'true'} - aria-label="Toggle sidebar" - className="dscCollapsibleSidebar__collapseButton euiButtonIcon--auto" - /> -
-
- - - - {resultState === 'none' && ( - - )} - {resultState === 'uninitialized' && } - - {/* Loading State */} - {resultState === 'loading' && ( -
- -
- )} - - {/* Ready State */} - {resultState === 'ready' && ( -
- - 0 ? hits : 0} - showResetButton={!!(savedSearch && savedSearch.id)} - onResetQuery={resetQuery} - /> - {opts.timefield && ( - - )} - - {opts.timefield && ( -
- {vis && rows.length !== 0 && ( -
- -
- )} -
- )} - -
-
-

- -

- {rows && rows.length && ( -
- ) || []} - onAddColumn={addColumn} - onFilter={onAddFilter} - onRemoveColumn={onRemoveColumn} - onSetColumns={onSetColumns} - onSort={onSort} - displayTimeColumn={displayTimeColumn} - services={services} - /> - - ​ - - {rows.length === opts.sampleSize && ( -
- - - window.scrollTo(0, 0)}> - - -
- )} -
- )} -
-
-
- )} -
-
-
-
-
-
- ); -} diff --git a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx index 8f512a10837..fe52673832b 100644 --- a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx @@ -8,6 +8,7 @@ import { AppMountParameters } from '../../../../../../core/public'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { DiscoverServices } from '../../../build_services'; import { TopNav } from './top_nav'; +import { DiscoverTable } from './discover_table'; interface CanvasProps { opts: { @@ -17,11 +18,12 @@ interface CanvasProps { export const Canvas = ({ opts }: CanvasProps) => { const { services } = useOpenSearchDashboards(); - + const { history: getHistory } = services; + const history = getHistory(); return (
- Canvas +
); }; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx new file mode 100644 index 00000000000..3161f1458db --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect } from 'react'; +import { History } from 'history'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { DiscoverServices } from '../../../build_services'; +import { SavedSearch } from '../../../saved_searches'; +import { DiscoverTableService } from './discover_table_service'; +import { fetchIndexPattern, fetchSavedSearch } from '../utils/index_pattern_helper'; + +export interface DiscoverTableProps { + services: DiscoverServices; + history: History; +} + +export const DiscoverTable = ({ history, services }: DiscoverTableProps) => { + const { core, chrome, data, uiSettings: config, toastNotifications } = services; + const [savedSearch, setSavedSearch] = useState(); + const [indexPattern, setIndexPattern] = useState(undefined); + // ToDo: get id from data explorer since it is handling the routing logic + // Original angular code: const savedSearchId = $route.current.params.id; + const savedSearchId = ''; + useEffect(() => { + const fetchData = async () => { + const indexPatternData = await fetchIndexPattern(data, config); + setIndexPattern(indexPatternData.loaded); + + const savedSearchData = await fetchSavedSearch( + core, + '', // basePath + history, + savedSearchId, + services, + toastNotifications + ); + if (savedSearchData && !savedSearchData?.searchSource.getField('index')) { + savedSearchData.searchSource.setField('index', indexPatternData); + } + setSavedSearch(savedSearchData); + + if (savedSearchId) { + chrome.recentlyAccessed.add( + savedSearchData.getFullPath(), + savedSearchData.title, + savedSearchData.id + ); + } + }; + fetchData(); + }, [data, config, core, chrome, toastNotifications, history, savedSearchId, services]); + + if (!savedSearch || !savedSearch.searchSource || !indexPattern) { + // ToDo: handle loading state + return null; + } + return ( + + ); +}; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx new file mode 100644 index 00000000000..1246e61f7f9 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React, { useState, useEffect } from 'react'; +import { EuiPage, EuiPageBody, EuiFlexGroup, EuiFlexItem, EuiPageContent } from '@elastic/eui'; +import { DataGridTable } from '../../components/data_grid/data_grid_table'; + +export const DiscoverTableApplication = ({ data$, indexPattern, savedSearch, services }) => { + const [fetchState, setFetchState] = useState({ + status: data$.getValue().status, + fetchCounter: 0, + fieldCounts: {}, + rows: [], + }); + + const { rows } = fetchState; + + useEffect(() => { + const subscription = data$.subscribe((next) => { + if ( + (next.status && next.status !== fetchState.status) || + (next.rows && next.rows !== fetchState.rows) + ) { + setFetchState({ ...fetchState, ...next }); + } + }); + return () => { + subscription.unsubscribe(); + }; + }, [data$, fetchState]); + + // ToDo: implement columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns using config, indexPattern, appState + + if (rows.length === 0) { + return
{'loading...'}
; + } else { + return ( + + + + + +
+ {}} + onFilter={() => {}} + onRemoveColumn={() => {}} + onSetColumns={() => {}} + onSort={() => {}} + sort={[]} + rows={rows} + displayTimeColumn={true} + services={services} + /> +
+
+
+
+
+
+ ); + } +}; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table_service.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_table_service.tsx new file mode 100644 index 00000000000..4c987d1eccd --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/discover_table_service.tsx @@ -0,0 +1,49 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect } from 'react'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; +import { DiscoverServices } from '../../../build_services'; +import { SavedSearch } from '../../../saved_searches'; +import { useDiscoverTableService } from './utils/use_discover_canvas_service'; +import { DiscoverTableApplication } from './discover_table_app'; + +export interface DiscoverTableAppProps { + services: DiscoverServices; + savedSearch: SavedSearch; + indexPattern: IndexPattern; +} + +export const DiscoverTableService = ({ + services, + savedSearch, + indexPattern, +}: DiscoverTableAppProps) => { + const { data$, refetch$ } = useDiscoverTableService({ + services, + savedSearch, + indexPattern, + }); + + // trigger manual fetch + // ToDo: remove this once we implement refetch data: + // Based on the controller, refetch$ should emit next when + // 1) appStateContainer interval and sort change + // 2) savedSearch id changes + // 3) timefilter.getRefreshInterval().pause === false + // 4) TopNavMenu updateQuery() is called + useEffect(() => { + refetch$.next(); + }, [refetch$]); + + return ( + + ); +}; diff --git a/src/plugins/discover/public/application/view_components/canvas/utils/use_discover_canvas_service.ts b/src/plugins/discover/public/application/view_components/canvas/utils/use_discover_canvas_service.ts new file mode 100644 index 00000000000..38d2b18b7b9 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/utils/use_discover_canvas_service.ts @@ -0,0 +1,49 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useMemo, useEffect } from 'react'; +import { DiscoverServices } from '../../../../build_services'; +import { SavedSearch } from '../../../../saved_searches'; +import { useSavedSearch } from '../../utils/use_saved_search'; +import { IndexPattern } from '../../../../opensearch_dashboards_services'; + +export interface DiscoverTableServiceProps { + services: DiscoverServices; + savedSearch: SavedSearch; + indexPattern: IndexPattern; +} + +export const useDiscoverTableService = ({ + services, + savedSearch, + indexPattern, +}: DiscoverTableServiceProps) => { + const searchSource = useMemo(() => { + savedSearch.searchSource.setField('index', indexPattern); + return savedSearch.searchSource; + }, [savedSearch, indexPattern]); + + const { data$, refetch$ } = useSavedSearch({ + searchSource, + services, + indexPattern, + }); + + useEffect(() => { + const dataSubscription = data$.subscribe((data) => {}); + const refetchSubscription = refetch$.subscribe((refetch) => {}); + + return () => { + dataSubscription.unsubscribe(); + refetchSubscription.unsubscribe(); + }; + }, [data$, refetch$]); + + return { + data$, + refetch$, + indexPattern, + }; +}; diff --git a/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts b/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts new file mode 100644 index 00000000000..f83c764565c --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { OpenSearchQuerySortValue, IndexPattern } from '../../../../opensearch_dashboards_services'; +import { SortOrder } from '../components/table_header/helpers'; +import { getSort } from './get_sort'; +import { getDefaultSort } from './get_default_sort'; + +/** + * Prepares sort for search source, that's sending the request to OpenSearch + * - Adds default sort if necessary + * - Handles the special case when there's sorting by date_nanos typed fields + * the addon of the numeric_type guarantees the right sort order + * when there are indices with date and indices with date_nanos field + */ +export function getSortForSearchSource( + sort?: SortOrder[], + indexPattern?: IndexPattern, + defaultDirection: string = 'desc' +): OpenSearchQuerySortValue[] { + if (!sort || !indexPattern) { + return []; + } else if (Array.isArray(sort) && sort.length === 0) { + sort = getDefaultSort(indexPattern, defaultDirection); + } + const { timeFieldName } = indexPattern; + return getSort(sort, indexPattern).map((sortPair: Record) => { + if (indexPattern.isTimeNanosBased() && timeFieldName && sortPair[timeFieldName]) { + return { + [timeFieldName]: { + order: sortPair[timeFieldName], + numeric_type: 'date_nanos', + }, + } as OpenSearchQuerySortValue; + } + return sortPair as OpenSearchQuerySortValue; + }); +} diff --git a/src/plugins/discover/public/application/view_components/utils/index_pattern_helper.ts b/src/plugins/discover/public/application/view_components/utils/index_pattern_helper.ts new file mode 100644 index 00000000000..f0586a07652 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/index_pattern_helper.ts @@ -0,0 +1,114 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { SearchSource, IndexPattern } from 'src/plugins/data/public'; +import { SavedObject, ToastsStart } from 'opensearch-dashboards/public'; +import { redirectWhenMissing, getUrlTracker } from '../../../opensearch_dashboards_services'; +import { getIndexPatternId } from '../../helpers/get_index_pattern_id'; + +export type IndexPatternSavedObject = SavedObject & { title: string }; +export interface IndexPatternData { + loaded: IndexPattern; + stateVal: string; + stateValFound: boolean; +} + +export const fetchIndexPattern = async (data, config) => { + await data.indexPatterns.ensureDefaultIndexPattern(); + const indexPatternList = await data.indexPatterns.getCache(); + const id = getIndexPatternId('', indexPatternList, config.get('defaultIndex')); + const indexPatternData = await data.indexPatterns.get(id); + const ip: IndexPatternData = { + loaded: indexPatternData, + stateVal: '', // ToDo: get stateVal from appStateContainer + stateValFound: false, // ToDo: get stateValFound from appStateContainer + }; + return ip; +}; + +export const fetchSavedSearch = async ( + core, + basePath, + history, + savedSearchId, + services, + toastNotifications +) => { + try { + const savedSearch = await services.getSavedSearchById(savedSearchId); + return savedSearch; + } catch (error) { + // ToDo: handle redirect with Data Explorer + redirectWhenMissing({ + history, + navigateToApp: core.application.navigateToApp, + basePath, + mapping: { + search: '/', + 'index-pattern': { + app: 'management', + path: `opensearch-dashboards/objects/savedSearches/${savedSearchId}`, + }, + }, + toastNotifications, + onBeforeRedirect() { + getUrlTracker().setTrackedUrl('/'); + }, + }); + } +}; + +export function resolveIndexPattern( + ip: IndexPatternData, + searchSource: SearchSource, + toastNotifications: ToastsStart +) { + const { loaded: loadedIndexPattern, stateVal, stateValFound } = ip; + + const ownIndexPattern = searchSource.getOwnField('index'); + + if (ownIndexPattern && !stateVal) { + return ownIndexPattern; + } + + if (stateVal && !stateValFound) { + const warningTitle = i18n.translate('discover.valueIsNotConfiguredIndexPatternIDWarningTitle', { + defaultMessage: '{stateVal} is not a configured index pattern ID', + values: { + stateVal: `"${stateVal}"`, + }, + }); + + if (ownIndexPattern) { + toastNotifications.addWarning({ + title: warningTitle, + text: i18n.translate('discover.showingSavedIndexPatternWarningDescription', { + defaultMessage: + 'Showing the saved index pattern: "{ownIndexPatternTitle}" ({ownIndexPatternId})', + values: { + ownIndexPatternTitle: ownIndexPattern.title, + ownIndexPatternId: ownIndexPattern.id, + }, + }), + }); + return ownIndexPattern; + } + + toastNotifications.addWarning({ + title: warningTitle, + text: i18n.translate('discover.showingDefaultIndexPatternWarningDescription', { + defaultMessage: + 'Showing the default index pattern: "{loadedIndexPatternTitle}" ({loadedIndexPatternId})', + values: { + loadedIndexPatternTitle: loadedIndexPattern.title, + loadedIndexPatternId: loadedIndexPattern.id, + }, + }), + }); + } + + return loadedIndexPattern; +} diff --git a/src/plugins/discover/public/application/view_components/utils/update_data_source.ts b/src/plugins/discover/public/application/view_components/utils/update_data_source.ts new file mode 100644 index 00000000000..00ab963e886 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/update_data_source.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ISearchSource, IndexPattern } from 'src/plugins/data/public'; +import { DiscoverServices } from '../../../build_services'; +import { SortOrder } from '../../../saved_searches/types'; +import { getSortForSearchSource } from './get_sort_for_search_source'; +import { SORT_DEFAULT_ORDER_SETTING, SAMPLE_SIZE_SETTING } from '../../../../common'; + +export interface UpdateDataSourceProps { + searchSource: ISearchSource; + indexPattern: IndexPattern; + services: DiscoverServices; + sort: SortOrder[] | undefined; +} + +export const updateDataSource = ({ + searchSource, + indexPattern, + services, + sort, +}: UpdateDataSourceProps) => { + const { uiSettings, data } = services; + const sortForSearchSource = getSortForSearchSource( + sort, + indexPattern, + uiSettings.get(SORT_DEFAULT_ORDER_SETTING) + ); + const size = uiSettings.get(SAMPLE_SIZE_SETTING); + const updatedSearchSource = searchSource + .setField('index', indexPattern) + .setField('sort', sortForSearchSource) + .setField('size', size) + .setField('query', data.query.queryString.getQuery() || null) + .setField('filter', data.query.filterManager.getFilters()) + .setField('highlightAll', true) + .setField('version', true); + + return updatedSearchSource; +}; diff --git a/src/plugins/discover/public/application/view_components/utils/use_saved_search.ts b/src/plugins/discover/public/application/view_components/utils/use_saved_search.ts new file mode 100644 index 00000000000..0696e5a562a --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/use_saved_search.ts @@ -0,0 +1,131 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useCallback, useMemo, useRef } from 'react'; +import { ISearchSource, IndexPattern } from 'src/plugins/data/public'; +import { BehaviorSubject, Subject, merge } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; +import { useEffect } from 'react'; +import { DiscoverServices } from '../../../build_services'; + +import { validateTimeRange } from '../../../application/helpers/validate_time_range'; +import { updateDataSource } from './update_data_source'; + +export enum FetchStatus { + UNINITIALIZED = 'uninitialized', + LOADING = 'loading', + COMPLETE = 'complete', + ERROR = 'error', +} + +export interface SavedSearchData { + status: FetchStatus; + fetchCounter?: number; + fieldCounts?: Record; + fetchError?: Error; + hits?: number; + rows?: any[]; // ToDo: type +} + +export type SavedSearchRefetch = 'refetch' | undefined; + +export type DataSubject = BehaviorSubject; +export type RefetchSubject = BehaviorSubject; + +export const useSavedSearch = ({ + indexPattern, + searchSource, + services, +}: { + indexPattern: IndexPattern; + searchSource: ISearchSource; + services: DiscoverServices; +}) => { + const { data, filterManager } = services; + const timefilter = data.query.timefilter.timefilter; + const fetchStateRef = useRef<{ + abortController: AbortController | undefined; + fieldCounts: Record; + fetchStatus: FetchStatus; + }>({ + abortController: undefined, + fieldCounts: {}, + fetchStatus: FetchStatus.UNINITIALIZED, + }); + + const data$ = useMemo( + () => new BehaviorSubject({ state: FetchStatus.UNINITIALIZED }), + [] + ); + const refetch$ = useMemo(() => new Subject(), []); + + const fetch = useCallback(async () => { + if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) { + return Promise.reject(); + } + + if (fetchStateRef.current.abortController) fetchStateRef.current.abortController.abort(); + fetchStateRef.current.abortController = new AbortController(); + const sort = undefined; + const updatedSearchSource = updateDataSource({ searchSource, indexPattern, services, sort }); + + try { + const fetchResp = await updatedSearchSource.fetch({ + abortSignal: fetchStateRef.current.abortController.signal, + }); + const hits = fetchResp.hits.total as number; + const rows = fetchResp.hits.hits; + for (const row of rows) { + const fields = Object.keys(indexPattern.flattenHit(row)); + for (const fieldName of fields) { + fetchStateRef.current.fieldCounts[fieldName] = + (fetchStateRef.current.fieldCounts[fieldName] || 0) + 1; + } + } + fetchStateRef.current.fieldCounts = fetchStateRef.current.fieldCounts!; + fetchStateRef.current.fetchStatus = FetchStatus.COMPLETE; + data$.next({ + status: FetchStatus.COMPLETE, + fieldCounts: fetchStateRef.current.fieldCounts, + hits, + rows, + }); + } catch (err) { + // ToDo: handle the error + } + }, [data$, timefilter, services, searchSource, indexPattern, fetchStateRef]); + + useEffect(() => { + const fetch$ = merge( + refetch$, + filterManager.getFetches$(), + timefilter.getFetch$(), + timefilter.getAutoRefreshFetch$(), + data.query.queryString.getUpdates$() + ).pipe(debounceTime(100)); + + const subscription = fetch$.subscribe(() => { + (async () => { + try { + await fetch(); + } catch (error) { + data$.next({ + status: FetchStatus.ERROR, + fetchError: error, + }); + } + })(); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [data$, data.query.queryString, filterManager, refetch$, timefilter, fetch]); + + return { + data$, + refetch$, + }; +}; diff --git a/src/plugins/discover_legacy/public/application/angular/doc_viewer_links.tsx b/src/plugins/discover_legacy/public/application/angular/doc_viewer_links.tsx new file mode 100644 index 00000000000..763a75e5130 --- /dev/null +++ b/src/plugins/discover_legacy/public/application/angular/doc_viewer_links.tsx @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { DocViewerLinks } from '../components/doc_viewer_links/doc_viewer_links'; + +export function createDocViewerLinksDirective(reactDirective: any) { + return reactDirective( + (props: any) => { + return ; + }, + [ + 'hit', + ['indexPattern', { watchDepth: 'reference' }], + ['columns', { watchDepth: 'collection' }], + ], + { + restrict: 'E', + scope: { + hit: '=', + indexPattern: '=', + columns: '=?', + }, + } + ); +}