diff --git a/src/plugins/discover/public/application/angular/context.html b/src/plugins/discover/public/application/angular/context.html index adafb3a62275f..9fe2708d84b69 100644 --- a/src/plugins/discover/public/application/angular/context.html +++ b/src/plugins/discover/public/application/angular/context.html @@ -2,6 +2,7 @@ anchor-id="contextAppRoute.anchorId" columns="contextAppRoute.state.columns" index-pattern="contextAppRoute.indexPattern" + index-pattern-id="contextAppRoute.indexPatternId" app-state="contextAppRoute.state" state-container="contextAppRoute.stateContainer" filters="contextAppRoute.filters" diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js index fb3493475b699..f24454c1cc57e 100644 --- a/src/plugins/discover/public/application/angular/context.js +++ b/src/plugins/discover/public/application/angular/context.js @@ -8,12 +8,19 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { CONTEXT_DEFAULT_SIZE_SETTING } from '../../../common'; +import { + CONTEXT_DEFAULT_SIZE_SETTING, + CONTEXT_STEP_SETTING, + CONTEXT_TIE_BREAKER_FIELDS_SETTING, + SEARCH_FIELDS_FROM_SOURCE, +} from '../../../common'; import { getAngularModule, getServices } from '../../kibana_services'; import './context_app'; import { getState } from './context_state'; import contextAppRouteTemplate from './context.html'; import { getRootBreadcrumbs } from '../helpers/breadcrumbs'; +import { getContextQueryDefaults } from './context_query_state'; +import { getFirstSortableField } from './context/api/utils/sorting'; const k7Breadcrumbs = ($route) => { const { indexPattern } = $route.current.locals; @@ -49,15 +56,30 @@ getAngularModule().config(($routeProvider) => { }); function ContextAppRouteController($routeParams, $scope, $route) { + this.indexPattern = $route.current.locals.indexPattern.ip; + this.anchorId = $routeParams.id; + this.indexPatternId = $route.current.params.indexPatternId; + const { uiSettings, history, core } = getServices(); const filterManager = getServices().filterManager; - const indexPattern = $route.current.locals.indexPattern.ip; + const stateContainer = getState({ - defaultStepSize: getServices().uiSettings.get(CONTEXT_DEFAULT_SIZE_SETTING), - timeFieldName: indexPattern.timeFieldName, - storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'), - history: getServices().history(), - toasts: getServices().core.notifications.toasts, - uiSettings: getServices().core.uiSettings, + defaultStepSize: parseInt(uiSettings.get(CONTEXT_DEFAULT_SIZE_SETTING), 10), + timeFieldName: this.indexPattern.timeFieldName, + storeInSessionStorage: uiSettings.get('state:storeInSessionStorage'), + history: history(), + toasts: core.notifications.toasts, + uiSettings: core.uiSettings, + getContextQueryDefaults: () => + getContextQueryDefaults( + this.indexPatternId, + this.anchorId, + parseInt(uiSettings.get(CONTEXT_STEP_SETTING), 10), + getFirstSortableField( + this.indexPattern, + uiSettings.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING) + ), + !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE) + ), }); const { startSync: startStateSync, @@ -70,8 +92,6 @@ function ContextAppRouteController($routeParams, $scope, $route) { } = stateContainer; this.stateContainer = stateContainer; this.state = { ...appState.getState() }; - this.anchorId = $routeParams.id; - this.indexPattern = indexPattern; filterManager.setFilters(_.cloneDeep(getFilters())); startStateSync(); diff --git a/src/plugins/discover/public/application/angular/context/api/context.ts b/src/plugins/discover/public/application/angular/context/api/context.ts index cd81ca7b216b2..0d9f8a64dad8b 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -18,10 +18,7 @@ import { getServices } from '../../../../kibana_services'; export type SurrDocType = 'successors' | 'predecessors'; export type EsHitRecord = Required< - Pick< - estypes.SearchResponse['hits']['hits'][number], - '_id' | 'fields' | 'sort' | '_index' | '_version' - > + Pick > & { _source?: Record; _score?: number; diff --git a/src/plugins/discover/public/application/angular/context/query/actions.tsx b/src/plugins/discover/public/application/angular/context/query/actions.tsx index f79c28bf6a120..fb530874d1e3b 100644 --- a/src/plugins/discover/public/application/angular/context/query/actions.tsx +++ b/src/plugins/discover/public/application/angular/context/query/actions.tsx @@ -17,13 +17,13 @@ import { fetchAnchorProvider } from '../api/anchor'; import { EsHitRecord, EsHitRecordList, fetchContextProvider, SurrDocType } from '../api/context'; import { getQueryParameterActions } from '../query_parameters'; import { - ContextAppState, + ContextQueryState, FailureReason, LoadingStatus, LoadingStatusEntry, LoadingStatusState, QueryParameters, -} from '../../context_app_state'; +} from '../../context_query_state'; interface DiscoverPromise extends PromiseConstructor { try: (fn: () => Promise) => Promise; @@ -43,7 +43,7 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { indexPatterns ); - const setFailedStatus = (state: ContextAppState) => ( + const setFailedStatus = (state: ContextQueryState) => ( subject: keyof LoadingStatusState, details: LoadingStatusEntry = {} ) => @@ -53,17 +53,17 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { ...details, }); - const setLoadedStatus = (state: ContextAppState) => (subject: keyof LoadingStatusState) => + const setLoadedStatus = (state: ContextQueryState) => (subject: keyof LoadingStatusState) => (state.loadingStatus[subject] = { status: LoadingStatus.LOADED, }); - const setLoadingStatus = (state: ContextAppState) => (subject: keyof LoadingStatusState) => + const setLoadingStatus = (state: ContextQueryState) => (subject: keyof LoadingStatusState) => (state.loadingStatus[subject] = { status: LoadingStatus.LOADING, }); - const fetchAnchorRow = (state: ContextAppState) => () => { + const fetchAnchorRow = (state: ContextQueryState) => () => { const { queryParameters: { indexPatternId, anchorId, sort, tieBreakerField }, } = state; @@ -100,7 +100,7 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { ); }; - const fetchSurroundingRows = (type: SurrDocType, state: ContextAppState) => { + const fetchSurroundingRows = (type: SurrDocType, state: ContextQueryState) => { const { queryParameters: { indexPatternId, sort, tieBreakerField }, rows: { anchor }, @@ -153,40 +153,40 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { ); }; - const fetchContextRows = (state: ContextAppState) => () => + const fetchContextRows = (state: ContextQueryState) => () => Promise.all([ fetchSurroundingRows('predecessors', state), fetchSurroundingRows('successors', state), ]); - const fetchAllRows = (state: ContextAppState) => () => + const fetchAllRows = (state: ContextQueryState) => () => Promise.try(fetchAnchorRow(state)).then(fetchContextRows(state)); - const fetchContextRowsWithNewQueryParameters = (state: ContextAppState) => ( + const fetchContextRowsWithNewQueryParameters = (state: ContextQueryState) => ( queryParameters: QueryParameters ) => { setQueryParameters(state)(queryParameters); return fetchContextRows(state)(); }; - const fetchAllRowsWithNewQueryParameters = (state: ContextAppState) => ( + const fetchAllRowsWithNewQueryParameters = (state: ContextQueryState) => ( queryParameters: QueryParameters ) => { setQueryParameters(state)(queryParameters); return fetchAllRows(state)(); }; - const fetchGivenPredecessorRows = (state: ContextAppState) => (count: number) => { + const fetchGivenPredecessorRows = (state: ContextQueryState) => (count: number) => { setPredecessorCount(state)(count); return fetchSurroundingRows('predecessors', state); }; - const fetchGivenSuccessorRows = (state: ContextAppState) => (count: number) => { + const fetchGivenSuccessorRows = (state: ContextQueryState) => (count: number) => { setSuccessorCount(state)(count); return fetchSurroundingRows('successors', state); }; - const setAllRows = (state: ContextAppState) => ( + const setAllRows = (state: ContextQueryState) => ( predecessorRows: EsHitRecordList, anchorRow: EsHitRecord, successorRows: EsHitRecordList diff --git a/src/plugins/discover/public/application/angular/context/query/state.ts b/src/plugins/discover/public/application/angular/context/query/state.ts index fefadf9009185..43f9e2c66d67c 100644 --- a/src/plugins/discover/public/application/angular/context/query/state.ts +++ b/src/plugins/discover/public/application/angular/context/query/state.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { LoadingStatus, LoadingStatusState } from '../../context_app_state'; +import { LoadingStatus, LoadingStatusState } from '../../context_query_state'; export function createInitialLoadingStatusState(): LoadingStatusState { return { diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts b/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts index fac3e1ea6fad6..8e4fb3ceaef68 100644 --- a/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts +++ b/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts @@ -9,11 +9,11 @@ import { getQueryParameterActions } from './actions'; import { FilterManager, SortDirection } from '../../../../../../data/public'; import { coreMock } from '../../../../../../../core/public/mocks'; -import { ContextAppState, LoadingStatus, QueryParameters } from '../../context_app_state'; +import { ContextQueryState, LoadingStatus, QueryParameters } from '../../context_query_state'; import { EsHitRecord } from '../api/context'; const setupMock = coreMock.createSetup(); -let state: ContextAppState; +let state: ContextQueryState; let filterManager: FilterManager; let filterManagerSpy: jest.SpyInstance; diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/actions.ts b/src/plugins/discover/public/application/angular/context/query_parameters/actions.ts index 02eab422b68a4..ffaf65e5e3737 100644 --- a/src/plugins/discover/public/application/angular/context/query_parameters/actions.ts +++ b/src/plugins/discover/public/application/angular/context/query_parameters/actions.ts @@ -16,14 +16,14 @@ import { IndexPatternField, } from '../../../../../../data/public'; import { popularizeField } from '../../../helpers/popularize_field'; -import { ContextAppState, QueryParameters } from '../../context_app_state'; +import { ContextQueryState, QueryParameters } from '../../context_query_state'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, QUERY_PARAMETER_KEYS } from './constants'; export function getQueryParameterActions( filterManager: FilterManager, indexPatterns?: IndexPatternsContract ) { - const setPredecessorCount = (state: ContextAppState) => (predecessorCount: number) => { + const setPredecessorCount = (state: ContextQueryState) => (predecessorCount: number) => { return (state.queryParameters.predecessorCount = clamp( MIN_CONTEXT_SIZE, MAX_CONTEXT_SIZE, @@ -31,7 +31,7 @@ export function getQueryParameterActions( )); }; - const setSuccessorCount = (state: ContextAppState) => (successorCount: number) => { + const setSuccessorCount = (state: ContextQueryState) => (successorCount: number) => { return (state.queryParameters.successorCount = clamp( MIN_CONTEXT_SIZE, MAX_CONTEXT_SIZE, @@ -39,7 +39,7 @@ export function getQueryParameterActions( )); }; - const setQueryParameters = (state: ContextAppState) => (queryParameters: QueryParameters) => { + const setQueryParameters = (state: ContextQueryState) => (queryParameters: QueryParameters) => { return Object.assign(state.queryParameters, pick(queryParameters, QUERY_PARAMETER_KEYS)); }; @@ -47,7 +47,7 @@ export function getQueryParameterActions( filterManager.setFilters(filters); }; - const addFilter = (state: ContextAppState) => async ( + const addFilter = (state: ContextQueryState) => async ( field: IndexPatternField | string, values: unknown, operation: string diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/state.ts b/src/plugins/discover/public/application/angular/context/query_parameters/state.ts index d56602b301774..0bb12f950b82e 100644 --- a/src/plugins/discover/public/application/angular/context/query_parameters/state.ts +++ b/src/plugins/discover/public/application/angular/context/query_parameters/state.ts @@ -7,17 +7,19 @@ */ export function createInitialQueryParametersState( + indexPatternId: string, + anchorId: string, defaultStepSize: number = 5, tieBreakerField: string = '_doc' ) { return { - anchorId: null, + anchorId, columns: [], defaultStepSize, filters: [], - indexPatternId: null, - predecessorCount: 0, - successorCount: 0, + indexPatternId, + predecessorCount: 5, + successorCount: 5, sort: [], tieBreakerField, }; diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index 5ee1fac7a0bd7..d5b7279e5dcec 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -3,11 +3,13 @@ filter="contextApp.actions.addFilter" hits="contextApp.state.rows.all" index-pattern="contextApp.indexPattern" + index-pattern-id="contextApp.indexPatternId" app-state="contextApp.appState" state-container="contextApp.stateContainer" sorting="contextApp.state.queryParameters.sort" columns="contextApp.state.queryParameters.columns" minimum-visible-rows="contextApp.state.rows.all.length" + anchor-id="contextApp.anchorId" anchor-status="contextApp.state.loadingStatus.anchor.status" anchor-reason="contextApp.state.loadingStatus.anchor.reason" default-step-size="contextApp.state.queryParameters.defaultStepSize" diff --git a/src/plugins/discover/public/application/angular/context_app.js b/src/plugins/discover/public/application/angular/context_app.js index 7c9c5f8ce4b42..d868156b5e82d 100644 --- a/src/plugins/discover/public/application/angular/context_app.js +++ b/src/plugins/discover/public/application/angular/context_app.js @@ -34,6 +34,7 @@ getAngularModule().directive('contextApp', function ContextApp() { anchorId: '=', columns: '=', indexPattern: '=', + indexPatternId: '=', appState: '=', stateContainer: '=', filters: '=', @@ -89,13 +90,13 @@ function ContextAppController($scope, Private) { newQueryParameters.anchorId !== queryParameters.anchorId || !_.isEqual(newQueryParameters.sort, queryParameters.sort) ) { - this.actions.fetchAllRowsWithNewQueryParameters(_.cloneDeep(newQueryParameters)); + // this.actions.fetchAllRowsWithNewQueryParameters(_.cloneDeep(newQueryParameters)); } else if ( newQueryParameters.predecessorCount !== queryParameters.predecessorCount || newQueryParameters.successorCount !== queryParameters.successorCount || !_.isEqual(newQueryParameters.filters, queryParameters.filters) ) { - this.actions.fetchContextRowsWithNewQueryParameters(_.cloneDeep(newQueryParameters)); + // this.actions.fetchContextRowsWithNewQueryParameters(_.cloneDeep(newQueryParameters)); } } ); diff --git a/src/plugins/discover/public/application/angular/context_app_state.ts b/src/plugins/discover/public/application/angular/context_query_state.ts similarity index 64% rename from src/plugins/discover/public/application/angular/context_app_state.ts rename to src/plugins/discover/public/application/angular/context_query_state.ts index 0d9d6d6ea5978..bf2b435d025b5 100644 --- a/src/plugins/discover/public/application/angular/context_app_state.ts +++ b/src/plugins/discover/public/application/angular/context_query_state.ts @@ -10,8 +10,10 @@ import { Filter } from '../../../../data/public'; import { EsHitRecord } from './context/api/context'; import { EsHitRecordList } from './context/api/context'; import { SortDirection } from './context/api/utils/sorting'; +import { createInitialLoadingStatusState } from './context/query'; +import { createInitialQueryParametersState } from './context/query_parameters'; -export interface ContextAppState { +export interface ContextQueryState { loadingStatus: LoadingStatusState; queryParameters: QueryParameters; rows: ContextRows; @@ -55,6 +57,35 @@ export interface QueryParameters { interface ContextRows { all: EsHitRecordList; anchor: EsHitRecord; - predecessors: EsHitRecordList; - successors: EsHitRecordList; + // predecessors: EsHitRecordList; + // successors: EsHitRecordList; +} + +export function getContextQueryDefaults( + indexPatternId: string, + anchorId: string, + defaultStepSize: number, + tieBreakerField: string, + useNewFieldsApi: boolean +): ContextQueryState { + return { + queryParameters: createInitialQueryParametersState( + indexPatternId, + anchorId, + defaultStepSize, + tieBreakerField + ), + rows: { + all: [], + anchor: { + fields: [], + sort: [], + _id: '', + }, + // predecessors: [], + // successors: [], + }, + loadingStatus: createInitialLoadingStatusState(), + useNewFieldsApi, + }; } diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index 9cfea7f01e4ab..9b1a52cdb4e0a 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -16,26 +16,27 @@ import { withNotifyOnErrors, ReduxLikeStateContainer, } from '../../../../kibana_utils/public'; -import { esFilters, FilterManager, Filter, Query } from '../../../../data/public'; +import { esFilters, FilterManager, Filter, Query, SortDirection } from '../../../../data/public'; import { handleSourceColumnState } from './helpers'; +import { ContextQueryState } from './context_query_state'; -export interface AppState { +export interface AppState extends ContextQueryState { /** * Columns displayed in the table, cannot be changed by UI, just in discover's main app */ - columns: string[]; + columns?: string[]; /** * Array of filters */ - filters: Filter[]; + filters?: Filter[]; /** * Number of records to be fetched before anchor records (newer records) */ - predecessorCount: number; + predecessorCount?: number; /** * Sorting of the records to be fetched, assumed to be a legacy parameter */ - sort: string[][]; + sort: [[string, SortDirection]]; /** * Number of records to be fetched after the anchor records (older records) */ @@ -54,7 +55,7 @@ export interface GetStateParams { /** * Number of records to be fetched when 'Load' link/button is clicked */ - defaultStepSize: string; + defaultStepSize: number; /** * The timefield used for sorting */ @@ -79,6 +80,11 @@ export interface GetStateParams { * core ui settings service */ uiSettings: IUiSettingsClient; + + /** + * Default state used for data querying + */ + getContextQueryDefaults: () => ContextQueryState; } export interface GetStateReturn { @@ -130,6 +136,7 @@ export function getState({ history, toasts, uiSettings, + getContextQueryDefaults, }: GetStateParams): GetStateReturn { const stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, @@ -145,7 +152,8 @@ export function getState({ defaultStepSize, timeFieldName, appStateFromUrl, - uiSettings + uiSettings, + getContextQueryDefaults ); const appStateContainer = createStateContainer(appStateInitial); @@ -267,17 +275,19 @@ function getFilters(state: AppState | GlobalState): Filter[] { * default state. The default size is the default number of successor/predecessor records to fetch */ function createInitialAppState( - defaultSize: string, + defaultSize: number, timeFieldName: string, urlState: AppState, - uiSettings: IUiSettingsClient + uiSettings: IUiSettingsClient, + getContextQueryDefaults: () => ContextQueryState ): AppState { - const defaultState = { + const defaultState: AppState = { columns: ['_source'], filters: [], - predecessorCount: parseInt(defaultSize, 10), - sort: [[timeFieldName, 'desc']], - successorCount: parseInt(defaultSize, 10), + predecessorCount: defaultSize, + sort: [[timeFieldName, SortDirection.desc]], + successorCount: defaultSize, + ...getContextQueryDefaults(), }; if (typeof urlState !== 'object') { return defaultState; diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx index c6edc8fe68b9d..95fd080480603 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx @@ -6,11 +6,12 @@ * Side Public License, v 1. */ -import React, { useState, Fragment } from 'react'; +import React, { useState, Fragment, useEffect, useRef } from 'react'; import classNames from 'classnames'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import './context_app_legacy.scss'; import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; +import { cloneDeep, isEqual } from 'lodash'; import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY } from '../../../../common'; import { ContextErrorMessage } from '../context_error_message'; import { @@ -18,7 +19,7 @@ import { DocTableLegacyProps, } from '../../angular/doc_table/create_doc_table_react'; import { IndexPattern } from '../../../../../data/common/index_patterns'; -import { LoadingStatus } from '../../angular/context_app_state'; +import { LoadingStatus } from '../../angular/context_query_state'; import { ActionBar, ActionBarProps } from '../../angular/context/components/action_bar/action_bar'; import { TopNavMenuProps } from '../../../../../navigation/public'; import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid'; @@ -27,17 +28,22 @@ import { getServices, SortDirection } from '../../../kibana_services'; import { GetStateReturn, AppState } from '../../angular/context_state'; import { useDataGridColumns } from '../../helpers/use_data_grid_columns'; import { EsHitRecord, EsHitRecordList } from '../../angular/context/api/context'; +import { useContextAppState } from './use_context_app_state'; +import { useContextAppQuery } from './use_context_app_query'; +import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../angular/context/query_parameters'; export interface ContextAppProps { topNavMenu: React.ComponentType; columns: string[]; hits: EsHitRecordList; indexPattern: IndexPattern; + indexPatternId: string; appState: AppState; stateContainer: GetStateReturn; filter: DocViewFilterFn; minimumVisibleRows: number; sorting: Array<[string, SortDirection]>; + anchorId: string; anchorStatus: string; anchorReason: string; predecessorStatus: string; @@ -60,18 +66,22 @@ function isLoading(status: string) { return status !== LoadingStatus.LOADED && status !== LoadingStatus.FAILED; } +function clamp(minimum: number, maximum: number, value: number) { + return Math.max(Math.min(maximum, value), minimum); +} + export function ContextAppLegacy(renderProps: ContextAppProps) { const services = getServices(); const { uiSettings: config, capabilities, indexPatterns } = services; const { indexPattern, + indexPatternId, anchorStatus, predecessorStatus, successorStatus, - appState, - stateContainer, - hits: rows, + // hits: rows, sorting, + anchorId, filter, minimumVisibleRows, useNewFieldsApi, @@ -84,15 +94,64 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { predecessorStatus === LoadingStatus.LOADED && successorStatus === LoadingStatus.LOADED; const isLegacy = config.get(DOC_TABLE_LEGACY); - const anchorId = rows?.find(({ isAnchor }) => isAnchor)?._id; + // const anchorId = rows?.find(({ isAnchor }) => isAnchor)?._id; + + const { state, stateContainer, setAppState } = useContextAppState({ + indexPattern, + indexPatternId, + anchorId, + services, + }); + const prevState = useRef(); + + useEffect(() => { + stateContainer.startSync(); + + return () => stateContainer.stopSync(); + }, [stateContainer]); + + const { context$, fetchAnchorRow, fetchAllRows } = useContextAppQuery({ + services, + useNewFieldsApi: !!useNewFieldsApi, + state, + }); + + /** + * Fetch docs + */ + useEffect(() => { + if (!prevState.current) { + fetchAllRows(); + } else if ( + prevState.current.predecessorCount !== state.predecessorCount || + prevState.current.successorCount !== state.successorCount || + !isEqual(prevState.current.filters, state.filters) + ) { + fetchAllRows(); + } + + prevState.current = cloneDeep(state); + }, [state, fetchAllRows, fetchAnchorRow, indexPatternId]); + + /** + * Sync app state with context$ + */ + useEffect(() => { + context$.subscribe((next) => { + setAppState(next); + }); + + return () => context$.unsubscribe(); + }, [context$, setAppState]); + const rows = state.rows; const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ capabilities, config, indexPattern, indexPatterns, - setAppState: stateContainer.setAppState, - state: appState, + setAppState, + state, useNewFieldsApi: !!useNewFieldsApi, }); @@ -122,7 +181,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { return { ariaLabelledBy: 'surDocumentsAriaLabel', columns, - rows: allRowsLoaded && rows, + rows: rows.all.length !== 0 && rows.all, indexPattern, expandedDoc, isLoading: !allRowsLoaded, diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts index 04484c179ecb2..7ac8a7fd5e881 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts @@ -14,11 +14,13 @@ export function createContextAppLegacy(reactDirective: any) { ['filter', { watchDepth: 'reference' }], ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], + ['indexPatternId', { watchDepth: 'reference' }], ['appState', { watchDepth: 'reference' }], ['stateContainer', { watchDepth: 'reference' }], ['sorting', { watchDepth: 'reference' }], ['columns', { watchDepth: 'collection' }], ['minimumVisibleRows', { watchDepth: 'reference' }], + ['anchorId', { watchDepth: 'reference' }], ['anchorStatus', { watchDepth: 'reference' }], ['anchorReason', { watchDepth: 'reference' }], ['defaultStepSize', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_query.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_query.tsx new file mode 100644 index 0000000000000..1463438c87df5 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_query.tsx @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { fromPairs } from 'lodash'; +import { Subject } from 'rxjs'; +import { DiscoverServices } from '../../../build_services'; +import { AppState, getState } from '../../angular/context_state'; +import { fetchAnchorProvider } from '../../angular/context/api/anchor'; +import { + EsHitRecord, + EsHitRecordList, + fetchContextProvider, + SurrDocType, +} from '../../angular/context/api/context'; +import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public'; + +export type ContextAppMessage = Partial; + +export function useContextAppQuery({ + services, + useNewFieldsApi, + state, +}: { + services: DiscoverServices; + state: AppState; + useNewFieldsApi: boolean; +}) { + const { data, indexPatterns, toastNotifications, filterManager } = services; + + const searchSource = useMemo(() => { + return data.search.searchSource.createEmpty(); + }, [data.search.searchSource]); + + const fetchAnchor = useMemo(() => { + return fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi); + }, [indexPatterns, searchSource, useNewFieldsApi]); + + const { fetchSurroundingDocs } = useMemo( + () => fetchContextProvider(indexPatterns, useNewFieldsApi), + [indexPatterns, useNewFieldsApi] + ); + + const context$ = useMemo(() => new Subject(), []); + + const fetchAnchorRow = useCallback(() => { + const { + queryParameters: { indexPatternId, anchorId, tieBreakerField }, + sort, + } = state; + + if (!tieBreakerField) { + // reject + } + + // set loading + const [[, sortDir]] = sort; + + return fetchAnchor(indexPatternId, anchorId, [ + fromPairs(sort), + { [tieBreakerField]: sortDir }, + ]).then( + (anchorDocument: EsHitRecord) => { + // set loaded + context$.next({ + rows: { + ...state.rows, + anchor: anchorDocument, + }, + }); + }, + (error: Error) => { + // set errors + toastNotifications.addDanger({ + title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { + defaultMessage: 'Unable to load the anchor document', + }), + text: toMountPoint({error.message}), + }); + throw error; + } + ); + }, [fetchAnchor, state, toastNotifications, context$]); + + const fetchSurroundingRows = useCallback( + (type: SurrDocType) => { + const { + queryParameters: { indexPatternId, tieBreakerField }, + rows: { anchor }, + sort, + } = state; + const filters = filterManager.getFilters(); + + const count = + type === 'successors' + ? state.queryParameters.successorCount + : state.queryParameters.predecessorCount; + + if (!tieBreakerField) { + // reject request + } + + // set loading + const [[sortField, sortDir]] = sort; + + return fetchSurroundingDocs( + type, + indexPatternId, + anchor, + sortField, + tieBreakerField, + sortDir, + count, + filters + ).then( + (documents: EsHitRecordList) => { + // set loaded + return documents; + }, + (error: Error) => { + // set error + toastNotifications.addDanger({ + title: i18n.translate('discover.context.unableToLoadDocumentDescription', { + defaultMessage: 'Unable to load documents', + }), + text: toMountPoint({error.message}), + }); + throw error; + } + ); + }, + [toastNotifications, fetchSurroundingDocs, filterManager, state] + ); + + const fetchAllRows = useCallback(() => { + fetchAnchorRow()?.then(() => { + Promise.all([fetchSurroundingRows('predecessors'), fetchSurroundingRows('successors')]).then( + ([predecessors, successors]) => { + context$.next({ + rows: { + ...state.rows, + all: [ + ...(predecessors || []), + ...(state.rows.anchor ? [state.rows.anchor] : []), + ...(successors || []), + ], + }, + }); + } + ); + }); + }, [fetchAnchorRow, fetchSurroundingRows, state, context$]); + + return { context$, fetchAnchorRow, fetchAllRows }; +} diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_state.ts b/src/plugins/discover/public/application/components/context_app/use_context_app_state.ts new file mode 100644 index 0000000000000..65157565318c9 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_state.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { useEffect, useMemo, useState } from 'react'; + +import { cloneDeep } from 'lodash'; +import { IndexPattern } from '../../../../../data/public'; +import { DiscoverServices } from '../../../build_services'; +import { AppState, getState } from '../../angular/context_state'; +import { getContextQueryDefaults } from '../../angular/context_query_state'; +import { + CONTEXT_DEFAULT_SIZE_SETTING, + CONTEXT_TIE_BREAKER_FIELDS_SETTING, + SEARCH_FIELDS_FROM_SOURCE, +} from '../../../../common'; +import { getFirstSortableField } from '../../angular/context/api/utils/sorting'; + +export function useContextAppState({ + indexPattern, + indexPatternId, + anchorId, + services, +}: { + indexPattern: IndexPattern; + indexPatternId: string; + anchorId: string; + services: DiscoverServices; +}) { + const { uiSettings: config, history, core, filterManager } = services; + + const stateContainer = useMemo(() => { + const defaultStepSize = parseInt(config.get(CONTEXT_DEFAULT_SIZE_SETTING), 10); + return getState({ + defaultStepSize, + timeFieldName: indexPattern.timeFieldName as string, + storeInSessionStorage: config.get('state:storeInSessionStorage'), + history: history(), + toasts: core.notifications.toasts, + uiSettings: config, + getContextQueryDefaults: () => + getContextQueryDefaults( + indexPatternId, + anchorId, + defaultStepSize, + getFirstSortableField(indexPattern, config.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING)), + !config.get(SEARCH_FIELDS_FROM_SOURCE) + ), + }); + }, [config, history, indexPattern, anchorId, indexPatternId, core.notifications.toasts]); + + const [state, setState] = useState(stateContainer.appState.getState()); + + /** + * Sync app state + */ + useEffect(() => { + // take care of context state updates + const unsubscribeAppState = stateContainer.appState.subscribe(async (newState) => { + setState(newState); + }); + + return () => unsubscribeAppState(); + }, [stateContainer, setState, filterManager]); + + /** + * Take care of filters + */ + useEffect(() => { + // sync initial app filters from state to filterManager + const filters = stateContainer.appState.getState().filters; + if (filters) { + filterManager.setAppFilters(cloneDeep(filters)); + } + + const { setFilters } = stateContainer; + const filterObservable = filterManager.getUpdates$().subscribe(() => { + setFilters(filterManager); + }); + + return () => filterObservable.unsubscribe(); + }, [filterManager, stateContainer]); + + return { + state, + stateContainer, + setAppState: stateContainer.setAppState, + }; +} diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx index e4b5e3b95c3f3..91d77c76d828c 100644 --- a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { ReactWrapper } from 'enzyme'; import { ContextErrorMessage } from './context_error_message'; -import { FailureReason, LoadingStatus } from '../../angular/context_app_state'; +import { FailureReason, LoadingStatus } from '../../angular/context_query_state'; import { findTestSubject } from '@elastic/eui/lib/test'; describe('loading spinner', function () { diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx b/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx index 85dc67e7029ee..0a75ae34d958f 100644 --- a/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiCallOut, EuiText } from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { FailureReason, LoadingStatus } from '../../angular/context_app_state'; +import { FailureReason, LoadingStatus } from '../../angular/context_query_state'; export interface ContextErrorMessageProps { /**