From c088ba13859cd8b3bd3a79157ba821089bdf596d 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 --- .../search/search_source/search_source.ts | 35 +++++ .../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 ++++ 11 files changed, 649 insertions(+), 2 deletions(-) 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/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 9a85ec85ce87..c1d93c4b5b3f 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -81,6 +81,8 @@ */ import { setWith } from '@elastic/safer-lodash-set'; +import { Observable, from } from 'rxjs'; +import { switchMap, tap, of } from 'rxjs/operators'; import { uniqueId, uniq, extend, pick, difference, omit, isObject, keys, isFunction } from 'lodash'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; @@ -296,6 +298,39 @@ export class SearchSource { return response; } + /** + * Fetch this source and return Observable + */ + fetch$(options: ISearchOptions = {}) { + const { getConfig } = this.dependencies; + + return from(this.requestIsStarting(options)).pipe( + switchMap(() => this.flatten()), + tap((searchRequest) => { + this.history = [searchRequest]; + let response$: Observable; + + if (getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES)) { + response$ = from(this.legacyFetch(searchRequest, options)); + } else { + const indexPattern = this.getField('index'); + searchRequest.dataSourceId = indexPattern?.dataSourceRef?.id; + + response$ = from(this.fetchSearch(searchRequest, options)); + } + + return response$; + }), + switchMap((response) => { + if ((response as any).error) { + throw new RequestFailure(null, response); + } + + return of(response); + }) + ); + } + /** * Add a handler that will be notified whenever requests start * @param {Function} handler 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 8f512a10837f..fe52673832b2 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 000000000000..3161f1458db2 --- /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 000000000000..1246e61f7f9d --- /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 000000000000..4c987d1eccda --- /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 000000000000..38d2b18b7b9b --- /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 000000000000..f83c764565c5 --- /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 000000000000..f0586a07652a --- /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 000000000000..00ab963e8863 --- /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 000000000000..0696e5a562a5 --- /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 000000000000..763a75e51300 --- /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: '=?', + }, + } + ); +}